<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hb_een.log</title>
        <link>https://velog.io/</link>
        <description>FE = 현빈</description>
        <lastBuildDate>Mon, 09 Feb 2026 07:01:10 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>hb_een.log</title>
            <url>https://velog.velcdn.com/images/hb_een/profile/1e613e1c-08c0-4483-b049-8181129111f7/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. hb_een.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hb_een" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[BEM, 이제 정말 안녕? CSS @scope의 등장!]]></title>
            <link>https://velog.io/@hb_een/BEM-%EC%9D%B4%EC%A0%9C-%EC%A0%95%EB%A7%90-%EC%95%88%EB%85%95-CSS-scope%EC%9D%98-%EB%93%B1%EC%9E%A5</link>
            <guid>https://velog.io/@hb_een/BEM-%EC%9D%B4%EC%A0%9C-%EC%A0%95%EB%A7%90-%EC%95%88%EB%85%95-CSS-scope%EC%9D%98-%EB%93%B1%EC%9E%A5</guid>
            <pubDate>Mon, 09 Feb 2026 07:01:10 GMT</pubDate>
            <description><![CDATA[<p>와, 다들 스매싱 매거진에 오늘 올라온 이 글 보셨어요? 아침에 커피 마시다가 정신이 번쩍 드네요. 드디어 CSS <code>@scope</code>가 나왔습니다.</p>
<p>맨날 BEM으로 클래스명 길게 짓거나 CSS Modules, 혹은 Styled Components 쓰면서 스타일 충돌 막으려고 애썼잖아요. 저만 그런 거 아니죠? 그런데 이제 네이티브 CSS만으로 컴포넌트의 스타일 범위를 딱 지정해서 가둬버릴 수 있게 됐어요. 특정 컴포넌트 안에서만 스타일이 먹히게 하는 거죠.</p>
<p>예를 들어 <code>.card</code> 컴포넌트 안의 <code>h2</code> 태그에만 스타일을 주고 싶으면, 이제 전역 오염 걱정 없이 깔끔하게 처리할 수 있다는 뜻입니다. 이거 진짜 물건이네요. 무거운 라이브러리나 복잡한 빌드 과정 없이도 컴포넌트 기반 스타일링이 훨씬 직관적으로 변하겠어요.</p>
<p>물론 당장 모든 프로젝트에 도입하긴 어렵겠지만, 이 기능이 가져올 변화의 바람은 무시 못 할 것 같습니다. <code>@scope</code>를 실무에 활용하기 위한 제 생각 몇 가지를 공유해 볼게요.</p>
<ul>
<li><strong>디자인 시스템 컴포넌트에 우선 적용해 보세요.</strong> 버튼, 카드, 인풋 필드처럼 재사용성이 높고 독립적이어야 하는 컴포넌트부터 <code>@scope</code>로 리팩토링하면 효과가 가장 클 겁니다. 스타일이 외부로 새어 나가거나 내부를 침범할 걱정이 확 줄어들겠죠.</li>
<li><strong>기존 CSS 방법론과 섞어서 써보세요.</strong> 처음부터 모든 걸 바꿀 순 없으니, 특히 스타일 충돌이 잦았던 복잡한 레이아웃이나 위젯 영역에만 부분적으로 <code>@scope</code>를 도입해서 점진적으로 개선해 나가는 전략이 현실적일 것 같아요.</li>
<li><strong>&#39;Donut Scope&#39; 개념을 이해해 두세요.</strong> <code>@scope</code>는 특정 영역을 시작점으로 잡고, 특정 영역을 끝점으로 지정해서 그 사이의 요소에만 스타일을 적용하는 것도 가능해요. 이걸 &#39;도넛 스코프&#39;라고 부르던데, 중첩된 컴포넌트 구조에서 아주 유용하게 쓰일 것 같습니다.</li>
</ul>
<p>오랜만에 CSS 네이티브에 이렇게 설레는 기능이 추가된 것 같네요. 더 이상 클래스명 짓느라 머리 싸매지 않아도 되는 날이 올지도 모르겠습니다.</p>
<p>다들 어떻게 생각하세요? 이거 실무에 바로 써먹을 수 있을까요? 댓글로 자유롭게 의견 나눠봐요!</p>
<p>🔗 <strong>원문 보기:</strong> <a href="https://smashingmagazine.com/2026/02/css-scope-alternative-naming-conventions/">https://smashingmagazine.com/2026/02/css-scope-alternative-naming-conventions/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🚨 React 19 사용자 필독! 취약점 분석 및 대응]]></title>
            <link>https://velog.io/@hb_een/React-19-%EC%82%AC%EC%9A%A9%EC%9E%90-%ED%95%84%EB%8F%85-%EC%B7%A8%EC%95%BD%EC%A0%90-%EB%B6%84%EC%84%9D-%EB%B0%8F-%EB%8C%80%EC%9D%91</link>
            <guid>https://velog.io/@hb_een/React-19-%EC%82%AC%EC%9A%A9%EC%9E%90-%ED%95%84%EB%8F%85-%EC%B7%A8%EC%95%BD%EC%A0%90-%EB%B6%84%EC%84%9D-%EB%B0%8F-%EB%8C%80%EC%9D%91</guid>
            <pubDate>Tue, 13 Jan 2026 07:26:12 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요! 오늘은 최근 프론트엔드 생태계를 뜨겁게 달군 <strong>React Server Components(RSC)</strong> 관련 치명적인 보안 취약점, <strong>CVE-2025-55182</strong>에 대해 정리해 보려고 합니다.</p>
<p>React 19 및 Next.js App Router를 사용하고 계신다면 이 글을 꼭 확인하시고, <strong>즉시 업데이트</strong>하시길 권장합니다.</p>
<hr>
<h2 id="🚨-cve-2025-55182">🚨 CVE-2025-55182?</h2>
<p>2025년 12월 초, React Server Components(RSC) 구현체에서 <strong>치명적인 원격 코드 실행(RCE)</strong> 취약점이 발견되었습니다.</p>
<ul>
<li><strong>CVE ID</strong>: CVE-2025-55182</li>
<li><strong>별칭</strong>: React2Shell</li>
<li><strong>심각도</strong>: <strong>Critical (CVSS 10.0 / 10.0)</strong></li>
<li><strong>영향 범위</strong>: React 19.0.0 ~ 19.2.0 버전을 사용하는 RSC 환경</li>
</ul>
<p>이 취약점은 해커가 인증 절차 없이 단 하나의 HTTP 요청만으로 서버에서 임의의 코드를 실행할 수 있게 만듭니다. 이미 야생에서 랜섬웨어 배포 등에 악용된 사례가 보고되기도 했습니다.</p>
<h2 id="🐛-상세-내용">🐛 상세 내용</h2>
<p>이 문제는 React Server Components가 클라이언트로부터 받은 요청을 처리할 때, <strong>데이터 역직렬화(Deserialization) 과정</strong>에서 발생합니다.</p>
<p>구체적으로 <code>react-server-dom-webpack</code>, <code>react-server-dom-turbopack</code> 등의 패키지가 페이로드 검증을 제대로 수행하지 않아, 공격자가 조작된 데이터를 보내면 서버 내부 로직을 오염시키고 임의의 자바스크립트 코드를 실행할 수 있게 됩니다.</p>
<h3 id="영향받는-버전">영향받는 버전</h3>
<p>다음 버전의 React를 사용 중이라면 위험합니다.</p>
<ul>
<li><code>19.0.0</code></li>
<li><code>19.1.0</code>, <code>19.1.1</code></li>
<li><code>19.2.0</code></li>
</ul>
<p>특히 <strong>Next.js (App Router)</strong> 와 같이 RSC를 기본적으로 사용하는 프레임워크가 주된 타겟이 됩니다.</p>
<h2 id="🛡️-대응-방법">🛡️ 대응 방법</h2>
<p>다행히 React 팀에서 빠르게 패치를 배포했습니다. <strong>가장 확실하고 유일한 해결책은 버전을 올리는 것입니다.</strong></p>
<h3 id="1-즉시-업데이트-권장">1. 즉시 업데이트 (권장)</h3>
<p>사용 중인 패키지 매니저에 맞춰 <code>react</code>와 <code>react-dom</code>을 최신 패치 버전(19.2.1 이상)으로 업데이트하세요.</p>
<p><strong>npm</strong></p>
<pre><code class="language-bash">npm install react@latest react-dom@latest</code></pre>
<p><strong>yarn</strong></p>
<pre><code class="language-bash">yarn upgrade react react-dom</code></pre>
<p><strong>pnpm</strong></p>
<pre><code class="language-bash">pnpm upgrade react react-dom</code></pre>
<h3 id="2-버전-확인">2. 버전 확인</h3>
<p>업데이트 후, <code>package.json</code>이나 락 파일에서 버전이 <strong>19.2.1</strong> (또는 19.0.1, 19.1.2) 이상인지 꼭 확인해 주세요.</p>
<h2 id="📝-마치며">📝 마치며</h2>
<p>프론트엔드 영역, 특히 서버 사이드 렌더링(SSR)과 RSC가 보편화되면서 프론트엔드 개발자도 서버 보안에 대해 더 민감하게 반응해야 할 시점인 것 같습니다.</p>
<p>우리 프로젝트도 확인해 보니 <code>19.2.0</code>을 사용 중이라 식겁해서 바로 업데이트를 진행했네요. 😅 여러분의 프로젝트도 지금 바로 확인해 보세요!</p>
<hr>
<p><strong>Reference</strong></p>
<ul>
<li><a href="https://nvd.nist.gov/vuln/detail/CVE-2025-55182">NVD - CVE-2025-55182</a></li>
<li>React 공식 블로그 및 보안 권고 사항</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA["왜 에러도 없이 멈출까?" - 프론트엔드 개발자가 자주 놓치는 Promise의 함정]]></title>
            <link>https://velog.io/@hb_een/%EC%99%9C-%EC%97%90%EB%9F%AC%EB%8F%84-%EC%97%86%EC%9D%B4-%EB%A9%88%EC%B6%9C%EA%B9%8C-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%9E%90%EC%A3%BC-%EB%86%93%EC%B9%98%EB%8A%94-Promise%EC%9D%98-%ED%95%A8%EC%A0%95</link>
            <guid>https://velog.io/@hb_een/%EC%99%9C-%EC%97%90%EB%9F%AC%EB%8F%84-%EC%97%86%EC%9D%B4-%EB%A9%88%EC%B6%9C%EA%B9%8C-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%9E%90%EC%A3%BC-%EB%86%93%EC%B9%98%EB%8A%94-Promise%EC%9D%98-%ED%95%A8%EC%A0%95</guid>
            <pubDate>Mon, 12 Jan 2026 23:45:45 GMT</pubDate>
            <description><![CDATA[<p>개발자라면 누구나 한 번쯤 겪어봤을 법한 상황이었습니다.
채팅 기능을 열심히 구현했고, <strong>이미지 업로드 기능</strong>까지 붙여서 배포 준비를 마치고 있었죠. 로컬 테스트에선 모든 게 완벽해 보였습니다.</p>
<p>그런데 간헐적으로 이상한 리포트가 들어오기 시작했습니다.</p>
<blockquote>
<p>&quot;어떤 사진을 선택하면 미리보기가 안 뜨고 그냥 <strong>가만히 있어요</strong>.&quot;</p>
</blockquote>
<p>보통 버그라면 앱이 죽거나(Crash), 콘솔에 빨간색 에러 로그라도 떠야 하는데,
이 녀석은 <strong>아무런 반응도, 로그도 없이</strong> 그저 조용히 멈춰버리는 것이 특징이었습니다.</p>
<p>범인은 바로 범용적으로 쓰던 이미지 압축 함수 속 <strong>&quot;끝나지 않는 Promise&quot;</strong>였습니다.</p>
<h2 id="원인-추적">원인 추적</h2>
<p>제가 구현하려던 로직은 아주 일반적인 흐름이었습니다.</p>
<ol>
<li>사용자가 <code>&lt;input type=&quot;file&quot; /&gt;</code>을 통해 채팅창에 보낼 사진을 선택합니다.</li>
<li>선택된 파일이 너무 클 수 있으니, <code>processImage</code> 함수가 <strong>리사이징 및 압축</strong>을 수행합니다.</li>
<li>압축이 완료되면 결과물을 받아 화면에 <strong>미리보기(Preview)</strong> 썸네일을 띄웁니다.</li>
</ol>
<p>대부분의 이미지는 잘 동작했지만, <strong>특정 고화질 이미지나 일부 손상된 파일</strong>을 선택했을 때 <strong>2번 단계</strong>에서 코드가 멈춰버렸습니다.
함수가 영원히 종료되지 않으니(Hang), 당연히 3번 단계인 &#39;미리보기 출력&#39; 코드는 실행될 기회조차 얻지 못했던 것이죠.</p>
<h2 id="원인-분석">원인 분석</h2>
<p>기존 <code>compressImage</code> 함수를 살펴보니, <code>Promise</code>를 반환하면서 내부적으로 <code>new Image()</code>를 생성해 로드하고 있었습니다.</p>
<h3 id="기존-코드">기존 코드</h3>
<pre><code class="language-typescript">const compressImage = (file: File): Promise&lt;File&gt; =&gt; {
  return new Promise(resolve =&gt; {
    const img = new Image()

    img.onload = () =&gt; {
      resolve(compressedFile) 
    }

    img.src = URL.createObjectURL(file)
  })
}</code></pre>
<p><strong>문제의 핵심</strong>:
브라우저가 <code>img.src</code>에 할당된 Blob URL을 해석하지 못하거나 로드에 실패할 경우 <code>onload</code> 이벤트가 발생하지 않습니다. 이때 <code>onerror</code> 핸들러가 없으면 이 <strong>Promise는 영원히 <code>pending</code> 상태</strong>로 남게 됩니다.</p>
<p>결국 <code>await compressImage(file)</code> 부분에서 코드가 멈춰버려, 그 뒤에 있는 <code>setPreviewImageSrc</code> 로직이 실행되지 않았던 것입니다.</p>
<h2 id="해결-방법">해결 방법</h2>
<p>두 가지를 개선하여 코드를 수정했습니다.</p>
<ol>
<li><strong><code>onerror</code> 핸들러 추가</strong>: 이미지 로드 실패 시, 압축을 포기하고 <strong>원본 파일 그대로 <code>resolve</code></strong> 하도록 처리하여 로직이 멈추지 않게 함.</li>
<li><strong>메모리 누수 방지</strong>: <code>URL.createObjectURL</code>로 만든 객체는 사용 후 반드시 <code>URL.revokeObjectURL</code>로 해제해 주어야 하므로, 성공/실패 모든 케이스에 cleanup 로직 추가.</li>
</ol>
<h3 id="수정된-코드">수정된 코드</h3>
<pre><code class="language-typescript">const compressImage = (file: File): Promise&lt;File&gt; =&gt; {
  return new Promise(resolve =&gt; {
    const canvas = document.createElement(&#39;canvas&#39;)
    const ctx = canvas.getContext(&#39;2d&#39;)
    const img = new Image()

    const objectUrl = URL.createObjectURL(file)


    img.onload = () =&gt; {
      canvas.toBlob(blob =&gt; {
        URL.revokeObjectURL(objectUrl)
        if (blob) {
          const compressedFile = new File([blob], file.name, {
            type: &#39;image/jpeg&#39;,
            lastModified: Date.now(),
          })
          resolve(compressedFile)
        } else {
          resolve(file)
        }
      }, &#39;image/jpeg&#39;, 0.8)
    }

    img.onerror = () =&gt; {
      URL.revokeObjectURL(objectUrl)

      resolve(file)
    }

    img.src = objectUrl
  })
}</code></pre>
<h2 id="결과">결과</h2>
<p>이제 손상된 이미지나 브라우저가 압축할 수 없는 포맷이 들어오더라도, <code>catch</code>나 <code>onerror</code>를 타고 원본 파일로 fallback 처리되므로 <strong>프리뷰가 즉시 정상적으로 표시</strong>됩니다.</p>
<p>프론트엔드에서 <code>Promise</code>를 직접 생성(<code>new Promise</code>)해서 사용할 때는, <code>resolve</code>뿐만 아니라 예외 케이스(<code>reject</code> 혹은 fallback <code>resolve</code>)를 반드시 고려해야 함을 다시 한번 상기하게 되었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next 배포한다고? 이거 안보면 손해]]></title>
            <link>https://velog.io/@hb_een/%ED%8C%8C%EC%9D%BC%EB%9F%BF-%EC%B6%9C%EC%8B%9C%EB%A1%9C-%EA%B9%A8%EB%8B%AC%EC%9D%80-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-SSR-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%A9%94%ED%8A%B8%EB%A6%AD%EC%9D%98-%EC%A4%91%EC%9A%94%EC%84%B1</link>
            <guid>https://velog.io/@hb_een/%ED%8C%8C%EC%9D%BC%EB%9F%BF-%EC%B6%9C%EC%8B%9C%EB%A1%9C-%EA%B9%A8%EB%8B%AC%EC%9D%80-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-SSR-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%A9%94%ED%8A%B8%EB%A6%AD%EC%9D%98-%EC%A4%91%EC%9A%94%EC%84%B1</guid>
            <pubDate>Tue, 06 Jan 2026 01:08:31 GMT</pubDate>
            <description><![CDATA[<p>최근 프로젝트의 파일럿 버전을 AWS EC2 기반으로 성공적으로 출시했습니다. 배포 후 실제 사용자들이 유입되면서, 인스턴스 위에서 돌아가는 SSR(Server-Side Rendering) 서버의 안정성을 유지하기 위해 하드웨어 지표를 실시간으로 살피는 것이 얼마나 중요한지 체감했습니다.</p>
<p>특히 EC2 인스턴스를 직접 운영하면서, 하드웨어 성능이 곧 서비스 응답 속도와 직결된다는 것을 뼈저리게 배웠습니다. CloudWatch를 뚫어지게 보며 분석했던 4대 핵심 지표와 실전 기록을 공유합니다.</p>
<hr>
<h2 id="cpu-usage-t3-인스턴스의-크레딧-소진과-ttfb의-함수-관계">CPU Usage: t3 인스턴스의 크레딧 소진과 TTFB의 함수 관계</h2>
<p>파일럿 초기, 저렴한 t3 인스턴스를 사용하다가 특정 시점에 페이지 응답 속도(TTFB)가 갑자기 수 초대로 늘어나는 현상을 겪었습니다.</p>
<p><strong>CPU Credits &amp; Burst</strong> 
t계열 인스턴스 특유의 &#39;CPU 크레딧&#39;이 모두 소진되자 성능이 기본 수준으로 베이스라인화되면서 SSR 렌더링 속도가 처참하게 느려졌습니다. 프로덕션급 SSR 서버에는 c5나 m5 같은 전용(Dedicated) 인스턴스 타입이 왜 권장되는지 실감했습니다.</p>
<p><strong>Event Loop Lag 모니터링</strong>
CloudWatch의 CPU 사용률만 봐서는 알 수 없는 <strong>Node.js Event Loop Delay</strong>가 100ms를 돌파하고 있었습니다. CPU가 100%가 아니더라도 싱글 스레드가 복잡한 연산에 묶이면 전체 요청이 밀린다는 점을 확인하고 로직을 최적화했습니다.</p>
<p>Prometheus 설정 과정은 다음 게시물에 올리겠습니다</p>
<h2 id="memory-usage-스왑swap-공간과-불시의-프로세스-종료">Memory Usage: 스왑(Swap) 공간과 불시의 프로세스 종료</h2>
<p>운영 중 Node.js 서버 프로세스가 조용히 종료(Crash)되는 현상이 발생했습니다. 범인은 메모리 부족으로 인한 리눅스 커널의 자가방어였습니다.</p>
<p> <strong>OOM Killer와 dmesg</strong>
인스턴스 메모리가 꽉 차자 리눅스 커널이 가장 메모리를 많이 먹는 Node.js 프로세스를 죽여버렸습니다. <code>dmesg</code> 명령어로 OOM(Out Of Memory) 킬러의 흔적을 발견하고 깜짝 놀랐습니다.</p>
<p> <strong>Swap File 설정의 명암</strong>
메모리 부족 시 바로 죽지 않도록 EBS에 스왑 파일을 생성해 두었으나, 디스크를 메모리처럼 쓰니 I/O 부하가 치솟으며 서버가 &#39;좀비 상태&#39;가 되었습니다. 결국 인스턴스 RAM 용량을 올리고 누수가 발생하는 전역 캐시 로직을 걷어냈습니다.</p>
<h2 id="network-bandwidth-데이터-전송-비용egress의-압박">Network Bandwidth: 데이터 전송 비용(Egress)의 압박</h2>
<p>파일럿 서비스임에도 불구하고 월말 AWS 청구서에서 &#39;Data Transfer Out&#39; 항목이 예상치를 훌쩍 상회했습니다.</p>
<p><strong>Egress 비용 최적화</strong>
EC2에서 사용자의 브라우저로 나가는 데이터 전송 비용이 상당했습니다. 완성된 HTML 크기를 줄이기 위해 Gzip/Brotli 압축을 적용하고, 거대했던 API 응답 데이터를 BFF(Backend For Frontend) 레이어에서 파이썬처럼 필요한 것만 발췌해 렌더링하도록 수정했습니다.</p>
<p><strong>ALB와 커넥션 유지</strong>
Application Load Balancer(ALB)와 EC2 간의 Keep-Alive 설정을 최적화하여 잦은 연결 수립으로 인한 오버헤드를 줄였습니다.</p>
<h2 id="disk-usage--io-ebs-성능과-isr-페이지-갱신">Disk Usage &amp; I/O: EBS 성능과 ISR 페이지 갱신</h2>
<p>Next.js의 ISR(Incremental Static Regeneration) 기능을 쓰면서 디스크 성능이 렌더링 지연에 영향을 줄 수 있다는 것을 알게 되었습니다.</p>
<p><strong>EBS IOPS의 벽</strong>
새로운 페이지를 생성하고 디스크에 쓰는 과정에서 EBS의 초당 입출력 횟수(IOPS)가 부족하면 <code>I/O Wait</code>이 발생했습니다. 이는 CPU가 연산을 못 하고 디스크 작업이 끝나길 기다리게 만들어 전체 응답 속도를 떨어뜨렸습니다.</p>
<p><strong>로그 로테이션(Logrotate)</strong>
<code>pm2</code> 프로세스 매니저가 뿜어내는 로그가 EBS 용량을 가득 채워 서버가 멈추는 사고를 방지하기 위해, 주기적으로 S3로 로그를 넘기고 로컬은 비우는 정책을 세웠습니다.</p>
<hr>
<h2 id="파일럿은-시작일-뿐입니다">파일럿은 시작일 뿐입니다.</h2>
<p>이번 파일럿 출시를 통해 단순히 브라우저 단의 성능 최적화를 넘어, <strong>AWS 인프라의 관점</strong>에서 서비스를 바라보는 법을 배웠습니다.</p>
<p><strong>CloudWatch 대시보드</strong>의 꺾은선 그래프 하나하나가 우리 사용자가 느끼는 실제 반응 속도임을 깨달았습니다. EC2 인프라를 직접 만지며 얻은 이 값진 수치들을 바탕으로, 정식 버전에서는 더욱 탄탄하고 효율적인 아키텍처를 구축하려 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[모노레포에서 에러 추적 다들 어떻게 사용하시나요?]]></title>
            <link>https://velog.io/@hb_een/Monorepo%EC%97%90%EC%84%9C-Sentry-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9C%BC%EB%A1%9C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hb_een/Monorepo%EC%97%90%EC%84%9C-Sentry-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9C%BC%EB%A1%9C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 05 Jan 2026 02:35:51 GMT</pubDate>
            <description><![CDATA[<p>Turborepo 같은 모노레포 환경에서 Sentry를 세팅하려고 하면 생각보다 머리가 아픕니다.</p>
<p>하지만 저는 기존에 세팅해둔 Shared Package 전략이 있어 한번 세팅을 해보려합니다. (이전 게시글 참고)</p>
<p>sentry 공식 홈페이지에서도 모노레포를 위한 문서가 딱히 없어 걱정반 설렘 반으로 작업을 시작했네요.</p>
<p>그 과정 바로 공유해 드릴게요.</p>
<hr>
<h2 id="왜-그냥-설치하지-않고-패키지로-만드나요">왜 그냥 설치하지 않고 &quot;패키지&quot;로 만드나요?</h2>
<p>Shared Package 전략글에 상세히 설명되어 있어 간단히 짚고만 가겠습니다.</p>
<p>보통은 <code>npx @sentry/wizard</code>를 각 앱에서 돌리지만, 모노레포에선 이게 <strong>기술 부채</strong>가 됩니다.</p>
<ul>
<li><strong>중복 설정</strong>: <code>beforeSend</code> 로직이나 에러 마스킹 수정을 모든 앱에서 반복해야 합니다.</li>
<li><strong>버전 불일치</strong>: A 앱은 Sentry v7, B 앱은 Sentry v10을 쓰는 혼란이 생깁니다.</li>
<li><strong>파편화된 DSN</strong>: DSN 주소 하나 바뀌면 모든 <code>.env</code>를 뒤져야 하죠.</li>
</ul>
<p>우리는 <code>@repo/sentry</code>라는 이름의 독립된 공간에서 이 모든 걸 관리할 겁니다.</p>
<hr>
<h2 id="shared-package-구축하기">Shared Package 구축하기</h2>
<p><code>packages/sentry</code>에 자리를 잡고, 모든 앱이 가져다 쓸 수 있는 &quot;Sentry 컨트롤 타워&quot;를 만듭니다.</p>
<h3 id="통합-entry-point-설계-indexts">통합 Entry Point 설계 (<code>index.ts</code>)</h3>
<p>단순히 초기화만 하는 게 아니라, 서버/클라이언트/엣지 환경에 맞춰 최적화된 초기화 함수를 정의합니다.</p>
<p>센트리 옵션은 공식문서, 블로그 등 많은 곳에세 이미 정리되어 있기에 index.ts에 어떻게 합쳤는지만 보여드리겠습니다.</p>
<pre><code class="language-typescript">import * as Sentry from &quot;@sentry/nextjs&quot;;

// 중앙에서 관리하는 DSN
const SENTRY_DSN = &#39;https://...&#39;; 

export const initSentryClient = () =&gt; {
  Sentry.init({
    dsn: SENTRY_DSN,
    tracesSampleRate: 1.0,
    integrations: [Sentry.consoleLoggingIntegration({ levels: [&quot;log&quot;, &quot;warn&quot;, &quot;error&quot;] })],
    enableLogs: true,
  });
};

export const initSentryServer = () =&gt; {
  Sentry.init({
    dsn: SENTRY_DSN,
    tracesSampleRate: 1.0,
  });
};

// ...Edge 로직도 동일하게</code></pre>
<h3 id="nextconfigjs-옵션-공통화"><code>next.config.js</code> 옵션 공통화</h3>
<p>빌드 시 소스맵 업로드나 터널링(광고 차단 우회) 설정도 패키지에서 미리 정의해 둡니다.</p>
<pre><code class="language-typescript">export const sentryConfigOptions = {
  org: &quot;조직명&quot;,
  project: &quot;프로젝트명&quot;,
  silent: !process.env.CI,
  tunnelRoute: &quot;/monitoring&quot;,
};</code></pre>
<hr>
<h2 id="앱에서-간단하게-적용하기">앱에서 간단하게 적용하기</h2>
<p>현재 저는 Next.js 15+ 버전을 사용중이기에 최신 규격에 맞게 <code>instrumentation.ts</code>를 활용했습니다.</p>
<p>instrumentation 훅은 아래와 같으니 참고해보세요.</p>
<h3 id="서버와-클라이언트를-동시에-잡는-법">서버와 클라이언트를 동시에 잡는 법</h3>
<p><strong><code>instrumentation.ts</code> (Server)</strong></p>
<pre><code class="language-typescript">import * as Sentry from &#39;@repo/sentry&#39;;

export async function register() {
  if (process.env.NEXT_RUNTIME === &#39;nodejs&#39;) {
    const { initSentryServer } = await import(&#39;@repo/sentry&#39;);
    initSentryServer();
  }
}
export const onRequestError = Sentry.captureRequestError;</code></pre>
<p><strong><code>instrumentation-client.ts</code> (Client)</strong></p>
<pre><code class="language-typescript">import * as Sentry from &#39;@repo/sentry&#39;;
Sentry.initSentryClient();
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;</code></pre>
<hr>
<h2 id="여담">여담</h2>
<p>요번 작업에서 제 스스로 잘 도입했다 싶은 기능들을 아래에 정리하였으니 참고해보세요.</p>
<ol>
<li><strong>Tunneling</strong>: &quot;광고 차단기에 의해 로그가 유실되는 걸 막기 위해 Rewrites를 통해 터널링을 구축&quot;</li>
<li><strong>Shared Package</strong>: &quot;모노레포 환경에서 일관된 에러 트래킹을 위해 Sentry 설정을 공통 패키지화하여 DX를 개선&quot;</li>
<li><strong>Source Maps</strong>: &quot;CI/CD 단계에서 자동 소스맵 업로드, 배포 후에도 난독화된 코드 대신 원본 코드로 에러 분석&quot;</li>
</ol>
<hr>
<h2 id="마무리하며">마무리하며</h2>
<p>Sentry는 단순히 에러를 쌓아두는 창고가 아닙니다. 잘 세팅된 Sentry는 개발자가 잠든 사이에도 서비스의 건강도를 체크해 주는 소중한 동료가 되죠.</p>
<p>오늘 정리해 드린 <strong>모노레포 Sentry 설정</strong>으로 여러분의 DX를 한 단계 높여보세요!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Changeset이 말합니다. "패키지 변경되었다니까?"]]></title>
            <link>https://velog.io/@hb_een/Changeset%EC%9D%B4-%EB%A7%90%ED%95%A9%EB%8B%88%EB%8B%A4.-%ED%8C%A8%ED%82%A4%EC%A7%80-%EB%B3%80%EA%B2%BD%EB%90%98%EC%97%88%EB%8B%A4%EB%8B%88%EA%B9%8C</link>
            <guid>https://velog.io/@hb_een/Changeset%EC%9D%B4-%EB%A7%90%ED%95%A9%EB%8B%88%EB%8B%A4.-%ED%8C%A8%ED%82%A4%EC%A7%80-%EB%B3%80%EA%B2%BD%EB%90%98%EC%97%88%EB%8B%A4%EB%8B%88%EA%B9%8C</guid>
            <pubDate>Tue, 23 Dec 2025 00:45:30 GMT</pubDate>
            <description><![CDATA[<p><code>changeset</code>을 사용하다 보면, 분명 나는 한 개의 패키지만 수정했는데 <code>pnpm changeset</code> 명령어를 실행했을 때 수정하지 않은 다른 패키지들이 목록에 모두 나타나거나 자동으로 선택되는 경우가 있습니다.</p>
<p>오늘은 이 문제의 원인과 해결 방법에 대해 정리해 보겠습니다.</p>
<hr>
<h2 id="문제-상황">문제 상황</h2>
<ul>
<li>특정 브랜치(예: <code>feature/bridge-fix</code>)에서 작업을 완료한 후 <code>pnpm changeset</code> 실행.</li>
<li>나는 <code>A</code> 패키지만 수정했는데, changeset 목록에는 <code>B</code>, <code>C</code>, <code>D</code> 등 프로젝트 내 거의 모든 패키지가 포함됨.</li>
<li>실수로 그대로 진행할 경우, 의도치 않은 패키지들의 버전이 일괄적으로 올라가는 대참사가 발생할 수 있음.</li>
</ul>
<h2 id="원인-분석-basebranch-설정">원인 분석: <code>baseBranch</code> 설정</h2>
<p><code>changeset</code>은 현재 브랜치의 변경 사항을 감지하기 위해 <strong>기준이 되는 브랜치</strong>와 비교를 수행합니다. 이 기준은 <code>.changeset/config.json</code> 파일의 <code>baseBranch</code> 속성에 정의되어 있습니다.</p>
<p>보통 기본값은 <code>main</code> 또는 <code>master</code>로 되어 있습니다.</p>
<pre><code class="language-json">// .changeset/config.json
{
  &quot;$schema&quot;: &quot;https://unpkg.com/@changesets/config@3.0.0/schema.json&quot;,
  &quot;baseBranch&quot;: &quot;main&quot;,
  ...
}</code></pre>
<h3 id="왜-문제가-발생하나요">왜 문제가 발생하나요?</h3>
<p>만약 여러분의 작업 흐름(Workflow)이 다음과 같다면 문제가 발생합니다.</p>
<ol>
<li><code>main</code> 브랜치에서 <code>dev</code> 브랜치가 파생됨.</li>
<li><code>dev</code> 브랜치에 여러 피처 브랜치들이 머지됨 (이 과정에서 많은 패키지들의 코드가 수정됨).</li>
<li>아직 <code>dev</code> 브랜치의 내용이 <code>main</code>으로 머지되지 않음.</li>
<li>나는 <code>dev</code> 브랜치에서 새로운 <code>feature/my-task</code> 브랜치를 따서 작업함.</li>
<li><code>pnpm changeset</code> 실행.</li>
</ol>
<p>이때 changeset은 <strong><code>feature/my-task</code>와 <code>main</code>을 비교</strong>합니다. <code>dev</code>에 이미 머지되어 있던 모든 변경 사항들이 <code>main</code>에는 없기 때문에, changeset 입장에서는 그 모든 것들이 &quot;이번 작업에서 변경된 사항&quot;으로 간주되는 것입니다.</p>
<h2 id="해결-포인트">해결 포인트</h2>
<h3 id="basebranch-수정"><code>baseBranch</code> 수정</h3>
<p>만약 프로젝트의 주요 개발 및 머지 기준 브랜치가 <code>dev</code>라면, changeset 설정에서도 이를 명시해 주어야 합니다.</p>
<pre><code class="language-json">// .changeset/config.json 수정
{
  &quot;baseBranch&quot;: &quot;dev&quot;,
  ...
}</code></pre>
<p>이렇게 수정하면 changeset이 현재 브랜치를 <code>dev</code>와 비교하게 되어, 내가 실제로 이번 태스크에서 수정한 파일이 포함된 패키지만 정확하게 필터링해 줍니다.</p>
<h2 id="마치며">마치며</h2>
<p>changeset은 버전 관리와 Changelog 생성을 자동화해 주는 아주 편리한 도구입니다. 하지만 잘못된 기준 설정은 의도치 않은 버전 펌핑(Version Bumping)을 유발할 수 있으므로, 프로젝트의 브랜치 전략에 맞춰 <code>config.json</code>을 꼭 확인해 보세요!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ESM 방식(type: module)을 쓰니 모듈 탐색이 안된다?!]]></title>
            <link>https://velog.io/@hb_een/%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC%EC%97%90%EC%84%9C-Prettier-Tailwind-CSS-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%A0%95%EB%A0%AC%EC%9D%B4-%EC%95%88-%EB%90%A0-%EB%95%8C-Format-on-Save-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@hb_een/%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC%EC%97%90%EC%84%9C-Prettier-Tailwind-CSS-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%A0%95%EB%A0%AC%EC%9D%B4-%EC%95%88-%EB%90%A0-%EB%95%8C-Format-on-Save-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Mon, 22 Dec 2025 06:31:30 GMT</pubDate>
            <description><![CDATA[<p>pnpm이랑 Turbo로 모노레포 세팅하고 Prettier 공통 설정을 패키지로 빼서 쓰다 보면, 희한하게 터미널에선 잘 되는데 VS Code에서만 <strong>Format on Save</strong>가 안 먹힐 때가 있습니다.</p>
<p>Prettier 로그를 열어보면 대충 이런 에러가 떠있죠.</p>
<blockquote>
<p><strong>Error: Cannot find module &#39;prettier-plugin-tailwindcss&#39;</strong></p>
</blockquote>
<p>분명 공통 설정 패키지(<code>@repo/prettier-config</code>)에는 설치를 해뒀는데, VS Code 확장 프로그램이 이걸 못 찾아서 생기는 문제입니다. 앱마다 똑같은 플러그인을 중복으로 깔자니 모노레포 쓰는 의미가 퇴색되는 것 같고... 이럴 때 깔끔하게 해결하는 방법을 공유합니다.</p>
<hr>
<h2 id="왜-못-찾는-걸까">왜 못 찾는 걸까?</h2>
<p>VS Code의 Prettier 확장은 현재 작업 중인 파일의 위치나 프로젝트 루트에서 플러그인을 찾으려고 합니다. 그런데 모노레포 구조에선 실제 플러그인이 저 멀리 공통 패키지의 <code>node_modules</code> 안에 숨어있으니 확장 프로그램 입장에선 &quot;플러그인 어디 있어?&quot; 하고 뻗어버리는 거죠.</p>
<p>특히 ESM 방식(<code>type: module</code>)을 쓰면 모듈 탐색이 더 까다로워지는데, 이걸 해결하려면 <strong>절대 경로</strong>를 직접 꽂아주는 게 가장 확실합니다.</p>
<hr>
<h2 id="해결-절대-경로-주입하기">해결: 절대 경로 주입하기</h2>
<p><code>createRequire</code>를 써서 플러그인이 설치된 실제 경로를 찾아낸 다음, Prettier 설정의 <code>plugins</code> 배열에 넘겨주면 됩니다.</p>
<h3 id="packagesprettier-configbasejs-수정"><code>packages/prettier-config/base.js</code> 수정</h3>
<pre><code class="language-javascript">import { createRequire } from &#39;module&#39;

const require = createRequire(import.meta.url)

export default {
  singleQuote: true,
  semi: false,
  tabWidth: 2,
  trailingComma: &#39;es5&#39;,
  printWidth: 80,
  bracketSpacing: true,
  arrowParens: &#39;avoid&#39;,
  endOfLine: &#39;auto&#39;,
  // 이름만 적는 게 아니라 require.resolve로 실제 파일 위치를 넘겨줍니다.
  plugins: [require.resolve(&#39;prettier-plugin-tailwindcss&#39;)],
}</code></pre>
<p>이렇게 하면 <code>prettier-config</code> 패키지가 있는 위치를 기준으로 플러그인을 찾아서 절대 경로를 반환합니다. VS Code 확장은 이 경로를 보고 바로 플러그인을 로드할 수 있게 됩니다.</p>
<hr>
<h2 id="적용-결과">적용 결과</h2>
<p>수정하고 나서 VS Code에서 <code>Cmd + Shift + P</code> -&gt; <strong>Developer: Reload Window</strong> 한 번 해주세요.</p>
<p>이제 하단 바에 Prettier가 정상 작동하면서, 파일 저장할 때마다 Tailwind 클래스 정렬이랑 포맷팅이 아주 시원하게 잘 돌아갈 겁니다.</p>
<h3 id="요약">요약</h3>
<ul>
<li>각 앱마다 플러그인 깔지 말고 공통 설정 패키지 하나에서 관리하자.</li>
<li>인식이 안 되면 <code>require.resolve</code>로 절대 경로를 주입하면 끝.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA["아니, 이걸 매번 설치한다고?" Prettier & ESLint 완벽 중앙화 가이드]]></title>
            <link>https://velog.io/@hb_een/%EC%95%84%EB%8B%88-%EC%9D%B4%EA%B1%B8-%EB%A7%A4%EB%B2%88-%EC%84%A4%EC%B9%98%ED%95%9C%EB%8B%A4%EA%B3%A0-Prettier-ESLint-%EC%99%84%EB%B2%BD-%EC%A4%91%EC%95%99%ED%99%94-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@hb_een/%EC%95%84%EB%8B%88-%EC%9D%B4%EA%B1%B8-%EB%A7%A4%EB%B2%88-%EC%84%A4%EC%B9%98%ED%95%9C%EB%8B%A4%EA%B3%A0-Prettier-ESLint-%EC%99%84%EB%B2%BD-%EC%A4%91%EC%95%99%ED%99%94-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Mon, 22 Dec 2025 01:20:57 GMT</pubDate>
            <description><![CDATA[<p>Turborepo를 도입하여 모노레포 환경을 구축하고, 프로젝트가 어느 정도 안정화 단계에 접어들었습니다. 초기 세팅이 끝나고 개발 사이클이 원활하게 돌아가기 시작하자, 이제는 <strong>&quot;더 효율적인 구조로 개선할 부분은 없을까?&quot;</strong> 하며 코드를 다시 살펴보게 되었습니다.</p>
<p>전체적인 의존성 관리 상태를 점검하던 중, 최적화할 수 있는 포인트 하나를 발견했습니다. 바로 <strong>Prettier와 ESLint 관련 플러그인의 관리 방식</strong>이었습니다.</p>
<h2 id="불필요한-중복-발견">불필요한 중복 발견</h2>
<p>현재 구조에서도 린팅과 포맷팅은 잘 동작하고 있었습니다. 하지만 각 앱(<code>apps/*</code>)의 <code>package.json</code>을 열어보니 불필요한 중복이 눈에 띄었습니다.</p>
<pre><code class="language-json">// apps/client/package.json
{
  &quot;devDependencies&quot;: {
    &quot;prettier&quot;: &quot;^3.0.0&quot;,
    &quot;prettier-plugin-tailwindcss&quot;: &quot;^0.5.0&quot;, // 중복 Prettier Plugin
    &quot;eslint-plugin-react&quot;: &quot;^7.33.0&quot;, // 중복 ESLint Plugin
    &quot;@next/eslint-plugin-next&quot;: &quot;^14.0.0&quot;, // 중복...
    &quot;@repo/prettier-config&quot;: &quot;workspace:*&quot;,
    &quot;@repo/eslint-config&quot;: &quot;workspace:*&quot;
  }
}</code></pre>
<p>이미 <code>@repo/*-config</code>라는 공통 패키지를 만들어 두었음에도 불구하고, 정작 플러그인은 각 프로젝트마다 개별적으로 설치해서 사용하고 있었습니다.</p>
<blockquote>
<p><strong>&quot;모노레포의 장점은 의존성 중앙 관리인데, 서드파티 플러그인까지 각 프로젝트가 일일이 들고 있을 필요가 있을까? 공통 패키지에서 한 번에 제공해줄 수 있지 않을까?&quot;</strong></p>
</blockquote>
<p>이 부분을 개선하면 각 서비스 프로젝트는 비즈니스 로직에만 더 집중할 수 있는 가벼운 상태가 될 것이라 판단했습니다.</p>
<h2 id="고도화-준비">고도화 준비</h2>
<p>단순히 동작하게 만드는 단계를 넘어, <strong>구조적 효율성</strong>을 높이는 것을 목표로 했습니다.</p>
<ul>
<li><strong>AS-IS</strong>: 공통 설정(<code>config</code>)은 중앙에서, 실행 도구(<code>plugin</code>)는 각 앱에서 관리.</li>
<li><strong>TO-BE</strong>: 설정과 실행 도구 모두 <strong>공통 패키지</strong>가 캡슐화하여 제공. 각 앱은 &#39;사용&#39;만 함.</li>
</ul>
<h2 id="prettier">Prettier</h2>
<h3 id="packagejson-파일">package.json 파일</h3>
<p>먼저 흩어져 있던 의존성을 한곳으로 모으는 작업을 진행했습니다. <code>apps</code>에 있는 플러그인을 제거하고, 이를 <code>packages/prettier-config</code>로 옮깁니다.</p>
<pre><code class="language-bash">cd packages/prettier-config
pnpm add prettier-plugin-tailwindcss</code></pre>
<pre><code class="language-json">// packages/prettier-config/package.json
{
  &quot;name&quot;: &quot;@repo/prettier-config&quot;,
  &quot;dependencies&quot;: {
    // 플러그인을 이곳의 직접 의존성으로 선언합니다.
    &quot;prettier-plugin-tailwindcss&quot;: &quot;^0.7.2&quot;
  }
}</code></pre>
<h3 id="config-파일">Config 파일</h3>
<p>이 부분이 Prettier 설정 개선의 핵심입니다. 단순히 의존성만 옮기면 Prettier가 실행될 때 플러그인을 찾지 못하는 문제가 발생합니다
<strong>(Prettier는 실행 컨텍스트의 <code>node_modules</code>를 탐색하기 때문입니다)</strong></p>
<p>이를 해결하기 위해 <strong>설정 파일 내부에서 플러그인을 직접 주입</strong>하는 방식으로 변경했습니다.</p>
<pre><code class="language-javascript">// packages/prettier-config/base.js
import * as tailwindPlugin from &quot;prettier-plugin-tailwindcss&quot;;

export default {
  // ... 기존 포맷팅 룰
  plugins: [
    tailwindPlugin, // 문자열 이름 대신, import한 플러그인 객체를 직접 할당
  ],
};</code></pre>
<p>이렇게 하면 각 앱에 플러그인이 설치되어 있지 않아도, Config 패키지가 내부적으로 플러그인을 품고 있기 때문에 정상적으로 동작하게 됩니다.</p>
<blockquote>
<p>ESM 환경 호환성을 위해 <code>import * as</code> 문법을 사용하여 안정적으로 모듈을 로드했습니다.</p>
</blockquote>
<h3 id="각-서비스-프로젝트-경량화">각 서비스 프로젝트 경량화</h3>
<ul>
<li><strong>package.json</strong>: <code>prettier-plugin-tailwindcss</code> 의존성 삭제</li>
<li><strong>prettier.config.js</strong>:</li>
</ul>
<pre><code class="language-javascript">// apps/admin/prettier.config.js
import baseConfig from &quot;@repo/prettier-config&quot;;

export default {
  ...baseConfig, // 완벽하게 캡슐화된 설정을 상속
};</code></pre>
<h2 id="eslint">ESLint</h2>
<p>Prettier를 정리하고 나니 ESLint도 마찬가지였습니다. <code>eslint-plugin-react</code>, <code>eslint-plugin-react-hooks</code>, <code>@next/eslint-plugin-next</code> 등 수많은 의존성이 각 앱마다 중복 설치되어 있었습니다.</p>
<h3 id="packagejson">package.json</h3>
<p>ESLint 역시 공통 패키지(<code>@repo/eslint-config</code>)로 모든 플러그인을 이동시켰습니다. 이때 주의할 점은, <strong>이 공유 패키지를 설치하는 소비 앱들이 플러그인을 찾을 수 있게 하려면 <code>dependencies</code>에 플러그인을 명시해야 한다</strong>는 것입니다.</p>
<pre><code class="language-json">// packages/eslint-config/package.json
{
  &quot;name&quot;: &quot;@repo/eslint-config&quot;,
  &quot;dependencies&quot;: {
    // devDependencies가 아닌 dependencies에 넣어야 합니다.
    &quot;@eslint/js&quot;: &quot;^9.39.1&quot;,
    &quot;@next/eslint-plugin-next&quot;: &quot;^15.4.6&quot;,
    &quot;eslint-plugin-react&quot;: &quot;^7.37.4&quot;,
    &quot;eslint-plugin-react-hooks&quot;: &quot;^5.2.0&quot;,
    &quot;typescript-eslint&quot;: &quot;^8.39.0&quot;
  },
  &quot;peerDependencies&quot;: {
    &quot;eslint&quot;: &quot;^9.39.1&quot;
  }
}</code></pre>
<h3 id="flat-config">Flat Config</h3>
<p>ESLint v9의 Flat Config 시스템을 사용하였습니다. 공통 패키지에서 <strong>완성된 설정 객체(Config Object)</strong> 배열을 export 해주면 됩니다.</p>
<p>하지만 이 과정에서 기존 레거시 설정(eslintrc)과는 다른 몇 가지 작업을 해주어야 했습니다.</p>
<p><strong>1. React Prop Types 경고 끄기</strong>
TypeScript를 사용하는 환경에서는 <code>prop-types</code> 검사가 불필요한데, <code>eslint-plugin-react</code>가 기본적으로 이 규칙을 켜둡니다. 중앙 설정에서 이를 명시적으로 꺼주어야 합니다.</p>
<p><strong>2. Plugins/Rules 정의</strong>
Flat Config에서는 플러그인을 객체로 직접 매핑하고, 규칙을 정의합니다.</p>
<pre><code class="language-javascript">// packages/eslint-config/next.js
import pluginNext from &quot;@next/eslint-plugin-next&quot;;
import pluginReact from &quot;eslint-plugin-react&quot;;
// ... imports

export const nextJsConfig = [
  ...baseConfig,
  {
    plugins: {
      &quot;@next/next&quot;: pluginNext, // 플러그인 객체 매핑
    },
    rules: {
      ...pluginNext.configs.recommended.rules,
      // TypeScript 프로젝트이므로 prop-types 검사 비활성화
      &quot;react/prop-types&quot;: &quot;off&quot;,
    },
  },
  // ...
];</code></pre>
<h3 id="각-서비스-프로젝트-경량화-1">각 서비스 프로젝트 경량화</h3>
<p>이제 앱에서는 복잡한 플러그인이나 <code>extends</code>를 볼 필요가 없습니다.</p>
<ul>
<li><strong>package.json</strong>: <code>eslint</code> 본체와 <code>@repo/eslint-config</code>만 남기고 나머지 플러그인 삭제</li>
<li><strong>eslint.config.mjs</strong>:</li>
</ul>
<pre><code class="language-javascript">import { nextJsConfig } from &quot;@repo/eslint-config/next-js&quot;;

export default nextJsConfig;</code></pre>
<p>간단하게 수십 개의 규칙과 플러그인 설정이 적용됩니다.</p>
<h2 id="개선-결과">개선 결과</h2>
<p>리팩토링 후 <code>pnpm format</code>과 <code>pnpm lint</code>를 전체 실행해 보았습니다. 결과는 성공적이었습니다.</p>
<ol>
<li><strong>관리 포인트 일원화</strong>: Prettier와 ESLint 관련 플러그인 버전을 올릴 때, 이제 <strong>한 곳(<code>packages</code>)</strong>만 수정하면 모든 앱에 반영됩니다.</li>
<li><strong>보일러플레이트 감소</strong>: 새로운 프로젝트를 생성할 때 반복적인 플러그인 세팅 과정이 사라졌습니다.</li>
<li><strong>구조적 완성도</strong>: 설정 패키지가 정말 설정에 필요한 모든 것을 책임지는 구조가 되었습니다.</li>
</ol>
<p>모노레포 구축은 초기 세팅이 끝이 아니라, 이렇게 <strong>운영하면서 발견되는 중복을 하나씩 제거하고 구조를 다듬어가는 과정</strong>에서 진정한 가치가 드러나는 것 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[팀 내 유일한 프론트엔드 개발자가 회사에서 살아남는 법]]></title>
            <link>https://velog.io/@hb_een/%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC%EC%97%90-Simplified-Git-Flow%EC%99%80-Changesets-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hb_een/%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC%EC%97%90-Simplified-Git-Flow%EC%99%80-Changesets-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 19 Dec 2025 02:12:24 GMT</pubDate>
            <description><![CDATA[<h2 id="프론트엔드-개발자님-이거-언제-배포되나요">&quot;프론트엔드 개발자님, 이거 언제 배포되나요?&quot;</h2>
<p>저희 회사는 빠르게 성장하고 있는 조직입니다. AI 서비스 특성상 관리자 대시보드, B2C 클라이언트, 파트너사 페이지까지 총 3개의 제품을 운영하고 있죠.</p>
<p>문제는 <strong>프론트엔드 개발자가 저 혼자</strong>라는 점이었습니다.</p>
<p>기획자, 디자이너, 백엔드 개발자분들이 쏟아내는 요구사항을 혼자 감당해야 하는데, 버전 관리까지 수동으로 하다 보니 병목이 생기기 시작했습니다.</p>
<ul>
<li><strong>높은 컨텍스트 스위칭 비용</strong>: 하루에도 몇 번씩 앱을 오가며 작업하다 보니, &quot;어? 이거 패치했나?&quot; 헷갈리는 순간이 잦아졌습니다.</li>
<li><strong>배포 리스크</strong>: 급하게 핫픽스를 배포하다가 다른 앱에 사이드 이펙트가 발생한 적이 있습니다. 등골이 서늘했죠.</li>
<li><strong>문서화의 부재</strong>: 바쁘다는 핑계로 CHANGELOG 작성을 미루다 보니, 히스토리 파악이 안 돼서 똑같은 문제를 또 고민하게 되더라고요.</li>
</ul>
<p>혼자라고 해서 주먹구구식으로 할 순 없었습니다. 오히려 혼자니까 더 <strong>시스템</strong>이 필요했죠.
<strong>제 리소스를 &#39;운영&#39;이 아닌 &#39;기능 개발&#39;에 온전히 쏟기 위해</strong> 자동화 시스템을 구축하기로 결심했습니다.</p>
<h2 id="나를-복제할-수-없다면-시스템을-만들자">나를 복제할 수 없다면, 시스템을 만들자</h2>
<p>그래서 도입한 것이 <strong>Simplified Git Flow + Changesets + CI/CD</strong> 파이프라인입니다.</p>
<p>도입 후 변화는 명확했습니다.</p>
<p>✅ 릴리스 프로세스 <strong>완전 자동화</strong>
✅ 의존성 패키지 업데이트 실수가 사라짐<br>✅ 어떤 기능이 배포되었는지 전사 공유 가능<br>✅ 나중에 팀원이 충원되어도 바로 적응 가능한 체계 구축</p>
<blockquote>
<p>이제부터 어떻게 &#39;혼자서도 잘 돌아가는 시스템&#39;을 만들었는지 공유 해드릴게요</p>
</blockquote>
<h2 id="제-프로젝트-구조부터-소개할게요">제 프로젝트 구조부터 소개할게요</h2>
<p>먼저 제가 뚝딱거리고 있는 모노레포 구조입니다.</p>
<pre><code>ai-client-mono/
├── apps/
│   ├── ai-agent-admin/      # 관리자 대시보드 (포트: 3000)
│   ├── ai-agent-client/     # 클라이언트 앱 (포트: 3333)
│   └── ai-partner-admin/    # 파트너 관리 (포트: 3001)
├── packages/
│   ├── ui/                  # 공유 UI 컴포넌트
│   ├── eslint-config/       # ESLint 설정
│   ├── prettier-config/     # Prettier 설정
│   ├── tailwind-config/     # Tailwind 설정
│   └── typescript-config/   # TypeScript 설정
└── turbo.json               # Turborepo 설정</code></pre><p>3개의 앱이 <code>packages/ui</code> 같은 공유 패키지를 함께 쓰고 있어요. 그래서 버전 관리가 더 중요했죠.</p>
<h2 id="1인-개발인데-git-flow가-필요할까">1인 개발인데 Git Flow가 필요할까?</h2>
<p>처음엔 고민했습니다. &quot;어차피 나 혼자 다 머지할 건데, 그냥 메인 브랜치 하나면 되지 않나?&quot;</p>
<p>하지만 <strong>안정적인 서비스 운영</strong>을 위해선 타협할 수 없는 기준들이 있었습니다.</p>
<ol>
<li>프로덕션 배포는 언제나 신뢰할 수 있어야 한다. (Main Branch)</li>
<li>개발 중인 기능이 운영 환경을 오염시키면 안 된다. (Dev Branch)</li>
<li>긴급 버그 픽스는 다른 기능 개발과 격리되어야 한다. (Hotfix)</li>
</ol>
<p>그래서 복잡한 Git Flow 대신, <strong>실용성에 초점을 맞춘 Simplified Git Flow</strong>를 정립했습니다.</p>
<h3 id="브랜치-구조">브랜치 구조</h3>
<pre><code>main (프로덕션)
  ↑
dev (개발 통합)
  ↑
feature/* (기능 개발)
hotfix/* (긴급 수정)
release/* (릴리스 준비)</code></pre><h3 id="각-브랜치의-역할">각 브랜치의 역할</h3>
<p><strong>1. main</strong> - 프로덕션 브랜치</p>
<ul>
<li>항상 배포 가능한 상태</li>
<li>직접 커밋 금지 (PR을 통해서만 병합)</li>
<li>태그를 통한 버전 관리</li>
</ul>
<p><strong>2. dev</strong> - 개발 통합 브랜치</p>
<ul>
<li>다음 릴리스를 위한 개발 브랜치</li>
<li>feature 브랜치들이 병합되는 곳</li>
<li>CI/CD를 통한 자동 테스트</li>
</ul>
<p><strong>3. feature/</strong> - 기능 개발 브랜치</p>
<blockquote>
<p>모노레포 특성상 앱별/패키지별로 명확한 네이밍이 필요합니다</p>
</blockquote>
<pre><code class="language-bash"># 앱별 기능
feature/agent-admin/user-management
feature/agent-client/chat-interface
feature/partner-admin/dashboard

# 공유 패키지
feature/ui/button-component
feature/config/eslint-rules

# 공통 기능
feature/monorepo/ci-optimization</code></pre>
<p><strong>4. hotfix/</strong> - 긴급 수정</p>
<pre><code class="language-bash">hotfix/agent-admin/login-bug
hotfix/shared/security-patch</code></pre>
<p><strong>5. release/</strong> - 릴리스 준비</p>
<pre><code class="language-bash">release/v1.2.0
release/2025-12-ai-agent-admin</code></pre>
<h2 id="커밋-컨벤션-히스토리는-회사의-자산이니까요">커밋 컨벤션: 히스토리는 회사의 자산이니까요</h2>
<p>&quot;나중에 보면 알겠지&quot;라는 생각은 프로답지 못합니다. 제가 퇴사하더라도 코드는 남고, 히스토리는 자산이 됩니다.</p>
<p>특히 모노레포 환경에서는 <strong>어떤 앱에 변경사항이 발생했는지</strong> 명확해야 합니다.
그래서 <strong>Conventional Commits + Scope</strong>를 강제하여, 커밋 로그만 봐도 프로젝트 흐름이 보이도록 만들었습니다.</p>
<h3 id="형식">형식</h3>
<pre><code>&lt;type&gt;(&lt;scope&gt;): &lt;subject&gt;</code></pre><h3 id="예시">예시</h3>
<pre><code class="language-bash">feat(agent-admin): 사용자 관리 페이지 추가
fix(agent-client): 채팅 메시지 렌더링 버그 수정
chore(ui): 버튼 컴포넌트 스타일 개선
docs(readme): Git 전략 문서 추가
refactor(partner-admin): 대시보드 코드 리팩토링
perf(shared): Tailwind 설정 최적화</code></pre>
<h3 id="scope-예시">Scope 예시</h3>
<ul>
<li><strong>앱</strong>: <code>agent-admin</code>, <code>agent-client</code>, <code>partner-admin</code></li>
<li><strong>패키지</strong>: <code>ui</code>, <code>config</code>, <code>eslint-config</code>, <code>tailwind-config</code></li>
<li><strong>공통</strong>: <code>monorepo</code>, <code>deps</code>, <code>ci</code></li>
</ul>
<h2 id="changesets-운영-비용을-줄이는-마법">Changesets: 운영 비용을 줄이는 마법</h2>
<p>가장 큰 병목은 <strong>CHANGELOG 관리와 버전 태깅</strong>이었습니다.
기능 개발 후 일일이 문서를 작성하고 버전을 올리는 건, 단순 반복 작업이자 리소스 낭비였죠.</p>
<p>&quot;개발 단계에서 변경사항을 선언하면, 배포는 시스템이 알아서 할 수 없을까?&quot;</p>
<p>이 물음에 대한 답이 <strong>Changesets</strong>이었습니다. 개발자는 코드와 함께 변경 의도를 남기기만 하면 됩니다. 나머지는 도구가 알아서 하니까요.</p>
<h3 id="install">install</h3>
<pre><code class="language-bash">pnpm add -D -w @changesets/cli</code></pre>
<h3 id="init">init</h3>
<p>Changesets를 초기화합니다.</p>
<pre><code class="language-bash">pnpm changeset init</code></pre>
<p>이렇게 init 작업을 하게 되면 <code>.changeset</code> 디렉터리가 생성되고, <code>config.json</code> 파일이 만들어집니다.</p>
<h3 id="configjson-설정">config.json 설정</h3>
<p><code>.changeset/config.json</code> 파일을 프로젝트에 맞게 수정합니다</p>
<pre><code class="language-json">{
  &quot;$schema&quot;: &quot;https://unpkg.com/@changesets/config@3.0.0/schema.json&quot;,
  &quot;changelog&quot;: &quot;@changesets/cli/changelog&quot;,
  &quot;commit&quot;: false,
  &quot;fixed&quot;: [],
  &quot;linked&quot;: [],
  &quot;access&quot;: &quot;restricted&quot;,
  &quot;baseBranch&quot;: &quot;main&quot;,
  &quot;updateInternalDependencies&quot;: &quot;patch&quot;,
  &quot;ignore&quot;: []
}</code></pre>
<h3 id="워크플로우-개발에만-집중하는-환경">워크플로우: 개발에만 집중하는 환경</h3>
<p>이제 Release 프로세스는 이렇게 변했습니다.</p>
<ol>
<li><code>pnpm changeset</code>으로 변경사항 기록 (10초)</li>
<li>PR 생성 및 병합 (코드 리뷰)</li>
<li><strong>나머지는 CI/CD가 처리</strong></li>
</ol>
<p>더 이상 <code>package.json</code> 버전을 수동으로 수정하거나, 히스토리를 찾아 헤맬 필요가 없습니다.
저는 그 시간에 <strong>다음 스프린트의 핵심 기능</strong>을 개발합니다.</p>
<h2 id="자동화로-신뢰성-확보하기">자동화로 신뢰성 확보하기</h2>
<p>혼자 일할 때 가장 위험한 건 <strong>&quot;체크해 줄 사람이 없다&quot;</strong>는 것입니다.
그래서 CI/CD 파이프라인을 구축해 <strong>코드의 품질을 기계가 검증</strong>하도록 했습니다.</p>
<h3 id="안정적인-배포-파이프라인">안정적인 배포 파이프라인</h3>
<ol>
<li><strong>Lint/Build 검사</strong>: 휴먼 에러가 있는 코드는 절대 병합되지 않습니다.</li>
<li><strong>자동 버전 업</strong>: Semantic Versioning 규칙에 따라 정확하게 버전이 올라갑니다.</li>
<li><strong>Release Note 생성</strong>: 배포 내역이 자동으로 문서화되어 팀에 공유됩니다.</li>
</ol>
<p>이제 저는 마음 놓고 배포 버튼을 누를 수 있습니다. <del>시스템이 저를 지켜주니까요.</del></p>
<h2 id="마치며-혼자라고-시스템을-포기하지-마세요">마치며: 혼자라고 시스템을 포기하지 마세요</h2>
<p>&quot;혼자니까 대충 해도 되겠지&quot;라고 타협하는 순간, 기술 부채는 걷잡을 수 없이 커집니다.
특히 빠르게 성장하는 스타트업일수록, 초기 단계의 시스템 구축이 중요하다고 생각합니다.</p>
<p>혼자 고군분투하는 프론트엔드 개발자분들에게 <strong>시간과 안정성</strong>이라는 두 마리 토끼를 분양드립니다 ㅎㅎ</p>
<blockquote>
<p><strong>&quot;나는 혼자여서 시스템이고 코드문화도 즐기지 못해&quot;</strong> 라는 생각보다 나중에 올 팀원 <del>(오긴 올까,,)</del>을 위해 시스템을 구축하고, 코드 문화를 정립해보는 경험도 좋을것 같습니다 🎶</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[3개 프로젝트를 하나로? Monorepo 마이그레이션 실전 후기]]></title>
            <link>https://velog.io/@hb_een/3%EA%B0%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%ED%95%98%EB%82%98%EB%A1%9C-Monorepo-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%8B%A4%EC%A0%84-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@hb_een/3%EA%B0%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%ED%95%98%EB%82%98%EB%A1%9C-Monorepo-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%8B%A4%EC%A0%84-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Thu, 18 Dec 2025 05:16:02 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>&quot;혼자서 3개 프로젝트 관리하다가 미쳐버릴 뻔한 개발자의 생존기&quot;</strong><br>코드 복붙의 늪에서 벗어나 개발 생산성 향상을 달성한 진짜 이야기</p>
</blockquote>
<hr>
<h2 id="또-같은-컴포넌트를-복사하고-있다">&quot;또 같은 컴포넌트를 복사하고 있다...&quot;</h2>
<p>어느 날, 나는 세 번째 프로젝트에서 또다시 같은 컴포넌트를 복사 붙여넣기하고 있었습니다.</p>
<p><strong>&quot;이게 맞나...?&quot;</strong></p>
<p>그리고 며칠 후, 버튼에 버그가 발견되었습니다. 3개 프로젝트를 모두 수정해야 했죠.</p>
<p>더 큰 문제는 <strong>각 프로젝트마다 모든 설정과 의존성을 개별 관리</strong>하고 있었다는 것입니다.</p>
<blockquote>
<p> 😱 3개 프로젝트 × 20개+ 패키지 = 60개+ 중복 설치
😱 node_modules: 150MB × 3 = 450MB
😱 ESLint 설정 3개, Prettier 설정 3개, Tailwind 설정 3개...
😱 설정 하나 바꾸려면? 3곳 모두 수정...</p>
</blockquote>
<p>그때 깨달았습니다.</p>
<p><strong>&quot;이건 지속 가능하지 않다.&quot;</strong></p>
<hr>
<h2 id="monorepo라는-게-있다던데">&quot;Monorepo라는 게 있다던데?&quot;</h2>
<h3 id="monorepo를-처음-들었을-때">Monorepo를 처음 들었을 때</h3>
<p><strong>나:</strong> &quot;Monorepo? 그거 구글이나 페이스북 같은 대기업에서나 쓰는 거 아니야?&quot;<br><strong>동료:</strong> &quot;요즘은 1인 개발자도 많이 쓰던데?&quot;<br><strong>나:</strong> &quot;설마... 나한테도 필요할까?&quot;</p>
<p>이후 Turborepo 문서를 읽으며 깨달았습니다.</p>
<pre><code class="language-text">❌ 기존 방식
- 3개 레포지토리 관리
- 같은 코드 3번 작성
- 버그 수정도 3번
- 의존성 업데이트도 3번
- 정신적 스트레스 ∞

✅ Monorepo
- 1개 레포지토리
- 코드 1번 작성
- 버그 수정 1번
- 의존성 업데이트 1번
- 정신적 평화 ∞</code></pre>
<p><strong>&quot;이거다!&quot;</strong></p>
<hr>
<h2 id="monorepo-도입-검토-및-설계">Monorepo 도입 검토 및 설계</h2>
<h3 id="아키텍처-설계">아키텍처 설계</h3>
<pre><code class="language-text">ai-client-mono/
├── apps/                           # 애플리케이션 레이어
│   ├── ai-agent-admin/            # 독립 배포 가능
│   ├── ai-agent-client/           # 독립 배포 가능
│   └── ai-partner-admin/          # 독립 배포 가능
│
├── packages/                       # 공유 레이어
│   ├── ui/                        # UI 컴포넌트 (shadcn/ui 모듈화)
│   ├── eslint-config/             # 린팅 규칙
│   ├── prettier-config/           # 포맷팅 규칙
│   ├── tailwind-config/           # 디자인 시스템
│   └── typescript-config/         # 타입 설정
│
└── 인프라 레이어
    ├── Turborepo                  # 빌드 오케스트레이션
    └── pnpm workspace             # 의존성 관리</code></pre>
<h3 id="핵심-설계-원칙">핵심 설계 원칙</h3>
<p><strong>Single Source of Truth</strong></p>
<ul>
<li>공통 컴포넌트는 <code>packages/ui</code>에만 존재</li>
<li>설정 파일은 각 <code>packages/*-config</code>에만 존재</li>
</ul>
<p><strong>Loose Coupling</strong></p>
<ul>
<li>각 앱은 독립적으로 배포 가능</li>
<li>공유 패키지 변경이 앱에 자동 반영</li>
</ul>
<hr>
<h2 id="마이그레이션">마이그레이션</h2>
<h3 id="공통-패키지-생성-ui--설정">공통 패키지 생성 (UI + 설정)</h3>
<p>가장 먼저 한 일은 <strong>중복된 모든 것</strong>을 찾는 것이었습니다.</p>
<h4 id="중복-분석">중복 분석</h4>
<pre><code class="language-bash"># UI 컴포넌트 중복
$ grep -r &quot;export const Button&quot; apps/
apps/ai-agent-admin/components/Button.tsx
apps/ai-agent-client/components/Button.tsx
apps/ai-partner-admin/components/Button.tsx

# Input, Select, Dialog, Modal... 총 25개 컴포넌트가 중복!

# 설정 파일 중복
$ find apps -name &quot;eslint.config.js&quot; -o -name &quot;prettier.config.js&quot;
apps/ai-agent-admin/eslint.config.js
apps/ai-agent-admin/prettier.config.js
apps/ai-agent-admin/tailwind.config.ts
apps/ai-agent-admin/tsconfig.json
# ... 각 앱마다 4개씩 = 총 12개 설정 파일!</code></pre>
<h4 id="공유-패키지-전략">공유 패키지 전략</h4>
<p><strong>5개의 공유 패키지로 모듈화:</strong></p>
<pre><code class="language-text">packages/
├── ui/                 # shadcn/ui 컴포넌트 (25개)
├── eslint-config/      # ESLint 규칙
├── prettier-config/    # Prettier 규칙
├── tailwind-config/    # Tailwind 디자인 시스템
└── typescript-config/  # TypeScript 설정</code></pre>
<h4 id="repoui-패키지-생성">@repo/ui 패키지 생성</h4>
<blockquote>
<p><strong>shadcn/ui를 Monorepo 공유 패키지로!</strong></p>
<p>shadcn/ui는 각 프로젝트에 컴포넌트를 복사하는 방식입니다.<br>하지만 Monorepo에서는 이를 <strong>공유 패키지(<code>@repo/ui</code>)로 모듈화</strong>하여<br>모든 앱이 동일한 shadcn/ui 컴포넌트를 사용하게 만들었습니다!</p>
</blockquote>
<p><strong>디렉토리 구조:</strong></p>
<pre><code class="language-text">packages/ui/
├── src/
│   ├── components/          # shadcn/ui 컴포넌트들
│   │   ├── button.tsx      # shadcn/ui button
│   │   ├── input.tsx       # shadcn/ui input
│   │   ├── dialog.tsx      # shadcn/ui dialog
│   │   ├── form.tsx        # shadcn/ui form
│   │   └── ... (25개)
│   ├── lib/
│   │   └── utils.ts        # cn() 등 유틸리티
│   └── styles/
│       ├── adminGlobals.css
│       └── clientGlobals.css
├── package.json
└── components.json         # shadcn/ui 설정</code></pre>
<p><strong>package.json 설정:</strong></p>
<pre><code class="language-json">// packages/ui/package.json
{
  &quot;name&quot;: &quot;@repo/ui&quot;,
  &quot;version&quot;: &quot;0.0.0&quot;,
  &quot;private&quot;: true,
  &quot;exports&quot;: {
    &quot;./components/*&quot;: &quot;./src/components/*.tsx&quot;,
    &quot;./lib/*&quot;: &quot;./src/lib/*.ts&quot;,
    &quot;./styles/*&quot;: &quot;./src/styles/*.css&quot;
  },
  &quot;dependencies&quot;: {
    &quot;@radix-ui/react-slot&quot;: &quot;^1.2.4&quot;,
    &quot;@radix-ui/react-dialog&quot;: &quot;^1.1.15&quot;,
    &quot;class-variance-authority&quot;: &quot;^0.7.1&quot;,
    &quot;clsx&quot;: &quot;^2.1.1&quot;,
    &quot;tailwind-merge&quot;: &quot;^3.3.1&quot;
    // ... shadcn/ui가 사용하는 의존성들
  }
}</code></pre>
<p><strong>모든 앱에서 동일한 shadcn/ui 컴포넌트 사용</strong></p>
<p>shadcn/ui를 <code>@repo/ui</code>로 모듈화하면서 <strong>각 앱의 의존성이 대폭 감소</strong>했습니다!</p>
<pre><code class="language-json">// @repo/ui로 모듈화
// ai-agent-admin/package.json
{
  &quot;dependencies&quot;: {
    &quot;@repo/ui&quot;: &quot;workspace:*&quot;,  // 이것만!
    &quot;next&quot;: &quot;^15.5.6&quot;,
    &quot;react&quot;: &quot;^19.0.2&quot;
    // shadcn 관련 패키지 전부 제거!
  }
}

// ai-agent-client/package.json - 역시 깔끔!
// ai-partner-admin/package.json - 역시 깔끔!

// packages/ui/package.json - 여기에만 shadcn 의존성 존재
{
  &quot;dependencies&quot;: {
    &quot;@radix-ui/react-slot&quot;: &quot;^1.2.4&quot;,
    &quot;@radix-ui/react-dialog&quot;: &quot;^1.1.15&quot;,
    // ... 모든 shadcn 의존성
  }
}</code></pre>
<h3 id="의존성-지옥-탈출">의존성 지옥 탈출</h3>
<p><strong>문제:</strong></p>
<pre><code class="language-json">// ai-agent-admin/package.json
&quot;next&quot;: &quot;14.0.0&quot;

// ai-agent-client/package.json
&quot;next&quot;: &quot;15.0.0&quot;  // 버전이 다름!

// ai-partner-admin/package.json
&quot;next&quot;: &quot;14.2.0&quot;  // 또 다름!</code></pre>
<p><strong>해결책: pnpm workspace로 통합</strong></p>
<pre><code class="language-yaml"># pnpm-workspace.yaml
packages:
  - &quot;apps/*&quot;
  - &quot;packages/*&quot;</code></pre>
<pre><code class="language-json">// 루트 package.json
{
  &quot;dependencies&quot;: {
    &quot;next&quot;: &quot;^15.5.6&quot;, // 하나로 통일!
    &quot;react&quot;: &quot;^19.0.2&quot;
  }
}</code></pre>
<p><strong>결과:</strong></p>
<ul>
<li>버전 충돌 제로</li>
<li><code>node_modules</code> 크기 감소</li>
<li>설치 시간 빨라짐</li>
</ul>
<h3 id="step-3-turborepo-캐싱-마법">Step 3: Turborepo 캐싱 마법</h3>
<p>처음에는 빌드가 너무 느렸습니다.</p>
<p><strong>&quot;이건 아닌데...&quot;</strong></p>
<p>그러다 발견한 Turborepo의 캐싱 기능!</p>
<pre><code class="language-json">// turbo.json
{
  &quot;tasks&quot;: {
    &quot;build&quot;: {
      &quot;outputs&quot;: [&quot;.next/**&quot;, &quot;dist/**&quot;],
      &quot;cache&quot;: true // 이 한 줄이 마법!
    }
  }
}</code></pre>
<p><strong>&quot;와... 이게 진짜 마법이네!&quot;</strong></p>
<hr>
<h2 id="🔧-문제-해결">🔧 문제 해결</h2>
<h3 id="typescript-path-alias-충돌">TypeScript Path Alias 충돌</h3>
<p><strong>문제</strong></p>
<pre><code class="language-typescript">// apps/ai-agent-admin/tsconfig.json
{
  &quot;compilerOptions&quot;: {
    &quot;paths&quot;: {
      &quot;@/*&quot;: [&quot;./src/*&quot;]  // 앱 내부 경로
    }
  }
}

// packages/ui/tsconfig.json
{
  &quot;compilerOptions&quot;: {
    &quot;paths&quot;: {
      &quot;@/*&quot;: [&quot;./src/*&quot;]  // 패키지 내부 경로
    }
  }
}
// ❌ 충돌 발생!</code></pre>
<p><strong>해결</strong></p>
<pre><code class="language-typescript">// packages/ui/tsconfig.json
{
  &quot;compilerOptions&quot;: {
    &quot;paths&quot;: {
      &quot;@/lib/*&quot;: [&quot;./src/lib/*&quot;],
      &quot;@/components/*&quot;: [&quot;./src/components/*&quot;]
    }
  }
}

// turbo.json에도 추가
{
  &quot;tasks&quot;: {
    &quot;build&quot;: {
      &quot;dependsOn&quot;: [&quot;^build&quot;],
      &quot;env&quot;: [&quot;NODE_ENV&quot;]
    }
  }
}</code></pre>
<h3 id="tailwind-css-스타일-충돌">Tailwind CSS 스타일 충돌</h3>
<p><strong>문제</strong>
각 앱마다 다른 Tailwind 설정으로 스타일 불일치</p>
<p><strong>해결</strong></p>
<pre><code class="language-typescript">// packages/tailwind-config/index.ts
export default {
  theme: {
    extend: {
      colors: {
        primary: {
          50: &quot;#f0f9ff&quot;,
          // ... 공통 색상 팔레트
        },
      },
      spacing: {
        // 공통 간격 시스템
      },
    },
  },
};

// 각 앱에서 확장만 가능
// apps/ai-agent-admin/tailwind.config.ts
import sharedConfig from &quot;@repo/tailwind-config&quot;;

export default {
  ...sharedConfig,
  content: [&quot;./src/**/*.{ts,tsx}&quot;],
  theme: {
    extend: {
      // 앱별 추가 설정만
    },
  },
};</code></pre>
<h3 id="빌드-순서-의존성">빌드 순서 의존성</h3>
<p><strong>문제</strong>
앱이 <code>@repo/ui</code>를 사용하는데 빌드 순서가 보장되지 않음</p>
<p><strong>해결</strong></p>
<pre><code class="language-json">// turbo.json
{
  &quot;tasks&quot;: {
    &quot;build&quot;: {
      &quot;dependsOn&quot;: [&quot;^build&quot;], // ^ = 의존성 먼저 빌드
      &quot;outputs&quot;: [&quot;.next/**&quot;, &quot;dist/**&quot;]
    }
  }
}</code></pre>
<p>Turborepo가 의존성 그래프를 분석하여 자동으로 순서 결정:</p>
<pre><code class="language-text">1. packages/ui 빌드
2. apps/* 병렬 빌드 (ui 빌드 완료 후)</code></pre>
<hr>
<h2 id="💬-마치며">💬 마치며</h2>
<p>Monorepo 마이그레이션은 쉽지 않았습니다. 처음에는 &quot;내가 왜 이걸 시작했지?&quot;라는 생각도 들었죠.</p>
<p>하지만 지금은 추천할만큼 긍정적으로 생각합니다.</p>
<p>특히 아래 상황이라면 더더욱 추천드려요.</p>
<blockquote>
<p>✅ <strong>여러 프로젝트를 혼자 관리하는 분</strong></p>
<ul>
<li>코드 중복으로 고통받고 계신가요?</li>
<li>같은 작업을 여러 번 반복하시나요?</li>
</ul>
<p>✅ <strong>빠른 프로토타이핑이 필요한 분</strong></p>
<ul>
<li>공통 컴포넌트 재사용으로 개발 속도 향상</li>
</ul>
<p>✅ <strong>일관성을 중요하게 생각하는 분</strong></p>
<ul>
<li>모든 프로젝트에서 동일한 코드 스타일 유지</li>
</ul>
</blockquote>
<p>여러분도 코드 복붙의 늪에서 허우적대고 계신가요?<br>같은 작업을 여러 번 반복하며 시간을 낭비하고 계신가요?</p>
<p>그렇다면 Monorepo를 시도해보세요.</p>
<p>과거의 나에게 감사하게 될 겁니다.</p>
<hr>
<h2 id="📚-참고-자료">📚 참고 자료</h2>
<ul>
<li><a href="https://turbo.build/repo/docs">Turborepo 공식 문서</a></li>
<li><a href="https://pnpm.io/workspaces">pnpm Workspace</a></li>
<li><a href="https://monorepo.tools/">Monorepo Best Practices</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[우당탕탕 Sentry 도입기 (With Vue3)]]></title>
            <link>https://velog.io/@hb_een/%EC%9A%B0%EB%8B%B9%ED%83%95%ED%83%95-Sentry-%EB%8F%84%EC%9E%85%EA%B8%B0</link>
            <guid>https://velog.io/@hb_een/%EC%9A%B0%EB%8B%B9%ED%83%95%ED%83%95-Sentry-%EB%8F%84%EC%9E%85%EA%B8%B0</guid>
            <pubDate>Sun, 03 Mar 2024 05:34:12 GMT</pubDate>
            <description><![CDATA[<h2 id="우리-서비스에-sentry를">우리 서비스에 Sentry를,,?</h2>
<p>입사한지 3개월 정식계약을 맺은 달 팀장님이 제게 Sentry도입을 맡기셨다.
여지껏 SI와 B2B 회사를 다닌 나는 운영 에러에 대한 고민을 해보지 못했다. (<del>이렇기에 B2C회사로 이직했다</del>)
그렇기에 Sentry에 대해서 무지했고 Sentry를 이용한 운영 에러 관리를 알 수 있는 좋은 기회라 생각이 들었다.
Sentry에 대해 공부하고 적용하면서 알게된 지식들을 남기고자 글을 쓰게 되었다!</p>
<h2 id="sentry란">Sentry란?</h2>
<p>Sentry는 <strong>실시간 로그 취합 및 분석 도구이자 모니터링 플랫폼</strong>이다.
로그에 대해 다양한 정보를 제공하고 이벤트별, 타임라인으로 얼마나 많은 이벤트가 발생하는지 알 수 있고 설정에 따라 알림을 받을 수 있다.
발생한 로그들을 시각화 도구로 쉽게 분석할 수 있도록 도와주며 다양한 플랫폼을 지원한다.
여기까지가 오피셜적인 Sentry 설명이라면 내가 사용하면서 정의내린 Sentry를 설명하겠다!
Sentry는 <strong>실시간 운영 에러에 대한 처리를 손쉽게 도와주는 플랫폼</strong>이다.
여기서 손쉽게라고 느껴진 포인트들은 이렇다</p>
<ol>
<li>실시간 발생되는 에러들이 대쉬보드에 비슷한 오류 통합하여 보여주니 에러들을 한 눈에 보기 편했다.
=&gt; 같은 이벤트면 하나의 항목에 EVENTS 갯수가 오르며, 발생된 USER수를 USERS에 보여준다.
<img src="https://velog.velcdn.com/images/hb_een/post/6b025fda-df80-4885-bc19-8a227f2caba5/image.png" alt=""></li>
<li>에러를 커스텀하여 가치를 높일 수 있고 커스텀한 내용은 에러 상세에 표시된다.
=&gt; 에러 커스텀은 밑에서 자세하게 다루겠다.</li>
<li>에러에 대한 알림을 여러 플랫폼과 연결할 수 있어 운영중에 에러 관리가 수월해진다.
=&gt; 무료버전에서는 플랫폼 제한이 있지만,,, 그래도 회사에서는 유료버전을 사용하기에 좋다고 느꼈다!</li>
<li>화면 Replay 기능을 제공하여 에러 발생 시 사용자가 어떤 행위를 했는지 볼 수 있어 좋았다.
=&gt; 에러 발생을 유발 시킨 행위를 알 수 있어 에러 추적이 매우 쉬웠다.</li>
</ol>
<p>이렇듯 Sentry를 사용하면 유지보수 특히, 운영 중 에러 관리가 매우 손쉬워질 수 있다.</p>
<h2 id="vue-프로젝트에-sentry-적용">Vue 프로젝트에 Sentry 적용</h2>
<ol>
<li>Sentry 설치<pre><code class="language-bash">yarn add @sentry/vue</code></pre>
</li>
<li>Sentry 설정
main.js에 Sentry 설정 기입 (자세한 설명은 주석 참고하세요!)<pre><code class="language-javascript">Sentry.init({
app,
// dsn: 이벤트를 전송하기 위한 식별 키 (Sentry 홈페이지에서 발급)
dsn: &quot;https://examplePublicKey@o0.ingest.sentry.io/0&quot;,
// environment: 애플리케이션 환경 (개발 테스트 환경 또는 운영 환경)
environment: &quot;prod&quot;,
// debug: true일 경우 Sentry SDK가 디버그 모드로 작동
// 브라우저 콘솔에 더 많은 디버깅 정보를 출력하게 됩니다. / 운영 환경에서는 false 추천
debug: false,
// integrations: 플랫폼 SDK별 통합 구성 설정
integrations: [
 // Breadcrumbs를 활성화 및 콘솔(console) 이벤트를 기록하도록 설정됨 (Breadcrumbs 관련은 하기 참조!)
 new Sentry.Integrations.Breadcrumbs({ console: true }),
 //Browser Tracing: 브라우저에서의 트레이싱을 활성화
 new Sentry.BrowserTracing({
         // 트레이싱은 애플리케이션 내에서의 성능 이슈를 식별하는 데 사용
         // tracingOrigin 및 tracePropagationTargets는 트레이싱의 대상을 설정
       tracingOrigins: [&#39;*&#39;],
       tracePropagationTargets: [&#39;*&#39;],
         // Vue.js 라우터의 인스턴스를 받아 Sentry에 라우팅 이벤트를 보고할 수 있도록 도와주는 도구
       // 라우터 이벤트의 추적은 사용자 경험을 모니터링하고, 발생한 오류와 연결된 페이지 정보를 제공하는 데 유용
       routingInstrumentation: Sentry.vueRouterInstrumentation(options.router),
 }),
  // Replay를 활성화 (Sentry에 화면 기록이 저장됨)
 new Sentry.Replay(),
],
// ignoreErrors: 특정 종류의 에러를 무시하도록 Sentry에게 지시 
// ResizeObserver loop limit exceeded 에러는 성능에 문제는 되지 않고 해결하기는 어려워 보편적으로 제외 시킴
ignoreErrors: [&#39;ResizeObserver loop limit exceeded&#39;],
</code></pre>
</li>
</ol>
<p>// tracesSampleRate: 성능 모니터링을 위한 트랜잭션 샘플링 비율을 설정
// 모든 트랜잭션을 모니터링하여 애플리케이션의 전반적인 성능을 파악하는 데 도움
// 만약 이 값을 낮추면 일부 트랜잭션만을 샘플링하여 전체 데이터 양을 줄임
// 그러면 에러 또는 성능 문제가 발생한 트랜잭션을 감지하는 데 시간이 걸릴 수 있음
tracesSampleRate: 1.0,
// Replay 관련 설정
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
});</p>
<pre><code>
## Sentry 소스맵 등록하기!
현재 우리 프로젝트는 보안을 위해 소스맵을 등록하지 않고 있다.
이렇기에 Sentry에서 에러를 추적하려 해도 어느 파일에서 발생했는지 알 수 없다.
에러 상세에서 표시되는건 암호화된 소스 파일로 보여지고 있다,,
프로젝트 빌드 과정에 Sentry 소스맵을 제출하는 부분이 포함 되어있다.
그렇기에 Sentry에는 소스맵을 제출하되 프로젝트 빌드 후에는 소스맵을 삭제하는 방식을 도입했다.
(vite vue 기준 입니다.)

1.  vue 프로젝트 설정에 소스맵 설정을 켜준다.
```javascript
// vite.config.ts
  build: {
    ...
    sourcemap: &#39;hidden&#39;,
  },</code></pre><ol start="2">
<li><p>sentryVitePlugin에 소스맵 관련 설정을 해준다.</p>
<pre><code class="language-javascript">// vite.config.ts
plugins: [
 ...
 sentryVitePlugin({
 ...
   sourcemaps: {
     assets: [&#39;./dist/assets/**&#39;],
     filesToDeleteAfterUpload: [&#39;./dist/assets/*.js.map&#39;],
   },
 }),
],</code></pre>
</li>
<li><p>배포 관련 YAML 파일에 Sentry 시크릿 키를 등록한다. (깃허브 액션 기준)
해당 키는 Sentry 홈페이지에서 발급 받을 수 있다.</p>
<pre><code class="language-bash">env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}</code></pre>
</li>
</ol>
<p><strong>다른 분들은 여기까지 하면 빌드 후 자동으로 소스맵이 삭제 된다고 하는데 삭제 되지 않는 문제점이 발생하였다.</strong>
<strong>시행 착오 끝에 깃허브 액션 YAML 파일에서 빌드 후 강제로 소스맵을 삭제하는 로직을 추가하였다😔</strong></p>
<ol start="4">
<li>배포 관련 YAML 파일에 소스맵 삭제 로직을 추가한다.<pre><code class="language-bash">- name: Remove .js.map files
     run: |
       find dist -name &quot;*.js.map&quot; -type f -delete</code></pre>
</li>
</ol>
<p><strong>여기까지 Sentry 초기 설정을 맞췄다!</strong>
<strong>이제 Sentry 상세 설명을 하겠다!</strong></p>
<hr/>

<h2 id="에러-가치-높이기">에러 가치 높이기!</h2>
<p>Sentry Issue 대쉬보드를 보면 여러가지 에러들이 보인다.
그 중 에러 하나를 선택해 에러 상세 페이지에 들어가보면 에러에 대한 여러가지 사항들을 볼 수 있다.
여러가지 사항들이 무엇을 의미하고 어떻게 커스텀할지에 대한 내용을 적어보겠다</p>
<h3 id="이벤트-로그">이벤트 로그</h3>
<ol>
<li><strong>Exception &amp; Message</strong>: 이벤트 로그 메시지 및 코드 라인 정보 ⇒ 소스맵 설정 필요<br/></li>
<li><strong>Device</strong>: 이벤트 발생 장비 정보 (name, family, model, memory 등)<br/></li>
<li><strong>Browser</strong>: 이벤트 발생 브라우저 정보 (name, version 등)<br/></li>
<li><strong>OS</strong>: 이벤트 발생 OS 정보 (name, version, build, kernelVersion 등)<br/></li>
<li><strong>Breadcrumbs</strong>: 이벤트 발생 과정</li>
</ol>
<h3 id="이벤트-로그-커스텀">이벤트 로그 커스텀</h3>
<p>기본 옵저버에서 제공되는 이벤트 로그 말고 코드 상에서 정보를 추가로 제공하여 커스텀 이벤트 로그를 남길 수 있다.
이렇게 하면 에러에 대한 필요한 정보를 손쉽게 얻어 효율을 높일 수 있다.</p>
<ol>
<li><p>Context 세팅
에러 처리 부분에서 콘텍스트 라벨과 넣고싶은 정보를 추가한다.</p>
<pre><code class="language-javascript">Sentry.setContext(&#39;Api Response Error&#39;, {
status,
path,
});</code></pre>
<p>에러가 발생되면 아래와 같이 이벤트 로그 Context에 추가로 표시된다.
<img src="https://velog.velcdn.com/images/hb_een/post/a6644fde-fe1d-47c0-9060-b705958f9365/image.png" alt=""></p>
</li>
<li><p>tag 세팅
에러 처리 부분에서 tag 네임과 value를 추가한다.</p>
<pre><code class="language-javascript">Sentry.withScope((scope) =&gt; {
 scope.setTag(&#39;type&#39;, &#39;api&#39;);
 scope.setTag(&#39;api-status&#39;, status);
 scope.setTag(&#39;api-path&#39;, path);
});</code></pre>
<p>에러가 발생되면 아래와 같이 이벤트 로그 tag에 추가로 표시된다.
<img src="https://velog.velcdn.com/images/hb_een/post/f33abfa5-76b1-4598-b35f-56979750ef9e/image.png" alt=""></p>
</li>
<li><p>에러 네임 설정
에러 처리 부분에서 이름을 추가한다.</p>
<pre><code class="language-javascript">Sentry.withScope((scope) =&gt; {
//new Error(status) 이 부분이 이름이 된다.
   Sentry.captureException(new Error(status));
});</code></pre>
<p>에러가 발생되면 Issue 리스트에 설정한 이름으로 표시된다.
<img src="https://velog.velcdn.com/images/hb_een/post/cee005e4-634d-4c41-ba48-f33d09d8d02e/image.png" alt=""></p>
</li>
<li><p>에러 레벨 설정
에러 처리 부분에서 레벨을 추가한다.</p>
<pre><code class="language-javascript">Sentry.withScope((scope) =&gt; {
   scope.setLevel(&#39;error&#39;);
});
</code></pre>
</li>
</ol>
<p>/*
에러 레벨
Fatal = &#39;fatal&#39;,  </p>
<p>Error = &#39;error&#39;,  </p>
<p>Warning = &#39;warning&#39;,  </p>
<p>Log = &#39;log&#39;,  </p>
<p>Info = &#39;info&#39;,  </p>
<p>Debug = &#39;debug&#39;,  </p>
<p>Critical = &#39;critical&#39;,
*/</p>
<pre><code>이렇게 에러 레벨을 설정하면 대쉬보드에서 에러 관리가 수월해 진다!
&lt;hr/&gt;

## 플랫폼 연동하기!
**다양한 플랫폼 연동 가능하나 유료 구독 회원만 지원하는 플랫폼이 대다수**

1. Setting → Integrations 으로 들어가서 원하는 플랫폼 설치
![](https://velog.velcdn.com/images/hb_een/post/ff700f80-70ca-4de5-bcc6-dede8da32167/image.png)

2. Alert 진입 후 Create Alert 클릭 후 Alert rule 설정
- 어떤 프로젝트의 어떤 환경을 대상으로 할 것인가?
- WHEN (ex. 새로운 이슈가 생성 됐을때, 해결한 이슈가 다시 발생됐을때 등)
- IF (ex. 이슈가 10분 이상 경과하면 등)
- THEN (ex. 연결된 플랫폼으로 알람을 보낸다 등)
⇒ 종합하면 새로운 이슈가 생성 됐을 때 이슈가 10분 이상 경과하면 연결된 플랫폼으로 알람을 보낸다.
(이런 식으로 Alert 설정 가능)
![](https://velog.velcdn.com/images/hb_een/post/6c9863a8-0eb6-4b45-a4fa-4d92ef89b344/image.png)

3. interval과 alert name 설정 하면 끝!</code></pre>]]></description>
        </item>
    </channel>
</rss>