<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>y-minion.log</title>
        <link>https://velog.io/</link>
        <description>앵맹!</description>
        <lastBuildDate>Tue, 24 Feb 2026 06:56:02 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>y-minion.log</title>
            <url>https://velog.velcdn.com/images/y-minion/profile/276e8c53-b5d1-4ab3-87fa-5900eb8d071a/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. y-minion.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/y-minion" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[macOS] 맥북 한영 전환(Caps Lock) 딜레이 없애고 네이티브로 해결하기 (focd, Karabiner 먹통 해결)]]></title>
            <link>https://velog.io/@y-minion/macOS-%EB%A7%A5%EB%B6%81-%ED%95%9C%EC%98%81-%EC%A0%84%ED%99%98Caps-Lock-%EB%94%9C%EB%A0%88%EC%9D%B4-%EC%97%86%EC%95%A0%EA%B3%A0-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-focd-Karabiner-%EB%A8%B9%ED%86%B5-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@y-minion/macOS-%EB%A7%A5%EB%B6%81-%ED%95%9C%EC%98%81-%EC%A0%84%ED%99%98Caps-Lock-%EB%94%9C%EB%A0%88%EC%9D%B4-%EC%97%86%EC%95%A0%EA%B3%A0-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-focd-Karabiner-%EB%A8%B9%ED%86%B5-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Tue, 24 Feb 2026 06:56:02 GMT</pubDate>
            <description><![CDATA[<h2 id="🚨-문제-상황-발생">🚨 문제 상황 발생</h2>
<p>기존에 맥북 기본 한영 전환(Caps Lock)의 타이핑 딜레이 문제 때문에 <code>focd</code>나 <code>Karabiner-Elements</code> 같은 외부 키 매핑 프로그램을 사용 중이었음.
내 맥북만 그런건지는 모르겠지만 맥북을 처음사고 한영전환을 시도해도 잘 작동하지 않아서 한동안 정신병 걸리는줄 알았다.</p>
<p>다른 커뮤니티에서는 <code>Karabiner-Elements를</code> 사용하면 해결된다고 했지만 내 맥북에서는 전<del>~</del>혀 해결되지 않았다.</p>
<p>그러던중 우연히 <code>focd</code> 라는 오픈소스를 발견하고 한동안 잘 사용했다.</p>
<p>하지만 오랜만에 맥북을 완전히 종료하고 다시 켰더니 갑자기 앱이 작동하지 않고, 한영 전환이 완전히 먹통이 되는 현상이 발생했다. 재설치나 권한 부여를 다시 해도 해결되지 않았다.</p>
<h2 id="🔍-원인-분석-왜-서드파티-앱이-죽었고-기본-전환에는-딜레이가-있는가">🔍 원인 분석: 왜 서드파티 앱이 죽었고, 기본 전환에는 딜레이가 있는가?</h2>
<p>이 문제를 해결하기 위해 시스템 로그를 뜯어본 결과, 하드웨어 고장이 아닌 <strong>macOS의 보안 정책(Hardened Runtime)</strong>과 <strong>이벤트 처리 방식</strong>의 문제였다.</p>
<h3 id="1-외부-앱focd-등이-막힌-이유-보안-정책-충돌">1. 외부 앱(focd 등)이 막힌 이유 (보안 정책 충돌)</h3>
<p>시스템 로그(<code>log show</code>)를 확인해보면 다음과 같은 에러가 발생했다.</p>
<blockquote>
<p><code>Prompting policy for hardened runtime; service: kTCCServiceAppleEvents requires entitlement com.apple.security.automation.apple-events but it is missing</code></p>
</blockquote>
<ul>
<li><strong>실제 발생한 일:</strong> <code>focd</code> 앱은 사용자의 키 입력을 모니터링(<code>keydown</code> 이벤트 리스닝)하다가, 특정 키가 눌리면 OS에 &quot;입력 소스를 변경해라&quot;라는 시스템 제어 명령를 전송한다.</li>
<li><strong>차단 원인:</strong> M4 칩셋이 탑재된 최신 macOS는 보안이 극도로 강화되었다고 한다. 앱 내부에 애플이 인가한 보안 권한이 명시되어 있지 않으면, 시스템이 이 명령을 악의적인 스크립트 실행으로 간주하고 커널단에서 강제로 차단해 버린다.</li>
</ul>
<h3 id="2-기본-caps-lock-한영-전환에-딜레이가-발생하는-이유">2. 기본 Caps Lock 한영 전환에 딜레이가 발생하는 이유</h3>
<aside>
👉 사실 이게 제일 짜증난다… 순정으로 사용하려고 해도 해결이 안되서 고객센터에 문의했더니 카라비너로 우회해서 사용하라고 하는…
</aside>

<p>애플이 의도한 macOS의 기본 Caps Lock 동작 로직 때문이다. Caps Lock 키는 1) 한영 전환, 2) 대문자 고정(원래 기능) 두 가지 역할을 수행해야 한다고 한다.</p>
<ul>
<li><strong>이벤트 처리 지연:</strong> 사용자가 키를 눌렀을 때, OS는 이것이 &#39;짧은 클릭&#39;인지 &#39;길게 누름&#39;인지 판단해야 함.</li>
<li><strong>결과:</strong> 빠르게 타이핑하는 도중 전환 키를 누르면, OS가 기다리는 동안 다음 키보드 입력이 들어오면서 이벤트 큐가 꼬이게 되고, 결국 한영 전환 명령이 무시되는 것 같다.</li>
</ul>
<h2 id="💡-해결-방법-hidutil을-이용한-로우-레벨-키-매핑">💡 해결 방법: hidutil을 이용한 로우 레벨 키 매핑</h2>
<p>외부 프로세스를 띄워 이벤트를 가로채는 방식 대신, macOS가 기본적으로 제공하는 <code>hidutil</code> 명령어를 사용하여 <strong>OS 커널의 HID드라이버 매핑 테이블 자체를 수정한다</strong>.</p>
<p>이 방식이 다른 오픈 소스와는 다르게 보안 권한 충돌이 없으며, 대기 시간이 없으므로 딜레이가 0이다.</p>
<h3 id="step-1-caps-lock을-f18-키로-매핑하기">Step 1. Caps Lock을 F18 키로 매핑하기</h3>
<p>터미널을 열고 다음 명령어를 실행한다. Caps Lock의 물리적 신호(<code>0x700000039</code>)를 사용하지 않는 키인 F18(<code>0x70000006d</code>)로 변경합니다.</p>
<pre><code class="language-bash">hidutil property --set &#39;{&quot;UserKeyMapping&quot;:[{&quot;HIDKeyboardModifierMappingSrc&quot;:0x700000039,&quot;HIDKeyboardModifierMappingDst&quot;:0x70000006d}]}&#39;</code></pre>
<blockquote>
<p><strong>레퍼런스:</strong> 이 설정값은 Apple 공식 개발자 문서인 <a href="https://developer.apple.com/library/archive/technotes/tn2450/_index.html">Technical Note TN2450: Remapping Keys in macOS</a>에 명시된 하드웨어 키보드 스캔 코드를 기반으로 작성되었습니다.</p>
</blockquote>
<h3 id="step-2-macos-설정에서-f18을-한영키로-등록필수-작업">Step 2. macOS 설정에서 F18을 한영키로 등록(필수 작업)</h3>
<ol>
<li><code>시스템 설정</code> &gt; <code>키보드</code> &gt; <code>키보드 단축키...</code> 로 이동한다.</li>
<li>왼쪽 메뉴에서 <code>입력 소스</code>를 선택.</li>
<li><code>이전 입력 소스 선택</code> 우측의 단축키 영역을 더블 클릭.</li>
<li><strong>Caps Lock 키</strong>를 누른다. (화면에 <code>F18</code>이라고 등록되면 성공이다.)<ol>
<li>만약 기존에 다른 오픈소스를 사용하던 사람들 같은 경우 아예 캡스락 기본 동작을 막아 놓았을 수도 있는데, 그런경우 <code>시스템 설정</code> &gt; <code>키보드</code> &gt; <code>키보드 단축키...</code> &gt; <code>보조키</code> 항목을 기본값으로 복원한뒤에 다시 시도하면 된다.</li>
</ol>
</li>
</ol>
<p>이제 타이핑을 해보면, 아무리 빠르게 쳐도 한영 전환 딜레이가 전혀 발생하지 않는 것을 확인할 수 있다.</p>
<h3 id="step-3-재부팅-시에도-설정-유지하기-자동화">Step 3. 재부팅 시에도 설정 유지하기 (자동화)</h3>
<p><code>hidutil</code>을 통한 매핑은 메모리에만 올라가므로 맥북을 재시작하면 초기화된다. 이를 방지하기 위해 부팅 시 명령어가 자동 실행되도록 <code>LaunchAgent</code> 데몬을 등록한다.</p>
<p>터미널에 아래 스크립트를 통째로 복사해서 붙여넣고 엔터치면 해결~!</p>
<pre><code class="language-bash">cat &lt;&lt;EOF &gt; ~/Library/LaunchAgents/com.user.capslock.plist
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;&lt;http://www.apple.com/DTDs/PropertyList-1.0.dtd&gt;&quot;&gt;
&lt;plist version=&quot;1.0&quot;&gt;
&lt;dict&gt;
    &lt;key&gt;Label&lt;/key&gt;
    &lt;string&gt;com.user.capslock&lt;/string&gt;
    &lt;key&gt;ProgramArguments&lt;/key&gt;
    &lt;array&gt;
        &lt;string&gt;/usr/bin/hidutil&lt;/string&gt;
        &lt;string&gt;property&lt;/string&gt;
        &lt;string&gt;--set&lt;/string&gt;
        &lt;string&gt;{&quot;UserKeyMapping&quot;:[{&quot;HIDKeyboardModifierMappingSrc&quot;:0x700000039,&quot;HIDKeyboardModifierMappingDst&quot;:0x70000006d}]}&lt;/string&gt;
    &lt;/array&gt;
    &lt;key&gt;RunAtLoad&lt;/key&gt;
    &lt;true/&gt;
&lt;/dict&gt;
&lt;/plist&gt;
EOF</code></pre>
<p>이후 아래 명령어로 시스템에 스크립트를 로드한다.</p>
<pre><code class="language-bash">launchctl load ~/Library/LaunchAgents/com.user.capslock.plist</code></pre>
<h2 id="🧹-마무리">🧹 마무리</h2>
<p>이제 더 이상 권한 충돌이나 딜레이 문제로 스트레스받을 필요가 없다. 기존에 사용하던 <code>focd</code> 등의 앱은 <code>시스템 설정</code> &gt; <code>개인정보 보호 및 보안</code>의 손쉬운 사용 탭에서 <code>-</code> 버튼으로 완전히 삭제해 주도록 하자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[설계 가이드라인]]></title>
            <link>https://velog.io/@y-minion/%EC%84%A4%EA%B3%84-%EA%B0%80%EC%9D%B4%EB%93%9C%EB%9D%BC%EC%9D%B8</link>
            <guid>https://velog.io/@y-minion/%EC%84%A4%EA%B3%84-%EA%B0%80%EC%9D%B4%EB%93%9C%EB%9D%BC%EC%9D%B8</guid>
            <pubDate>Fri, 31 Oct 2025 14:27:00 GMT</pubDate>
            <description><![CDATA[<html>
<head>

</head>
<body>
<p class="p2"><span class="s2"><hr></span></p>
<p class="p2"><span class="s2"><h2><b>🧭 설계 시작 순서 – 단계별로 쪼개기</b></h2></span></p>
<p class="p3"><br></p>
<p class="p4"><b>1️⃣ 가장 먼저 해야 할 건 “가장 중심이 되는 흐름”부터 설계하기!</b><b></b></p>
<p class="p1">Wise Wallet에서는 그게 바로 <span class="s1"><b>“입력 → 리스트 추가 → 수정/삭제”</b></span> 이 흐름이다.</p>
<p class="p2"><span class="s2"><hr></span></p>
<p class="p2"><span class="s2"><h3><b>✅ Step 1: 입력 바 (입력 폼)부터 시작</b></h3></span></p>
<p class="p3"><br></p>
<blockquote style="margin: 0.0px 0.0px 0.0px 15.0px; font: 19.0px '.AppleSystemUIFont'; color: #0e0e0e">이건 이 프로젝트에서 “가장 처음 사용자와 인터랙션하는 부분”이자</blockquote>
<blockquote style="margin: 0.0px 0.0px 0.0px 15.0px; font: 19.0px '.AppleSystemUIFont'; color: #0e0e0e">다른 모든 기능의 진입점이기 때문에 <span class="s1"><b>무조건 여기서 시작해야 한다.</b><b></b></span></blockquote>
<p class="p3"><br></p>
<p class="p2"><span class="s2"><h4><b>관련된 요구사항</b></h4></span></p>
<p class="p2"><span class="s2"><ul><li>
<p class="p1">날짜 선택</p>
</li><li>
<p class="p1">수입/지출 전환 버튼</p>
</li><li>
<p class="p1">금액 인풋 (3자리 쉼표 포맷)</p>
</li><li>
<p class="p1">카테고리 드롭다운 (수입/지출 구분)</p>
</li><li>
<p class="p1">결제수단 드롭다운 (추가/삭제 포함)</p>
</li><li>
<p class="p1">내용 인풋 (글자 수 제한 + 카운팅)</p>
</li><li>
<p class="p1">확인 버튼 (모든 유효성 통과 시 활성화)</p>
</li></ul></span></p>
<p class="p3"><br></p>
<p class="p2"><span class="s2"><h4><b>시작 시 설계 범위</b></h4></span></p>
<p class="p2"><span class="s2"><ul><li>
<p class="p1">core/<span class="s1">:</span></p>
<p class="p2"><span class="s2"><ul><li>
<p class="p1">AmountInput<span class="s1">, </span>DateInput<span class="s1">, </span>SwitchButton<span class="s1">, </span>Dropdown<span class="s1">, </span>DescriptionInput<span class="s1">, </span>Button</p>
</li></ul></span></p>
</li><li>
<p class="p1">units/TransactionForm/<span class="s1">:</span></p>
<p class="p2"><span class="s2"><ul><li>
<p class="p1">상위에서 모든 상태를 관리</p>
</li><li>
<p class="p1">유효성 검증</p>
</li><li>
<p class="p1">확인 버튼 클릭 시 리스트에 추가하는 로직 포함</p>
</li></ul></span></p>
</li></ul></span></p>
<p class="p2"><span class="s2"><hr></span></p>
<p class="p2"><span class="s2"><h3><b>✅ Step 2: 리스트 (TransactionList)</b></h3></span></p>
<p class="p3"><br></p>
<blockquote style="margin: 0.0px 0.0px 0.0px 15.0px; font: 19.0px '.AppleSystemUIFont'; color: #0e0e0e">사용자가 입력한 정보를 <span class="s1"><b>눈에 보이게 출력</b></span>하는 UI</blockquote>
<blockquote style="margin: 0.0px 0.0px 0.0px 15.0px; font: 19.0px '.AppleSystemUIFont'; color: #0e0e0e">삭제, 수정, 필터링, 정렬 등의 요구사항이 포함됨</blockquote>
<p class="p3"><br></p>
<p class="p2"><span class="s2"><h4><b>관련된 요구사항</b></h4></span></p>
<p class="p2"><span class="s2"><ul><li>
<p class="p1">날짜/요일별 그룹화</p>
</li><li>
<p class="p1">일자별 수입/지출 합계</p>
</li><li>
<p class="p1">체크박스 필터링 (수입/지출)</p>
</li><li>
<p class="p1">hover 시 삭제 버튼 노출</p>
</li><li>
<p class="p1">수정 시 내용 자동 채워짐</p>
</li><li>
<p class="p1">수정 완료 시 상태 해제</p>
</li></ul></span></p>
<p class="p3"><br></p>
<p class="p2"><span class="s2"><h4><b>설계 범위</b></h4></span></p>
<p class="p2"><span class="s2"><ul><li>
<p class="p1">core/TransactionItemDisplay<span class="s1">, </span>DateLabel<span class="s1">, </span>AmountText<span class="s1">, </span>CategoryTag</p>
</li><li>
<p class="p1">units/TransactionList<span class="s1">, </span>EditableTransactionItem<span class="s1">, </span>FilterToggleBar</p>
</li><li>
<p class="p1">hooks/useTransactionFilter.js</p>
</li></ul></span></p>
<p class="p2"><span class="s2"><hr></span></p>
<p class="p2"><span class="s2"><h3><b>✅ Step 3: 그 다음은 페이지(View) 단위 설계</b></h3></span></p>
<p class="p3"><br></p>
<blockquote style="margin: 0.0px 0.0px 0.0px 15.0px; font: 19.0px '.AppleSystemUIFont'; color: #0e0e0e">이제 입력과 리스트 흐름이 정리되면, 그걸 실제로 보여주는 페이지 단위 컴포넌트를 설계한다.</blockquote>
<p class="p3"><br></p>
<p class="p2"><span class="s2"><ul><li>
<p class="p1">views/MainView/</p>
<p class="p2"><span class="s1"><ul><li>
<p class="p1">상단: Header</p>
</li><li>
<p class="p1">중단: TransactionForm</p>
</li><li>
<p class="p1">하단: TransactionList</p>
</li></ul></span></p>
</li></ul></span></p>
<p class="p2"><span class="s2"><hr></span></p>
<p class="p2"><span class="s2"><h3><b>✅ 이후 확장</b></h3></span></p>
<p class="p2"><span class="s2"><ol start="1"><li>
<p class="p1"><b>달력 페이지 (views/CalendarView/)</b><b></b></p>
<p class="p2"><span class="s1"><ul><li>
<p class="p1">하루 수입/지출/합계를 박스에 표시</p>
</li><li>
<p class="p1">오늘 날짜 강조</p>
</li><li>
<p class="p1">하단에 월별 총합 표시</p>
</li></ul></span></p>
</li><li>
<p class="p1"><b>통계 페이지 (views/StatisticsView/)</b><b></b></p>
<p class="p2"><span class="s1"><ul><li>
<p class="p1">카테고리별 비율, 금액</p>
</li><li>
<p class="p1">클릭 시 소비추이 그래프와 리스트 확장</p>
</li></ul></span></p>
</li></ol></span></p>
<p class="p2"><span class="s2"><hr></span></p>
<p class="p2"><span class="s2"><h2><b>📌 추천 설계 순서 요약표</b></h2></span></p>



<table>
<thead>
<tr>
<th>단계</th>
<th>시작 포인트</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>1️⃣</td>
<td>TransactionForm</td>
<td>모든 데이터 흐름의 시작점</td>
</tr>
<tr>
<td>2️⃣</td>
<td>TransactionList</td>
<td>보여지는 데이터 관리, 수정/삭제 등</td>
</tr>
<tr>
<td>3️⃣</td>
<td>MainView</td>
<td>전체 구조화</td>
</tr>
<tr>
<td>4️⃣</td>
<td>CalendarView</td>
<td>날짜 기반 시각화</td>
</tr>
<tr>
<td>5️⃣</td>
<td>StatisticsView</td>
<td>통계 기능, 정렬/애니메이션 포함</td>
</tr>
</tbody></table>
</body>
</html>]]></description>
        </item>
        <item>
            <title><![CDATA[🌱 디렉토리 설계 기준 고민]]></title>
            <link>https://velog.io/@y-minion/%EB%94%94%EB%A0%89%ED%86%A0%EB%A6%AC-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%A4%80-%EA%B3%A0%EB%AF%BC</link>
            <guid>https://velog.io/@y-minion/%EB%94%94%EB%A0%89%ED%86%A0%EB%A6%AC-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%A4%80-%EA%B3%A0%EB%AF%BC</guid>
            <pubDate>Fri, 31 Oct 2025 14:26:00 GMT</pubDate>
            <description><![CDATA[<h2 id="🔍-시작-구조를-정해야-하는데-아무것도-모르겠을-때">🔍 시작: 구조를 정해야 하는데... 아무것도 모르겠을 때</h2>
<p>처음에는 너무 막막했다.</p>
<ul>
<li>어떤 기준으로 디렉토리를 나눠야 하는지도 몰랐고,</li>
<li>어떤 아키텍처를 써야 할지도 감이 없었다.</li>
<li>무작정 따라하고 싶지 않았고, <strong>내 기준으로 설계하고 싶었다.</strong></li>
</ul>
<p>이 과정은 단순한 기술 선택이 아니라<br><strong>&quot;나만의 사고 기준을 세우는 과정&quot;</strong>이었고, 시행착오를 거치며 조금씩 생각이 정리되었다.</p>
<hr>
<h2 id="🧠-기준을-찾기-위한-5가지-질문과-나의-답">🧠 기준을 찾기 위한 5가지 질문과 나의 답</h2>
<blockquote>
<p>디렉토리 구조에는 정답이 없지만, &quot;나만의 기준&quot;은 있어야 한다.</p>
</blockquote>
<h3 id="1-내가-관리하고-싶은-단위는-무엇인가">1. 내가 관리하고 싶은 단위는 무엇인가?</h3>
<p>✅ <strong>컴포넌트 단위</strong>로 관리하고 싶다.</p>
<ul>
<li>모든 컴포넌트는 UI만, 또는 로직만, 또는 둘 다 가질 수 있다.</li>
<li><strong>하나의 컴포넌트 = 하나의 폴더</strong>로 두고,
<code>index.jsx</code>, <code>UI.jsx</code>, <code>logic.js</code>, <code>style.js</code>처럼 내부 분리.</li>
</ul>
<h3 id="2-상태는-어디에서-관리하고-싶은가">2. 상태는 어디에서 관리하고 싶은가?</h3>
<p>✅ 최대한 <strong>순수한 컴포넌트</strong>를 유지하고, 상태는 상위에서 <code>prop</code>으로만 전달.</p>
<ul>
<li>어떤 컴포넌트든 독립적으로 동작해야 한다.</li>
<li>마치 <strong>레고 조각처럼</strong>, 연결만 해주면 잘 동작하는 구조를 지향한다.</li>
</ul>
<h3 id="3-얼마나-재사용할-것-같은가">3. 얼마나 재사용할 것 같은가?</h3>
<p>✅ 지금은 자주 재사용되지 않을 수 있지만,<br><strong>순수하게 설계하면 나중에 언제든 조립할 수 있다.</strong></p>
<ul>
<li>그래서 <strong>무조건 처음엔 core 폴더에 넣고 시작</strong>한다.</li>
</ul>
<h3 id="4-다른-사람이-봤을-때-구조를-어떻게-이해하길-원하는가">4. 다른 사람이 봤을 때 구조를 어떻게 이해하길 원하는가?</h3>
<p>✅ 전체 구조는 <strong>조립 순서대로 보여야 한다.</strong></p>
<ul>
<li><code>core</code>: 진짜 레고 부품</li>
<li><code>units</code>: 조립된 기능 단위</li>
<li><code>views</code>: 사용자에게 보이는 완성 화면</li>
<li>구조 흐름은 <code>views → units → core</code>로 내려간다.</li>
</ul>
<h3 id="5-내가-나중에-봐도-이해할-수-있는가">5. 내가 나중에 봐도 이해할 수 있는가?</h3>
<p>✅ <strong>YES.</strong>  </p>
<ul>
<li>명확한 계층, 명확한 책임 분리  </li>
<li>상위 구조만 보면 흐름이 보이고, 하위 구조는 그 기능의 세부로만 집중되어 있음</li>
</ul>
<hr>
<h2 id="⚙️-아키텍처-철학-순수한-레고를-조립하는-방식">⚙️ 아키텍처 철학: 순수한 레고를 조립하는 방식</h2>
<h3 id="핵심-원칙">핵심 원칙</h3>
<pre><code>| 항목 | 철학 |
|------|------|
| 🎯 기본 단위 | **모든 컴포넌트는 core에서 시작** |
| 🧼 상태 분리 | 상태는 core에서 다루지 않음 |
| 🧩 조립 원칙 | `units`에서 `core`를 조합해서 기능 부여 |
| 🔌 연결 방법 | 상위에서 prop으로 로직 전달 (composition) |
| 📐 확장 구조 | 필요할 경우 core → units → views 단계별 조립 |</code></pre><hr>
<h2 id="📁-구조-스케치">📁 구조 스케치</h2>
<pre><code>src/
├── core/         # 순수한 레고 부품들 (UI만 담당)
├── units/        # 레고 조합한 중간 기능들
├── views/        # 페이지 단위 컴포넌트
├── hooks/        # 로직용 커스텀 훅
├── utils/        # format, 계산 등 유틸
├── constants/    # 카테고리, 색상 상수 등
└── main.jsx</code></pre><hr>
<h2 id="📌-피그마-기준으로-판단한-core-컴포넌트-후보">📌 피그마 기준으로 판단한 core 컴포넌트 후보</h2>
<ul>
<li><code>DateInput</code>, <code>AmountInput</code>, <code>CategorySelect</code>, <code>PaymentSelect</code></li>
<li><code>SwitchButton</code>, <code>Button</code>, <code>Modal</code>, <code>Dropdown</code></li>
<li><code>TransactionItemDisplay</code>, <code>AmountText</code>, <code>CategoryTag</code></li>
<li><code>CalendarCell</code>, <code>StatsRow</code>, <code>MiniBarGraph</code></li>
</ul>
<p>→ 이들은 로직 없이 UI만 담당하므로 core에 위치</p>
<hr>
<h2 id="✨-이-기록을-왜-남기는가">✨ 이 기록을 왜 남기는가?</h2>
<p>이 문서는 내가 겪은 시행착오와 해결 과정의 결과물이다.</p>
<ul>
<li>처음엔 아무것도 몰랐지만,</li>
<li>질문하고, 정리하고, 조합하며</li>
<li><strong>나만의 기준과 방향성을 가진 개발자</strong>로 성장하고 있다.</li>
</ul>
<p>언제든 이 기준을 다시 확인하며,<br>더 나은 방향으로 <strong>내 설계를 발전시켜 나갈 수 있도록</strong> 이 문서를 남긴다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[디렉토리 설계 & 컴포넌트 계층 설계]]></title>
            <link>https://velog.io/@y-minion/%EB%94%94%EB%A0%89%ED%86%A0%EB%A6%AC-%EC%84%A4%EA%B3%84-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EA%B3%84%EC%B8%B5-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@y-minion/%EB%94%94%EB%A0%89%ED%86%A0%EB%A6%AC-%EC%84%A4%EA%B3%84-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EA%B3%84%EC%B8%B5-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Fri, 31 Oct 2025 14:24:31 GMT</pubDate>
            <description><![CDATA[<html>
<head>

</head>
<body>

<h2 id="✅-디렉토리-설계-기준">✅ 디렉토리 설계 기준</h2>
<table>
<thead>
<tr>
<th>폴더명</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>core/</td>
<td>UI만 담당하는 진짜 순수 컴포넌트</td>
</tr>
<tr>
<td>units/</td>
<td>core를 조립해서 로직과 상호작용을 가진 기능 컴포넌트</td>
</tr>
<tr>
<td>views/</td>
<td>화면 단위의 완성 페이지 컴포넌트</td>
</tr>
<tr>
<td>hooks/</td>
<td>재사용 가능한 커스텀 로직 함수들</td>
</tr>
<tr>
<td>utils/</td>
<td>format, 계산 등 비상태 유틸 함수</td>
</tr>
<tr>
<td>constants/</td>
<td>enum, 카테고리 목록 등 정적인 값</td>
</tr>
<tr>
<td>services/</td>
<td>fetch, API 통신 관련 로직</td>
</tr>
<tr>
<td>contexts/</td>
<td>전역 상태를 관리하는 context API</td>
</tr>
</tbody></table>
<h2 id="📁-최종-디렉토리-구조-기능요구사항-기반">📁 최종 디렉토리 구조 (기능/요구사항 기반)</h2>
<pre><code>src/
├── core/
│   ├── inputs/
│   │   ├── AmountInput/
│   │   ├── DateInput/
│   │   ├── DescriptionInput/
│   │   └── TextField/
│   ├── buttons/
│   │   ├── Button/
│   │   └── SwitchButton/              # 수입/지출 토글
│   ├── select/
│   │   ├── CategorySelect/
│   │   └── PaymentSelect/
│   ├── list/
│   │   ├── TransactionItemDisplay/
│   │   ├── AmountText/
│   │   ├── CategoryTag/
│   │   └── DateLabel/
│   ├── stats/
│   │   ├── StatsRow/
│   │   └── MiniBarGraph/
│   ├── calendar/
│   │   └── CalendarCell/
│   └── common/
│       ├── Modal/
│       ├── Dropdown/
│       ├── Label/
│       ├── Icon/
│       └── CheckBox/
│
├── units/
│   ├── TransactionForm/
│   ├── TransactionList/
│   ├── FilterToggleBar/              # 수입/지출 필터 토글
│   ├── EditableTransactionItem/      # 수정 가능한 리스트 아이템
│   ├── CategoryStatisticsList/
│   ├── CalendarGrid/
│   └── PaymentManagerModal/          # 결제수단 추가/삭제 모달
│
├── views/
│   ├── MainView/                     # 내역 입력 + 리스트
│   ├── CalendarView/
│   ├── StatisticsView/
│   └── NotFoundView/
│
├── hooks/
│   ├── useTransactionForm.js
│   ├── useModal.js
│   └── useCalendarData.js
│
├── services/
│   └── transactionAPI.js
│
├── constants/
│   ├── categories.js
│   ├── payments.js
│   └── colors.js
│
├── utils/
│   ├── formatAmount.js
│   ├── validateForm.js
│   └── dateUtils.js
│
├── contexts/
│   └── TransactionContext.js
│
├── App.jsx
└── main.jsx</code></pre><h3 id="📌-구성-특징-요약">📌 구성 특징 요약</h3>
<table>
<thead>
<tr>
<th>특징</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>core/</td>
<td>모든 UI 컴포넌트를 로직 없이 순수하게 분리, core부터 구현하기(로직이 들어갈 공간은 남겨둔다_prop으로 받음)</td>
</tr>
<tr>
<td>units/</td>
<td>각 코어기능들을 import한뒤 로직을 붙여서 조립한다.</td>
</tr>
<tr>
<td>views/</td>
<td>요구사항에 따른 바탕화면 구성</td>
</tr>
<tr>
<td>hooks/, services/</td>
<td>비즈니스 로직 분리</td>
</tr>
<tr>
<td>constants/, utils/</td>
<td>전역 조릭 사용 정리 영역</td>
</tr>
</tbody></table>
<h2 id="📁-컴포넌트-계층-구조-설계">📁 컴포넌트 계층 구조 설계</h2>
<h3 id="🧱-전체-컴포넌트-계층-시각화">🧱 전체 컴포넌트 계층 시각화</h3>
<pre><code>App ( =MainPage)
├── Header
│   ├── LogoButton
│   ├── MonthNavigator
│   │   ├── SwipePrevPageDate
│   │   └── SwipeNextPageDate
│   └── TabSelector
└── TabView
    ├── FinancialRecords
    │   ├── FormBox
    │   │   ├── RegDateBox
    │   │   ├── AmountBox
    │   │   ├── ContentsBox
    │   │   ├── MethodBox
    │   │   ├── ClassificationBox
    │   │   └── RegisterButtonBox
    │   └── FinancialRecordsBoard
    │       ├── BoardHeaderWrapper
    │       └── RecordsListWrapper
    ├── FinancialCalandar
    │   ├── CalendarGrid
    │   ├── DayBox[]
    │   └── MonthlySummaryBar
    └── FinancialStatistic
        ├── MonthlySummary
        ├── CategoryList
        ├── SpendingTrendGraph
        └── DetailList</code></pre></body>
</html>]]></description>
        </item>
        <item>
            <title><![CDATA[🧱 나만의 프론트엔드 디렉토리 아키텍처 철학 & 설계 규칙(a.k.a 레고 아키텍처)]]></title>
            <link>https://velog.io/@y-minion/%EB%82%98%EB%A7%8C%EC%9D%98-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EB%94%94%EB%A0%89%ED%86%A0%EB%A6%AC-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%B2%A0%ED%95%99-%EC%84%A4%EA%B3%84-%EA%B7%9C%EC%B9%99</link>
            <guid>https://velog.io/@y-minion/%EB%82%98%EB%A7%8C%EC%9D%98-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EB%94%94%EB%A0%89%ED%86%A0%EB%A6%AC-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%B2%A0%ED%95%99-%EC%84%A4%EA%B3%84-%EA%B7%9C%EC%B9%99</guid>
            <pubDate>Fri, 31 Oct 2025 14:23:17 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-핵심-철학-요약">📌 핵심 철학 요약</h2>
<ul>
<li><strong>컴포넌트 중심(Component-First)</strong>으로 설계한다.</li>
<li>모든 컴포넌트는 <strong>순수함(Pure)을 기본 전제</strong>로 한다.</li>
<li><strong>로직과 UI는 분리</strong>하되, 하나의 컴포넌트를 중심으로 관리한다.</li>
<li><strong>prop을 통한 조립형 설계</strong>로 컴포넌트 간 의존성을 느슨하게 유지한다.</li>
<li>전체 UI는 마치 <strong>레고처럼 조합 가능한 구조</strong>로 만든다.</li>
<li>기능 흐름이 필요한 경우는 <strong>units 레이어에서 로직을 조립</strong>한다.</li>
<li><strong>index.jsx에서 “너무 많은 역할”을 하게 하지 마라</strong></li>
<li><strong>로직은 다른 파일로 분리한다.</strong></li>
<li><strong>컨테이너 + 훅 구조로 설계한다.</strong></li>
</ul>
<hr>
<h2 id="🔍-디렉토리-구조-기본-개념">🔍 디렉토리 구조 기본 개념</h2>
<pre><code>src/
├── base-ui/         # 순수 컴포넌트 (UI 중심, 로직 없음)
├── units/        # 조합형 컴포넌트 (로직 + 상태관리 포함)
├── views/        # 페이지 단위 컴포넌트 (조립 결과물)
├── hooks/        # 비즈니스 로직 hook
├── utils/        # 유틸 함수
├── constants/    # 상수/카테고리 등
</code></pre><hr>
<h2 id="🧠-자문-질문-기반-규칙-정의">🧠 자문 질문 기반 규칙 정의</h2>
<h3 id="1-내가-관리하고-싶은-단위는-무엇인가">1. “내가 관리하고 싶은 단위는 무엇인가?”</h3>
<blockquote>
<p>✅ <strong>컴포넌트 단위</strong>를 중심으로 생각한다.</p>
</blockquote>
<ul>
<li>UI/로직을 분리하고 싶을 경우, <strong>컴포넌트 단위로 폴더로 분리</strong></li>
<li><code>index.jsx</code>를 엔트리로 두고 내부에 <code>UI</code>, <code>logic</code>, <code>style</code> 파일을 분리</li>
</ul>
<hr>
<h3 id="2-어디에서-상태를-관리하고-싶은가">2. “어디에서 상태를 관리하고 싶은가?”</h3>
<blockquote>
<p>✅ 최대한 <strong>컴포넌트는 순수하게</strong> 두고<br>상태는 상위에서 prop으로 연결한다.</p>
</blockquote>
<ul>
<li>컴포넌트 내부에서도 상태를 가질 수 있으나, 외부 의존성은 없게</li>
<li>상호작용은 <strong>prop으로 동작을 연결할 수 있도록 설계</strong></li>
</ul>
<hr>
<h3 id="3-내가-얼마나-재사용할-것-같은가">3. “내가 얼마나 재사용할 것 같은가?”</h3>
<blockquote>
<p>✅ 우선 <strong>재사용 여부와 관계없이 순수하게 만든다.</strong><br>→ 이후에 prop만 연결해서 다른 맥락에서도 사용 가능</p>
</blockquote>
<ul>
<li>core에 있는 컴포넌트들은 재사용이 쉬운 구조를 기본으로 설계</li>
<li>처음엔 재사용하지 않더라도, <strong>순수함</strong>을 유지하면 언제든 확장 가능</li>
</ul>
<hr>
<h3 id="4-다른-사람이-내-프로젝트를-본다면-어떻게-이해했으면-좋겠는가">4. “다른 사람이 내 프로젝트를 본다면 어떻게 이해했으면 좋겠는가?”</h3>
<blockquote>
<p>✅ <strong>레고 조립 구조처럼 계층적으로 이해되도록 구성</strong></p>
</blockquote>
<ul>
<li><code>core</code>: 부품, <code>units</code>: 조합, <code>views</code>: 완성된 페이지</li>
<li>흐름은 <strong>views → units → core</strong>로 구성되며<br>누구든 전체 구조를 빠르게 파악 가능하게 한다</li>
</ul>
<hr>
<h3 id="5-이-구조로-내가-3개월-뒤에도-다시-봐도-편할까">5. “이 구조로 내가 3개월 뒤에도 다시 봐도 편할까?”</h3>
<blockquote>
<p>✅ 충분히 편하다.<br><strong>독립적인 컴포넌트 + 명확한 조합 위치</strong> 덕분에<br>구조를 따라가면 자연스럽게 흐름을 이해할 수 있다.</p>
</blockquote>
<hr>
<h2 id="⚙️-컴포넌트-설계-규칙">⚙️ 컴포넌트 설계 규칙</h2>
<h3 id="core-component">Core Component</h3>
<ul>
<li>UI만 담당한다.</li>
<li>로직, 상태 없음 (<code>useState</code>, <code>useEffect</code> 사용 금지)</li>
<li>필요 시 <code>onClick</code>, <code>onChange</code> 등 <strong>prop으로 기능 연결점만 열어둠</strong></li>
<li>기본적으로 <code>props</code>만 받아서 렌더링만 수행</li>
</ul>
<h3 id="unit-component">Unit Component</h3>
<ul>
<li>Core 컴포넌트를 <strong>import해서 조립</strong></li>
<li>필요한 로직/상태를 <code>useState</code>, <code>useReducer</code>, <code>custom hooks</code> 등으로 처리</li>
<li>prop을 통해 core 컴포넌트에 동작 주입</li>
</ul>
<h3 id="view-component">View Component</h3>
<ul>
<li><strong>여러 Unit 컴포넌트를 조립</strong>해서 페이지 화면을 구성</li>
<li>route, layout, 데이터 fetch 등의 최상위 흐름 담당</li>
</ul>
<hr>
<h2 id="✅-핵심-원칙-정리">✅ 핵심 원칙 정리</h2>
<pre><code>| 원칙 | 설명 |
|------|------|
| 💡 순수성 | 모든 컴포넌트는 가능한 한 독립적으로 작동 |
| 🧱 조립 가능성 | 작은 컴포넌트를 상위에서 조립해서 기능화 |
| 🔌 prop 설계 | 기능은 prop으로 전달해서 유연성 확보 |
| 🧼 유지보수성 | core는 안정적, units는 변화 중심 |
| 📚 문서화 | `index.jsx`를 통해 진입점 명확히 함 |
</code></pre><hr>
<h2 id="📁-실전-예시">📁 실전 예시</h2>
<pre><code>core/
└── Button/
├── Button.jsx       # UI 구현만
├── style.js
└── index.jsx        # export Button

units/
└── SubmitButton/
├── SubmitButton.jsx # 로직 포함 (상태, 핸들러)
└── index.jsx

views/
└── FormPage/
├── FormPage.jsx     # submit 버튼 포함된 form 구성
└── index.jsx</code></pre><hr>
<blockquote>
<p>💬 이 문서는 내 프로젝트의 아키텍처 설계 철학을 정의하며<br>향후 확장/변경 시 언제든지 참조 가능한 <strong>디렉토리 구조 기준서</strong>이다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[FE가 낋여주는 배포 일지(5)_EC2 + Nginx+ S3+codedeploy로 블루/그린 무중단 배포를 해보자]]></title>
            <link>https://velog.io/@y-minion/%EB%B8%94%EB%A3%A8%EA%B7%B8%EB%A6%B0-%EB%AC%B4%EC%A4%91%EB%8B%A8-%EB%B0%B0%ED%8F%AC%EB%A5%BC-%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@y-minion/%EB%B8%94%EB%A3%A8%EA%B7%B8%EB%A6%B0-%EB%AC%B4%EC%A4%91%EB%8B%A8-%EB%B0%B0%ED%8F%AC%EB%A5%BC-%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Mon, 20 Oct 2025 18:10:48 GMT</pubDate>
            <description><![CDATA[<h2 id="필요성">필요성</h2>
<p>기존에 EC2 + Nginx + codedeploy 로 배포를 했다. 이 방법도 초반에 깃허브 액션으로 ssh연결을 통해 배포하던 방법보다는 굉장히 빠르고 안정적으로 배포를 할 수 있었다. 하지만 하직 큰 문제가 존재했다.
새로운 코드를 서버에 받아와서 새로운 서비스를 실행하는 순간에 기존의 서비스가 꺼지고, 새로운 서비스가 켜진다. 이 순간 사용자의 트래픽은 끊긴다. 그래서 이를 해결할 수 있는 블루/그린 전략을 해볼 것이다.</p>
<h2 id="블루그린-무중단-배포-전략">블루/그린 무중단 배포 전략</h2>
<p><img src="https://velog.velcdn.com/images/y-minion/post/9af507f1-2555-4e3b-b71b-04091ce9dbf0/image.svg" alt=""></p>
<h2 id="1-동적-전환을-위한-nginx-설정">1. 동적 전환을 위한 Nginx 설정</h2>
<p>Ngninx 가 바라보는 애플리케이션 포트를 배포 시점에 동적으로 변경할 수 있도록 proxy_pass 설정을 외부의 파일로 분리한다. 이로써 배포 스크립트가 Nginx설정을 건드리지 않고 단 하나의 파일만 수정함으로써 안정하게 포트를 조작할 수 있다. </p>
<h3 id="1-메인-nginx-설정">1. 메인 Nginx 설정</h3>
<p>먼저 /etc/nginx/sites-available/default 에서 프록시 설정은 바꿔줘야 한다.
기존에는 아래와 같이 nginx가 외부의 요청을 내부의 3000번 포트로 연결하도록 설정을 했다.
<img src="https://velog.velcdn.com/images/y-minion/post/6e6f8346-3253-45e5-aa31-f672b4f15706/image.png" alt=""></p>
<h3 id="2분리된-프록시-설정-파일">2.분리된 프록시 설정 파일</h3>
<p>하지만 이제는 외부의 proxy.conf 파일에서 proxy_pass를 결정한다.</p>
<pre><code class="language-bash"># /etc/nginx/sites-available/default
server{
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name supabase.y-minion.link;

    # ...
    location / {
        include /etc/nginx/conf.d/proxy_pass.inc
    }
}</code></pre>
<p>-&gt; 보통 3000 번 포트에 애플리케이션을 실행하지만 무중단 배포를 위해 임의로 3001,3002포트를 사용한다.(3000번 포트로 해도 상관없다. 하지만 블루그린 배포에서는 포트 2개를 사용한다는 것만 알아두자.)</p>
<h3 id="3-codedeploy가-수정할-포트">3. codedeploy가 수정할 포트</h3>
<p>프록시 설정파일 내부가 아닌 외부의 별도 파일에서 외부의 트래픽을 연결할 proxy_pass를 정해준다. 이렇게 하면 배포 스크립트는 이 파일의 내용만 수정해 안전하게 조작할 수 있다.
초기에는 블루 포트(3001)를 가리키도록 설정한다.</p>
<pre><code class="language-shell">#/etc/nginx/conf.d/proxy_pass.inc
proxy_pass http://127.0.0.1:3001;</code></pre>
<h2 id="2-codedeploy-배포-명세서-appspecyml">2. CodeDeploy 배포 명세서 (<code>appspec.yml</code>)</h2>
<p>CodeDeploy에게 배포 프로세스에서 어떤 스크립트를 실행할지 지시하는 appsepec.yml을 작성한다. 이때 프로젝트의 루트 디렉토리에 작성해야한다.
<strong>서비스 중단을 유발하는 <code>ApplicationStop</code> 훅은 의도적으로 사용하지 않는다.</strong>
    - 기존(블루)의 어플리케이션을 중단하게 되면 무중단 배포의 목적이 깨져버린다.
    - 하나의 서버에 블루,그린 어플리케이션을 띄워놓고 그린 어플리케이션의 헬스 체크가 완료되고 그린으로 트래픽 전환이 완료되면 기존(블루)의 어플리케이션을 종료한다.</p>
<pre><code class="language-yaml">version: 0.0
os: linux
files:
  - source: /
    destination: /home/ubuntu/application
    overwrite: yes

permissions:
  - object: /home/ubuntu
    pattern: &quot;**&quot;
    owner: ubuntu
    group: ubuntu

hooks:
  AfterInstall:
    # 1. 새 버전의 코드 의존성 설치
    - location: scripts/after-install.sh
      timeout: 300
      runas: ubuntu
  ApplicationStart:
    # 2. 새 버전(그린)을 새로운 포트에 실행
    - location: scripts/start-server.sh
      timeout: 300
      runas: ubuntu
  ValidateService:
    # 3. 그린 서버 검증 -&gt; 트래픽 전환 -&gt; 구버전(블루) 정리
    - location: scripts/validate_and_switch_and_cleanup.sh
      timeout: 90
      runas: ubuntu</code></pre>
<h2 id="3-핵심-배포-스크립트-작성">3. 핵심 배포 스크립트 작성</h2>
<p><code>appspec.yml</code> 이 참조하는 스크립트들은 프로젝트내 <code>scripts/</code>폴더에 작성한다.</p>
<h3 id="1-scriptsafter_installsh">1. <code>scripts/after_install.sh</code></h3>
<p>S3로부터 받아온 파일을 저장한 디렉토리로 이동한뒤 프로젝트의 의존성을 설치한다.
이때 환경변수를 설정해주는 부분을 주목하자.
기본적으로 우리가 ssh 로 직접 서버에 접근해서 node를 사용하는 경우(대화형 쉘)과는 다르게 배포 스크립트는 비대화형 쉘에서 실행되면서 node의 경로는 PATH에 추가되지 않는다. 그래서 환경변수에 추가하지 않으면 pm2나 npm같은 명령어는 못찾는다. 그래서 의도적으로 환경변수에 node의 경로를 추가해서 관련 명령어들을 사용할 수 있도록 한다.</p>
<pre><code class="language-shell">#!/bin/bash
NVM_BIN_PATH=&quot;/home/ubuntu/.nvm/versions/node/v22.20.0/bin&quot;

export PATH=&quot;$NVM_BIN_PATH:$PATH&quot;

cd /home/ubuntu/application

npm install</code></pre>
<h3 id="2-scriptsstart_serversh">2. <code>scripts/start_server.sh</code></h3>
<p>1단계에서 만든 <code>/etc/nginx/conf.d/proxy_pass.inc</code>에서 현재의 포트 번호를 알아낸다. -&gt; <code>CURRENT_BLUE_PORT</code>
그리고 블루환경에서 사용하지 않는 포트를 그린 환경의 포트로 지정해야한다.
그린환경의 포트를 정하면 그린환경에서 새로운 어플리케이션을 그린환경 전용 포트에 실행한다.
이때! 그린환경의 포트를 다른 스크립트에서 참조할 수 있도록 별도의 파일(<code>/tmp/green_port.txt</code>)에 저장한다.</p>
<pre><code class="language-shell">#!/bin/bash

cd /home/ubuntu/application

if [ ! -f /etc/nginx/conf.d/proxy_pass.inc]; then
    CURRENT_BLUE_PORT=3001
else
    CURRENT_BLUE_PORT=$(grep -oP &#39;(?&lt;=:)\d+&#39; /etc/nginx/conf.d/proxy_pass.inc)
fi

if [ &quot;$CURRENT_BLUE_PORT&quot; -eq 3001 ]; then
    GREEN_PORT=3002
else
    GREEN_PORT=3001
fi

echo &quot;&gt;&gt;&gt; Blue Port: $CURRENT_BLUE_PORT&quot;
echo &quot;&gt;&gt;&gt; Green Port (New Server): $GREEN_PORT&quot;

PORT=$GREEN_PORT pm2 start &quot;npm run start&quot; --name &quot;app-$GREEN_PORT&quot;


pm2 save

echo $GREEN_PORT &gt; /tmp/green_port.txt</code></pre>
<blockquote>
<p>⭐️중요!) npm run start 를 하기전에 먼저 해당 어플리케이션을 실행 시킬 PORT를 지정해줘야 한다.</p>
</blockquote>
<h3 id="3-scriptsvalidate_and_switch_and_cleanupsh">3. scripts/validate_and_switch_and_cleanup.sh</h3>
<p>무중단 배포의 핵심 로직이 담긴 중요한 스크립트다.
<code>/tmp/green_port.txt</code>에서 현재의 실행중인 그린환경의 포트 번호를 확인한다. 그리고 그린 환경의 서버가 잘 작동하는지 헬스체크를 한뒤 통과가 되면 <code>/etc/nginx/conf.d/proxy_pass.inc</code>의 포트 번호를 변경한다.
그리고 블루 환경의 어플리케이션은 종료한다.</p>
<pre><code class="language-shell">#!/bin/bash

NVM_BIN_PATH=&quot;/home/ubuntu/.nvm/versions/node/v22.20.0/bin&quot;

export PATH=&quot;$NVM_BIN_PATH:$PATH&quot;

if [ ! -f /tmp/green_port.txt ]; then
   echo  &quot;&gt;&gt;&gt; [Error]Could not find port file. The start_server step may have failed.&quot;
   exit 1
fi

GREEN_PORT=$(cat /tmp/green_port.txt)

echo &quot;&gt;&gt;&gt; [Step 1] Health check for New Green Server on port $GREEN_PORT&quot;

for i in {1..10}; do
    RESPONSE_CODE=$(curl -s -o /dev/null -w &quot;%{http_code}&quot; http://127.0.0.1:&quot;$GREEN_PORT&quot;/health)
    echo &quot;&gt;&gt;&gt; [debug] current code is $RESPONSE_CODE&quot;

    if [ &quot;$RESPONSE_CODE&quot; -eq 200 ]; then
        echo &quot;&gt;&gt;&gt; [Success] Health check successful.&quot;

        IDLE_PORT=$(grep -oP &#39;(?&lt;=:)\d+&#39; /etc/nginx/conf.d/proxy_pass.inc)

        echo &quot;&gt;&gt;&gt; [Step 2] Switching Nginx to Green Port: $GREEN_PORT&quot;
        echo &quot;proxy_pass http://127.0.0.1:$GREEN_PORT;&quot; | sudo tee /etc/nginx/conf.d/proxy_pass.inc

        sudo systemctl reload nginx
        echo &quot;&gt;&gt;&gt; [Success] Nginx reloaded. Traffic is now served by Green server.&quot;

        echo &quot;&gt;&gt;&gt; [Step 3] Stopping Old Blue server on port $IDLE_PORT&quot;
        pm2 stop &quot;app-$IDLE_PORT&quot;
        pm2 delete &quot;app-$IDLE_PORT&quot;
        pm2 save

        exit 0

    fi

    echo &quot;&gt;&gt;&gt; Health check failed. Retrying ... ($i/10)&quot;
    sleep 1
done

echo &quot;&gt;&gt;&gt; [Error] Health check failed after all retries. Rolling back to Blue server. GREEN_PORT is $GREEN_PORT&quot;
pm2 stop &quot;app-$GREEN_PORT&quot;
pm2 delete &quot;app-$GREEN_PORT&quot;
pm2 save
exit 1</code></pre>
<h3 id="4-헬스체크-전용-api-구현">4. 헬스체크 전용 api 구현</h3>
<p>헬스체크를 위한 API를 만들어야한다.-&gt;<code>app/health/route.ts</code></p>
<pre><code class="language-typescript">import { NextResponse } from &quot;next/server&quot;;

export const dynamic = &quot;force-dynamic&quot;;

export async function GET() {
  return NextResponse.json(
    {
      status: &quot;ok&quot;,
      message: &quot;Health check successful.&quot;,
      timestamp: new Date().toISOString(),
    },
    {
      status: 200,
    }
  );
}
</code></pre>
<h4 id="주의해야할-점-middleware설정">주의해야할 점 (middleware설정)</h4>
<p>만약 헬스 체크를 위한 api 요청을 보낼때 권한이 없어서(ex.로그인을 하지 않아 토큰이 없는 경우) 로그인 라우트로 리다이렉트가 될 수 있다. 그래서 헬스체크를 위한 api요청은 리다이렉트 대상이 되지 않도록 미들웨어에서 설정해야한다.
아래는 예시 코드다.</p>
<p>matcher에서 health관련 path는 검사하지 않도록 제외해주면 서버 내부에서 헬스체크를 해도 리다이렉트 되지 않고 안전하게 요청이 전달 된다.</p>
<pre><code class="language-typescript">import { updateSession } from &quot;@/lib/supabase/middleware&quot;;
import { type NextRequest } from &quot;next/server&quot;;

export async function middleware(request: NextRequest) {
  return await updateSession(request);
}

export const config = {
  matcher: [
    &quot;/((?!_next/static|health|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)&quot;,
  ],
};
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[FE가 낋여주는 배포 일지(3)_빌드 결과물 배포 전략,
램스왑으로 서버의 메모리 부족 문제 해결]]></title>
            <link>https://velog.io/@y-minion/FE%EA%B0%80-%EB%82%8B%EC%97%AC%EC%A3%BC%EB%8A%94-%EB%B0%B0%ED%8F%AC-%EA%B0%80%EC%9D%B4%EB%93%9C%EB%9D%BC%EC%9D%B83%EB%B9%8C%EB%93%9C-%EA%B2%B0%EA%B3%BC%EB%AC%BC-%EB%B0%B0%ED%8F%AC-%EC%A0%84%EB%9E%B5-%EB%B0%B0%ED%8F%AC%EC%A4%91-Ec2-%EB%A9%94%EB%AA%A8%EB%A6%AC%EC%9D%98-%EC%82%AC%EC%9A%A9%EB%9F%89-%EB%B6%84%EC%84%9D-%EB%9E%A8%EC%8A%A4%EC%99%91-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@y-minion/FE%EA%B0%80-%EB%82%8B%EC%97%AC%EC%A3%BC%EB%8A%94-%EB%B0%B0%ED%8F%AC-%EA%B0%80%EC%9D%B4%EB%93%9C%EB%9D%BC%EC%9D%B83%EB%B9%8C%EB%93%9C-%EA%B2%B0%EA%B3%BC%EB%AC%BC-%EB%B0%B0%ED%8F%AC-%EC%A0%84%EB%9E%B5-%EB%B0%B0%ED%8F%AC%EC%A4%91-Ec2-%EB%A9%94%EB%AA%A8%EB%A6%AC%EC%9D%98-%EC%82%AC%EC%9A%A9%EB%9F%89-%EB%B6%84%EC%84%9D-%EB%9E%A8%EC%8A%A4%EC%99%91-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Fri, 17 Oct 2025 04:16:56 GMT</pubDate>
            <description><![CDATA[<p>지난 시간에는 깃허브 액션의 러너에서 직접 ssh 접속을 하여 배포시도를 했다. 하지만 시간이 너무 오래걸려(10분) 실패했다. 
이 문제를 해결할 수 있는 새로운 방법에 대해서 알아보자.</p>
<h2 id="기존의-cd-플로우-비교-및-최적화-전략">기존의 CD 플로우 비교 및 최적화 전략</h2>
<p><strong>가장 느린 단계(빌드와 설치)</strong>를 <strong>빠르고 강력한 서버(CI Runner)</strong>에서 처리하고, <strong>가장 빠른 단계(파일 전송)</strong>만 배포 단계에 남기자.</p>
<h3 id="기존의-원격-빌드-플로우">기존의 원격 빌드 플로우</h3>
<ul>
<li>빌드/설치 장소 : EC2 인스턴스(저사양, 느림)</li>
<li>Cd 스텝: SSH $\rightarrow$ git pull $\rightarrow$ npm install $\rightarrow$ npm run build $\rightarrow$ pm2 reload</li>
<li>장점: 워크플로우 파일이 간결</li>
<li>단점: 느림. 타임 아웃 발생 위험</li>
</ul>
<h3 id="최적화할-플로우빌드-결과물-배포">최적화할 플로우(빌드 결과물 배포)</h3>
<ul>
<li>빌드/설치 장소 : GitHub Actions Runner</li>
<li>Cd 스텝: Runner: npm run build $\rightarrow$ 파일 전송 (rsync/scp) $\rightarrow$ SSH $\rightarrow$ pm2 reload</li>
<li>장점: CD 시간 대폭 단축. 서버 부하 감소.</li>
<li>단점: 파일 전송 로직이 추가되어 워크플로우 설정이 복잡(아티팩트 추가)</li>
</ul>
<h2 id="🚀-cd-시간-단축을-위한-최적화-플로우-구현">🚀 CD 시간 단축을 위한 최적화 플로우 구현</h2>
<p>현재의 타임아웃 문제를 근본적으로 해결하고 배포 시간을 단축하기 위해 GitHub Actions Runner에서 빌드를 완료한 후, EC2에는 최종 결과물만 전송하도록 워크플로우를 수정한다.</p>
<h3 id="1단계-레포지토리에서-코드-체크아웃">1단계: 레포지토리에서 코드 체크아웃</h3>
<p>러너에서 빌드를 해서 서버로 전송을 해야한다. 먼저 레포지토리에서 코드를 불러와야한다.
그리고 node.js의 환경도 실체 프로젝트의 노드 버전과 맞춰준다.</p>
<pre><code class="language-yaml">name: Deploy to EC2

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v5

      - name: Setup Node.js and Cache
        uses: actions/setup-node@v6
        with:
          node-version: &quot;22&quot;
          # npm install 속도 향상을 위한 캐시 (Optional)
          cache: &quot;npm&quot;
</code></pre>
<h3 id="2단계-의존성-설치--빌드--압축">2단계: 의존성 설치 &amp; 빌드 &amp; 압축</h3>
<p>러너에서 의존성 설치와 빌드를 해준다.</p>
<pre><code class="language-yaml">      - name: Install Dependencies &amp; Build Project
        run: |
          npm install
          npm run build

      # 🚨 핵심: 빌드 결과물을 압축 파일 하나로 묶기
      - name: Compress Build Artifacts
        run: |
          tar -czf artifact.tar.gz \
            .next \
            package.json \
            package-lock.json</code></pre>
<h3 id="3단계-빌드된-파일-artifact로-업로드">3단계: 빌드된 파일 Artifact로 업로드</h3>
<p>러너에서 빌드된 파일들을 모두 Artifact로 업로드를 해줘야한다.
이후에 다른 Job에서 해당 아티펙트를 다운받아서 ec2로 전송한다.</p>
<pre><code class="language-yaml">      - name: Upload Build Artifact
        uses: actions/upload-artifact@v4
        with:
          name: Supabase-Next.js-artifact #Supabase-Next.js-artifact이름으로 압축 업로드
          path: artifact.tar.gz
</code></pre>
<p>ec2에 보낼 파일은 압축된 파일을 보내준다.</p>
<h3 id="4단계-아티팩트-다운--ec2-전송">4단계: 아티팩트 다운 &amp; Ec2 전송</h3>
<p><code>deploy</code>라는 새로운 작업을 만든다. 그리고 해당 작업에서 build작업에서 업로드한 아티팩트를 다운받는다. 이때 이전의 작업인 build에서 아티팩트가 업로드 된후에 해당 작업이 실행되야 한다. build작업에 의존성을 걸어준다</p>
<pre><code class="language-yaml"> deploy:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name:  Download Compressed Artifact
        uses: actions/download-artifact@v4
        with:
          name: Supabase-Next.js-artifact # 다운받을 아티팩트 지정
          path: ./download/ # 압축 해제할 경로 지정
</code></pre>
<h3 id="5단계-아티팩트를-ec2로-전송">5단계: 아티팩트를 Ec2로 전송</h3>
<p>다운받은 아티팩트를 이제 Ec2로 전송해줄 차례다. 전의 워크플로우에서는 Ec2에서 빌드,의존성 설치를 했다. 하지만 해당 워크플로우에서는 속도 향상을 위해 러너환경에서 이 작업들을 대신 해준 것이다.</p>
<pre><code class="language-yaml">      - name: Transfer Artifacts to EC2 via SCP
        uses: appleboy/scp-action@v1
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}

          source: &quot;./download/artifact.tar.gz&quot;
          target: &quot;~/aws-playground&quot;
</code></pre>
<h3 id="6단계-ssh-접속후-서비스-재시동">6단계: SSH 접속후 서비스 재시동</h3>
<p>빌드 파일까지 전송이 완료된 상태다. 이제 서버에 ssh 연결후 의존성을 재설치하고 pm2를 이용해 서비스를 새로고침한다.</p>
<pre><code class="language-yaml">      - name: Restart Service via SSH
        uses: appleboy/ssh-action@v1
        with:
          # 🌟 1. 접속 정보
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          timeout: 30m

          # 2. 원격 실행 스크립트
          script: |
            set -x # 모든 명령어 실행 전후를 상세히 로깅

            NVM_BIN_PATH=&quot;/home/ubuntu/.nvm/versions/node/v22.20.0/bin&quot;
            export PATH=&quot;$NVM_BIN_PATH:$PATH&quot;

            echo &quot;--- 1. Directory Change ---&quot;
            cd ~/aws-playground || exit 1

            # 🚨 핵심: 전송된 압축 파일을 해제
            tar -xzf artifact.tar.gz

            echo &quot;--- 2. NPM INSTALL START ---&quot;
            npm ci # 🌟 npm ci로 변경 권장

            echo &quot;--- 3. NPM INSTALL FINISHED ---&quot; # 이 메시지가 출력되는지 확인
            pm2 reload 0 || exit 1

            set +x
            exit 0
</code></pre>
<h2 id="결과">결과</h2>
<p><img src="https://velog.velcdn.com/images/y-minion/post/e9944e0d-a55d-4f81-9253-deefc89939ca/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/y-minion/post/19a86cb3-8f46-48fb-a533-1f8a8979eb71/image.png" alt=""></p>
<p><del>개같이 실패</del></p>
<p>그래도 기존에 압축하지 않고 Ec2에 아티팩트를 전달할때는 시간이 오래걸리기도 했는데, 압축하니 안정적으로 Ec2에 보내진다.
하지만 여전히 SSH 연결 후 의존성 설치에서 실패한다.</p>
<h2 id="원인-분석">원인 분석...</h2>
<p>aws 계속 SSH 연결까지는 잘 되지만, 서버에서 의존성을 설치할때 timeout 에러가 발생한다. 하지만 CPU 사용량을 모니터링해도 문제는 없다.
<img src="https://velog.velcdn.com/images/y-minion/post/86dd99eb-d2cf-46ef-b87d-2ff93464e7e1/image.png" alt=""></p>
<p>그렇다면...메모리가 설마..?
-&gt; 그렇다. 나는 무료티어라서 인스턴스의 메모리가 1기가라서 충분히 그럴 위험이 있다.
내 눈으로 직접 확인해야겠다.</p>
<h2 id="🔎-sar-도구를-활용한-cd-디버깅">🔎 sar 도구를 활용한 CD 디버깅~!</h2>
<p>sar은 백그라운드에서 동작하며 시스템 활동 기록을 저장한다. 나는 npm ci 실행 시점에 이 기록을 확인해 메모리 과부하가 발생했는지 판단할 예정이다.</p>
<h3 id="1-ec2-사전-준비-sysstat-설치-최초-1회">1. EC2 사전 준비: sysstat 설치 (최초 1회)</h3>
<p>EC2 인스턴스에 SSH로 접속하여 sysstat 패키지를 설치한다.</p>
<pre><code class="language-yaml">sudo apt update
sudo apt install sysstat # (Ubuntu/Debian 기준임)</code></pre>
<h3 id="2-deploy-job-수정-원격-스크립트에-sar-통합">2. deploy Job 수정: 원격 스크립트에 sar 통합</h3>
<p>배포 스크립트에서 sar 명령을 실행하여 실시간으로 램(RAM)사용량을 기록한다.</p>
<pre><code class="language-yaml"># .github/workflows/cd.yml (Restart Service via SSH 스텝)
      - name: Restart Service via SSH
        uses: appleboy/ssh-action@v1
        with:
          # ... (접속 정보 및 timeout 유지) ...
          script: |
            NVM_BIN_PATH=&quot;/home/ubuntu/.nvm/versions/node/v22.20.0/bin&quot;
            export PATH=&quot;$NVM_BIN_PATH:$PATH&quot;

            cd ~/aws-playground || exit 1

            # 🚨 1. (디버깅) sar 명령을 통해 2초마다 15번 메모리 상태를 백그라운드에 기록한다.
            # 이 기록이 npm ci 실행 시간대와 겹치게 된다.
            sar -r -n DEV -o ~/sar_history.log 2 15 &amp; 
            SAR_PID=$!

            echo &quot;--- NPM CI START ---&quot;
            npm ci 

            # 🚨 2. npm ci 완료 후 sar 프로세스 종료 및 로그 파일 확인
            kill $SAR_PID || true

            echo &quot;--- SAR LOG (MEMORY/SWAP) ---&quot;
            sar -r -f ~/sar_history.log # 메모리/스와프 사용량 출력
            sar -d -f ~/sar_history.log # 디스크 I/O 

            echo &quot;--- NPM CI FINISHED ---&quot;
            pm2 reload 0 || exit 1</code></pre>
<h3 id="3-로그-분석을-통한-진단">3. 로그 분석을 통한 진단</h3>
<p><code>%memused</code> (사용된 메모리 비율)이 로그 기록 내내 84% ~ 85% 사이를 유지하고 있다.
<img src="https://velog.velcdn.com/images/y-minion/post/5d4efa11-2207-45a6-a9e5-8de5c6939fcf/image.png" alt="">
서버의 메모리 사용량이 너무 높아져서 계속해서 실패했었다.</p>
<h2 id="새로운-시도---램스왑">새로운 시도 -&gt; 램스왑</h2>
<ul>
<li><p><strong>현재 상황</strong>: aws에서 프리티어 계정에게 무료로 제공 인스턴스를 사용중. 메모리는 1기가인데 자동배포 과정에서 SSH연결로 의존성을 새로 서버에 설치하는 과정에서 메모리가 감당을 못해 계속해서 서버가 꺼지는 문제가 발생.</p>
</li>
<li><p><strong>해결 방법</strong>: 램스왑을 통해 부족한 RAM 공간을 디스크의 여유 공간으로 충당한다. 비록 램스왑중에 속도는 늦어질 수 있지만 <del>서버가 죽는것 보다는 최고!</del>(추후에 aws codedeploy로 안정적인 자동배포 환경을 구축할 예정이다.)</p>
</li>
</ul>
<h3 id="swap-space스왑-공간">Swap Space(스왑 공간)</h3>
<p>램스왑을 실행하기 전에 먼저 스왑할 공간을 만들어야한다. 스왑 공간은 램의 크기에 비례해서 만들어준다. 현재 램이 1기가이므로 2배인 2기가인 공간을 디스크에 만들어준다.</p>
<ul>
<li><code>fallocate</code> : 파일시스템 수준에서 디스크 공간을 미리 할당하여 파일을 생성하는 명령어</li>
<li><code>-l</code> : 파일의 크기 할당 옵션</li>
<li><code>chmod</code> : 스왑 파일의 권한을 설정해준다. -&gt; 오직 소유자만 읽고 쓸수 있도록 권한 설정.</li>
<li><code>mkswap</code> : ⭐️ 해당 파일이 스왑공간으로 사용될 수 있도록 포맷실행<pre><code class="language-yaml">sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile</code></pre>
</li>
</ul>
<h3 id="swap-실행">swap 실행</h3>
<p>아래 명령어를 통해 RAM Swap을 실행한다.</p>
<pre><code class="language-yaml">sudo swapon /swapfile</code></pre>
<h3 id="ram-swap-활성화">RAM swap 활성화</h3>
<p>지금 상태로는 서버가 꺼지면 RAM swap기능도 꺼진다. 서버가 재부팅되면 자동을 램스왑이 활성화 되도록 설정해야한다.</p>
<pre><code class="language-yaml">sudo vi /etc/fstab </code></pre>
<p>해당 파일을 열어서 수정해줘야한다.
<code>/swapfile swap swap defaults 0 0</code>
해당 명령어를 입력한다. (/swapfile 이라는 파일을 시스템의 스왑 공간으로 영구히 사용하겠다)
<img src="https://velog.velcdn.com/images/y-minion/post/23e36026-6330-4819-87d3-0baaf4dc792e/image.png" alt=""></p>
<p>이렇게 되면 모든 설정이 끝났다.</p>
<p><img src="https://velog.velcdn.com/images/y-minion/post/b299f906-de6a-4c80-916d-7681adae51cf/image.png" alt=""></p>
<p>CI/CD 작업이 모두 정상적으로 돌아가는것을 확인 할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[FE가 낋여주는 배포 일지(2)_CI/CD를 해보자.  🐶개초보도 할 수 있다!]]></title>
            <link>https://velog.io/@y-minion/FE%EA%B0%80-%EB%82%8B%EC%97%AC%EC%A3%BC%EB%8A%94-%EB%B0%B0%ED%8F%AC-%EA%B0%80%EC%9D%B4%EB%93%9C%EB%9D%BC%EC%9D%B82CICD%EB%A5%BC-%ED%95%B4%EB%B3%B4%EC%9E%90.-%EA%B0%9C%EC%B4%88%EB%B3%B4%EB%8F%84-%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8B%A4</link>
            <guid>https://velog.io/@y-minion/FE%EA%B0%80-%EB%82%8B%EC%97%AC%EC%A3%BC%EB%8A%94-%EB%B0%B0%ED%8F%AC-%EA%B0%80%EC%9D%B4%EB%93%9C%EB%9D%BC%EC%9D%B82CICD%EB%A5%BC-%ED%95%B4%EB%B3%B4%EC%9E%90.-%EA%B0%9C%EC%B4%88%EB%B3%B4%EB%8F%84-%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8B%A4</guid>
            <pubDate>Thu, 16 Oct 2025 05:40:06 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가기에-앞서">들어가기에 앞서...</h2>
<p>팀플에서 지속적인 배포은 굉장히 중요하다고 생각한다. 그것이...팀플이니까...!
사실 예전에 프론트1,백2로 팀플했을때 CI/CD 가 전혀 구축이 되지 않은 상태로 진행한 적이 있었다. 그래서 내가 만든 페이지들이 과연 BE 코드와 합쳐졌을때 돌아갈라나..? 이런 불안함에 프로젝트를 진행했었다.(<del>그래서 직접 express로 서버도 구현해서 로컬에서 테스트를 했다</del>) 하지만 로컬이 아닌 실제 배포환경에서 내가 만든 프론트 코드들이 BE의 코드와 잘 상호작용을 하는지는 다른문제다. 그리고 제일 큰 문제는 지속적인 통합과 배포가 없다보니 실제 서비스(배포) 환경에서 테스트를 할 수 없었다. 정말 내가 의지할 곳은 테스트 코드와 내가만든 노드서버뿐...! 그래서 이번에 새로운 서비스를 만드는데 팀원이 모두 프론트지만, 반드시 CI/CD는 구축해서 팀원들과 내가 다른 걱정 없이 개발에만 집중 할 수 있도록 하는게 목표다.</p>
<blockquote>
<p>실습 환경은 <a href="https://velog.io/@y-minion/SupabaseAppRouter%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EB%B0%B0%ED%8F%AC%ED%95%B4%EB%B3%B4%EC%9E%90">FE가 낋여주는 배포 일지(1)</a> 에서 만든 ec2 인스턴스에서 진행한다.</p>
</blockquote>
<blockquote>
<p>⭐️ 깃허브 액션을 사용한다 === CI/CD 작업을 <strong>대신 해주는 서버(컴퓨터)</strong>가 어떤 작업을 할지 조작한다! 라고 생각하면 된다!</p>
</blockquote>
<p><del>만약 모르는 용어가 있다면 직접 검색해서 찾아보길...</del> 그래도 이 글을 끝까지 읽으면 전체 흐름을 알 수 있다</p>
<h2 id="목차">목차</h2>
<ul>
<li>지속적 통합_CI</li>
<li>CD를 위한 사전 준비 작업</li>
<li>지속적 배포_CD
일단 이렇게 진행된다.</li>
</ul>
<h3 id="전체-아키텍처">전체 아키텍처</h3>
<p>해당 글에서 진행하는 아키텍처는 다음과 같다.
여기서 아래에 Ec2와 슈퍼베이스부분은 이미 <a href="https://velog.io/@y-minion/SupabaseAppRouter%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EB%B0%B0%ED%8F%AC%ED%95%B4%EB%B3%B4%EC%9E%90">FE가 낋여주는 배포 가이드라인(1)</a> 에서 진행을 했다. 이번시간에는 위에 있는 부분을 진행한다. 개발자가 로컬에서 깃허브 레포지토리에 Push를 하면 자동으로 CI를 진행하고, 특정 브랜치에 머지가 되면 배포가 되도록 한다.
<img src="https://velog.velcdn.com/images/y-minion/post/e775abe4-7742-4b7f-97ff-03e686d80886/image.svg" alt=""></p>
<h2 id="지속적-통합_ci">지속적 통합_CI</h2>
<p>각 팀마다 CI의 워크 플로우는 다를 수 있지만, 나는 PR이 병합되기 전, 모든 잠재적 위험을 발견하는 &#39;게이트&#39; 역할 목적으로 설계를 했다.</p>
<h3 id="✅-a-ci-트리거-설정-actions-yaml">✅ A. CI 트리거 설정 (Actions YAML)</h3>
<p>CI는 그냥 통합을 해주는게 아니다. 특정 트리거를 지정해주면, 해당 트리거가 발동 될때마다 CI가 실행된다. 이번 실습에서는 <strong>PR 이벤트</strong>와 <strong>브랜치 푸시 이벤트</strong> 두 가지를 모두 사용한다.</p>
<table>
<thead>
<tr>
<th>트리거 이벤트</th>
<th>GitHub Actions YAML</th>
<th>역할 및 목적</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Pull Request</strong></td>
<td><code>on: [pull_request]</code></td>
<td><strong>병합 차단 게이트:</strong> 테스트/빌드가 실패하면 병합 버튼을 비활성화하여 메인 브랜치 오염 방지.</td>
</tr>
<tr>
<td><strong>Push</strong></td>
<td><code>on: [push]</code></td>
<td><strong>신속 피드백: 팀원이</strong> feature 브랜치에 푸시할 때마다 테스트하여, 로컬 환경에서 놓친 문제점을 즉시 알려줌.</td>
</tr>
</tbody></table>
<h3 id="✅-b-ci-필수-job-구성">✅ B. CI 필수 Job 구성</h3>
<p>이제 각 워크플로우가 어떤 일을 수행할지 설계해보자.
이번 실습에서는 단순 실습이기에 최소 job을 수행한다(테스트,린트검사 제외).
즉, 5개의 스텝들로 이루어진 하나의 job을 실행할 예정이다.</p>
<h4 id="steps-목록">steps 목록</h4>
<ol>
<li>레포지토리 코드 체크아웃<ul>
<li>깃허브의 레포지토리에서 우리의 프로젝트 코드를 러너로 받아와야 한다.</li>
</ul>
</li>
<li>Node.js 환경설정.<ul>
<li>기본적으로 러너에는 노드가 설치되어 있다. 하지만 우리가 프로젝트에서 사용한 노드의 버전과 러너의 버전의 싱크가 일치하지 않을 수 있다. 그래서 우리의 프로젝트에서 사용한 node의 버전을 명시해 줘야한다. (<del>분명 내 컴퓨터에서는 잘 돌아갔는데..? 방지</del>)</li>
</ul>
</li>
<li>NPM 의존성 캐싱<ul>
<li>CI에서 시간이 제일 오래 걸리는 부분이 node_modules를 설치하는 부분이다. 하지만 이 폴더를 캐싱하면 매번 CI가 실행될때마다 새로설치하는게 아니라 캐시된 디렉토리를 사용해서 시간이 줄어든다.</li>
</ul>
</li>
<li>의존성 설치<ul>
<li>러너에 우리의 프로젝트 의존성들을 모두 설치해준다</li>
</ul>
</li>
<li>빌드 유효성 검사<ul>
<li>러너에서 프로젝트 코드가 빌드되는지 검사한다.</li>
</ul>
</li>
</ol>
<hr>
<h3 id="실습">실습!</h3>
<h4 id="workflow-위치">workflow 위치</h4>
<p>CI를 진행하려면 workflow가 있어야한다. 반드시 워크플로우 파일은 깃허브 레포지토리의 최상단의 /.github/workflows 디렉토리안에 있어야 깃허브 액션이 실행될 수 있다.
<img src="https://velog.velcdn.com/images/y-minion/post/5940796b-30a9-459e-8875-f4645e0488b2/image.png" alt=""></p>
<h4 id="workflow-생성yml">workflow 생성(.yml)</h4>
<h5 id="⚙️-워크플로우-이름">⚙️ 워크플로우 이름</h5>
<ul>
<li><p>먼저 해당 워크플로우의 이름을 만들어줘야한다. 이는 각 워크플로우를 식별 할 수 있는 이름이 된다.
<img src="https://velog.velcdn.com/images/y-minion/post/8a77c874-78ea-4c2a-bc75-a08c7ddc3661/image.png" alt=""></p>
</li>
<li><p>yaml 파일</p>
<pre><code class="language-yaml"># 워크플로우의 이름
name: CI Pipeline</code></pre>
</li>
</ul>
<hr>
<h5 id="⚙️-워크플로우-트리거-설정">⚙️ 워크플로우 트리거 설정</h5>
<p>CI가 언제 실행되는지 트리거를 정해줘야한다.
나는 main 브랜치에 push작업이 발생하거나, main브랜치에 PR이 발생하면 실행되도록 설정했다.</p>
<ul>
<li><code>on</code>: 언제 CI가 실행되는지 트리거를 설정할 수 있다.</li>
<li><code>push</code>: 푸시 발생을 트리거로 설정한다.</li>
<li><code>pull_request</code> : PR이 발생을 트리거로 설정한다. 이때 브랜치또한 key로 정해줘야한다.</li>
<li><code>branches</code> : 브랜치를 여러개 선택할 수 있다. 해당 배열안에 원하는 브랜치를 넣으면 된다.</li>
</ul>
<pre><code class="language-yaml">on:
  # main 브랜치로 Pull Request가 생성/업데이트될 때
  pull_request:
    branches: [&quot;main&quot;]
  # main 브랜치에 직접 Push될 때 (선택사항)
  push:
    branches: [&quot;main&quot;]</code></pre>
<hr>
<h5 id="⚙️-실행될-작업job들-설정">⚙️ 실행될 작업(Job)들 설정</h5>
<p>이 작업들의 이름또한 지정해줘야 한다. 작업들의 이름으로 하나의 워크플로우에서 일어나는 여러 작업들을 식별할 수 있다. 사진과 같이 각각의 job들이 실행되면서 각 작업의 로그를 추적할 수 있다.
<img src="https://velog.velcdn.com/images/y-minion/post/fd41a7be-a3fb-49be-bca9-c8f29caa7050/image.png" alt=""></p>
<p>이번 실습에서는 작업 이름을 <code>build</code>로 지정한다.</p>
<pre><code class="language-yaml">jobs:
  # 빌드 및 테스트를 담당하는 메인 작업
  build: # &lt;----jobs의 하위에 원하는 작업 이름을 지정하면 된다.(ex.build,test,job)
    # 가상 환경 지정
    runs-on: ubuntu-latest
</code></pre>
<p>위의 코드를 보면 <code>runs-on</code>이라는게 등장한다. 그리고 해당 키의 값에는 운영체제가 있다.
깃허브 액션은 위에서 말했듯이 서버를 실행시켜서 우리의 CI/CD 작업들을 대신 해주는 서비스다. 이때 우리가 만든 워크플로우의 각각의 jobs들이 runner(깃헙 액션 서버)에 의해서 실행된다. 그래서 우리는 우리의 작업들을 실행해줄 runner들의 운영체제를 선택해줘야한다. windows,macos를 선택해도 된다. 하지만 운영체제에 따라서 CI/CD 속도가 달라진다. 우리는 그중에서 제일 빠른 우분투를 사용한다.
<img src="https://velog.velcdn.com/images/y-minion/post/404e1f5e-bc1a-472d-8158-2f5ed993cb6f/image.png" alt=""></p>
<hr>
<h5 id="⚙️-실행될-스텝steps들-설정">⚙️ 실행될 스텝(steps)들 설정</h5>
<p>이제 하나의 작업에서 실행될 스텝들을 지정할 차례다. 이 스텝은 위의 jobs와는 다르게 <strong>순차적으로 실행</strong>된다. 그리고 이 steps들은 특정 job에 종속되어 있다. steps의 코드를 살펴보기 전에 steps에 사용되는 다음 항목들을 먼저 알아보자.</p>
<ul>
<li><p><code>name</code>: 스텝을 식별할 수 있는 이름</p>
</li>
<li><p><code>uses</code>: 깃허브 액션의 marketplace에서 사용할 액션 명시</p>
<ul>
<li>깃허브 액션에는 다른 사람들이 만들어 놓은 작업들을 사용할 수 있는 기능이 있다. 그리고 이 액션들이 모여있는 곳을 marketplace라고 부른다.
그리고 원하는 액션을 클릭하면 해당 액션의 사용법 또한 나와있다.
<img src="https://velog.velcdn.com/images/y-minion/post/98b69df7-fa8b-4833-900e-20cbee33418e/image.png" alt=""></li>
</ul>
</li>
<li><p><code>with</code>: 내가 선택한 액션을 사용할때 추가로 전달할 옵션들 명시. 이때 액션마다 추가로 전달할 옵션의 key가 다르다.(옵션의 예시로 다음과 같은 것들이 올 수 있다. -&gt; node-version,path,key) 각 액션마다 다르니 해당 액션의 사용법을 읽어봐야 한다.
<img src="https://velog.velcdn.com/images/y-minion/post/53a6ddd2-27d1-4cef-bf55-e02a6e14a1de/image.png" alt=""></p>
</li>
<li><p><code>run</code> : 워크플로우를 실행하는 러너가 서버라고 말했다. 그리고 우리는 러너의 운영체제를 선택했다. 이제 서버(=러너)에 전달하고 싶은 명령어를 워크플로우에 작성하면 해당 작업들이 러너 안에서 실행된다. 이때 명령어는 반드시 <code>run</code> 이라는 key의 값으로 전달이 되야한다. 예시로 run: echo &quot;Hello, World!&quot; 라고 하면 러너의 로그에 &quot;Hello, World!&quot;가 출력된다. 그래서 <code>run</code> 명령어를 잘 사용하면 우리가 원하는 대로 서버가 동작하게 할 수 있다.</p>
<blockquote>
<p>run 명령어를 통해 실행되는 모든 작업들은 러너안에서 실행되는것을 잊지 말자!</p>
</blockquote>
</li>
</ul>
<p>코드를 살펴보자. (주석들을 잘 읽어보자)</p>
<pre><code class="language-yaml">steps:
      # 1. 레포지토리 코드 체크아웃
      - name: Checkout repository
        uses: actions/checkout@v5

      # 2. Node.js 환경 설정
      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: &quot;22&quot; # 중요! -&gt; 현재 프로젝트에 맞는 Node.js 버전 지정

      # 3. NPM 의존성 캐싱 (2단계 Dependency Install 최적화)
      # - node_modules 폴더를 캐싱하여 매번 새로 설치하는 시간 절약
      - name: Cache node_modules
        uses: actions/cache@v4
        with:
          path: ~/.npm
          # 러너의 OS와 프로젝트의 package-lock.json를 key로 설정하여 해당 key가 바뀌지 않는 이상 캐시는 hit 된다!
          key: ${{ runner.os }}-node-${{ hashFiles(&#39;**/package-lock.json&#39;) }}
          restore-keys: |
            ${{ runner.os }}-node-

      # 4. 의존성 설치 (2단계 Dependency Install)
      # - CI 환경에서는 &#39;npm ci&#39;를 사용하는 것이 더 빠르고 안정적입니다.
      - name: Install Dependencies
        run: npm ci

      # 5. 빌드 유효성 검사 (3단계 Build Validation)
      - name: Run Build
        run: npm run build
</code></pre>
<p>위의 스텝들은 해당 글 상단에서 설명한 작업들을 yaml파일로 작성한 것이다.
러너가 프로젝트를 빌드하고 테스트하려면 의존성파일들이 있어야한다. 근데 CI를 실행할때마다 모든 노드모듈들을 설치한다면 시간이 많이 걸리게 된다. 그래서 설치하기 전에 먼저 <code>cache</code> 작업을 해줘야한다. 이렇게 되면 CI속도가 빨라진다.</p>
<p>그리고 <code>run</code>을 통해 의존성들을 설치해주고, 빌드한다.</p>
<p>이때 실제로 캐시가 되고있는지 확인하는 방법을 알아보자.
최초로 레포지토리에 push를 하면 CI작업이 실행되는데 캐시가 된게 없으므로 실패함을 알 수 있다. 그래서 최초로 의존성들을 설치한다. 이때 설치를 하면서 해당 러너에 캐싱을 한다.
<img src="https://velog.velcdn.com/images/y-minion/post/f1e34506-ee79-460f-8a3e-6dfc1a7470dd/image.png" alt=""></p>
<p>이후에 다시 push를 해보자.
<img src="https://velog.velcdn.com/images/y-minion/post/d791bcf1-0a2b-4357-a184-732e0bc04505/image.png" alt="">
캐시 히트됨을 알 수 있다. 그래서 그 뒤의 스텝인 의존성 설치 작업이 13s -&gt; 9s 로 단축된것을 확인 할 수 있다. 물론 해당 프로젝트는 실습용이기때문에 node_modules의 크기가 크지않다. 하지만 실제 프로젝트라면 이 캐시 작업덕분에 CI속도가 매우 빨라질 수 있다. <del>push해놓고 멍때리기 싫다면 꼭 캐시는 하자!</del></p>
<p>이로써 CI 는 끝났다.</p>
<hr>
<h2 id="지속적-배포_cd">지속적 배포_CD</h2>
<p>깃허브 액션을 이용해서 CD를 진행한다. 만약 처음 보는 사람이 있더라도 어렵게 생각하지 않아도 된다.
해당 글에서 다루는 깃헙 액션을 통한 CD는 수동 배포로 하던 작업들을 자동화 해주는 도구인 Github Actions를 통해 특정 조건을 만족하면 자동으로 배포를 하도록 관리하는 것 뿐이다.</p>
<h3 id="흐름을-먼저-알아보자">흐름을 먼저 알아보자</h3>
<p>Github Actions를 활용한 Ec2 배포를 위한 전체적인 흐름은 다음과 같다. 만약 이해가 안되더라도 일단 흐름만 알고 아래에서 직접 실습을 통해 알아볼 예정이다.</p>
<ol>
<li><p><strong>키페어 등록</strong>
EC2를 만들때 키페어를 만들 수 있다. 이 키페어 (ex.example-keypair.pem)를 깃허브 액션의 시크릿 환경변수에 저장을 하자.</p>
</li>
<li><p><strong>서버에 공개키 등록</strong>
Ec2의 인스턴스의 ~/.ssh/authorized_keys에 공개 키를 등록해야한다. 하지만! aws를 통해 ec2를 생성할때 <strong>키페어를 만들었다면</strong> 등록이 자동으로 된다.</p>
</li>
<li><p><strong>호스트 키 지문 획득 및 Secret 등록</strong></p>
<ul>
<li>호스트 키 지문 획득: ssh-keyscan 명령을 통해 EC2서버의 호스트 키 지문을 획득하자. -&gt; 깃헙 액션의 runner가 서버와 통신할때 신뢰할 수 있는 서버임을 증명 할 수 있다.</li>
<li>Secret 등록: 깃허브 레포지토리의 시크릿 환경변수에 다음 두가지를 등록한다.<ul>
<li>SSH_PRIVATE_KEY: EC2를 생성할때 만든 개인 키의 전체 내용 (사용자 인증용)<ul>
<li>KNOWN_HOSTS: 획득한 EC2 서버의 호스트 키 지문 정보. (서버 인증용)</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>CD 워크플로우(.yml)실행 단계</strong></p>
<ul>
<li>호스트 신뢰등록: KNOWN_HOSTS Secret을 사용하여 Runner의 ~/.ssh/known_hosts 파일에 서버의 신원을 등록한다.</li>
<li>SSH Agent 설정: webfactory/ssh-agent 액션을 사용하여 SSH_PRIVATE_KEY Secret을(3번에서 등록한 개인키)를 ssh-agent에 로드한다.</li>
<li>배포 실행: SSH연결을 통해 EC2 서버에 접속하고, 수동배포하듯이 배포 명령어를 실행한다.</li>
</ul>
</li>
</ol>
<hr>
<h3 id="1-키페어-등록">1. <strong>키페어 등록</strong></h3>
<p>배포할때 우리가 직접 하는게 아니라 깃허브 액션의 러너가 우리의 Ec2에 ssh로 접속해서 배포를 하게 된다. 이때 러너가 성공적으로 ssh접속을 하려면 반드시 해당 인스턴스의 SSH 프라이빗 키가 있어야한다. 이를 위해 EC2를 만들때 발급받은 키페어를 깃허브의 환경변수에 저장을 한다. 그리고 배포하는 워크플로우에서 해당 환경변수를 사용하여 ssh로 접속하게 된다.
프로젝트 레포지토리의 [Settings]&gt;[Actions secrets and variables
]&gt;[Actions] 순서로 이동한다.
<img src="https://velog.velcdn.com/images/y-minion/post/13f36df3-cc4e-43e9-9174-160c7bd8aa08/image.png" alt="">
이제 인스턴스를 생성할때 만든 프라이빗 키를 등록할 차례다. 간혹가다가 mac에서 프라이빗 키가 안열리는 경우가 있다. 제일 확인한 방법은 터미널을 통해서 <code>vi</code>편집기를 이용해 프라이빗 키를 확인하는게 제일 확실하다.
<code>vi example-key.pem</code> 을 통해 프라이빗키를 확인한다. 아래와 같이 프라이빗 키를 확인할 수 있다. (이 키는 절대 공유해서도 안되고 외부에 노출되어선 안된다!!)
<img src="https://velog.velcdn.com/images/y-minion/post/88e16984-ff0e-4a49-b396-682b588a0bdf/image.png" alt="">
해당 ssh 프라이빗 키 본문(-----BEGIN OPENSSH PRIVATE KEY-----로 시작하는 부분 부터)을 모두 복사해서 깃허브 시크릿 환경변수에 넣어준다. 그리고 저장을 한다. 이제 워크플로우에서 <code>${{ secrets.SSH_PRIVATE_KEY }}</code>로 해당 키를 사용할 수 있다.</p>
<h3 id="2-서버에-공개키-등록">2. <strong>서버에 공개키 등록</strong></h3>
<p>aws를 통해서 ec2를 만들때 프라이빗 키를 생성했다면 이미 인스턴스에 키가 등록되어 있다.
먼저 해당 인스턴스에 원격으로 접속한다.</p>
<pre><code class="language-bash">ssh {username}@{인스턴스 IP}</code></pre>
<p>.ssh 디렉토리에 인증관력 키들이 모여있다. authorized_keys를 vi 편집기 이용해서 확인해보면 공개키가 등록되어 있음을 확인 할 수 있다.
<img src="https://velog.velcdn.com/images/y-minion/post/833414d5-e8d5-467f-b4c3-2ddf965c4feb/image.png" alt=""></p>
<h3 id="3-호스트-키-지문-획득-및-secret-등록">3. <strong>호스트 키 지문 획득 및 Secret 등록</strong></h3>
<p>깃허브 액션의 러너가 우리의 서버에 접근하려고 할때 누군가 우리의 서버인척 흉내를 낼 수 있다. 이때 러너가 실제로 접속한 서버가 우리가 의도한 서버가 맞는지 확인하는 절차를 하지 않으면 <strong>중요한 정보들이 탈취</strong>당할 수 있다.(<a href="https://www.ibm.com/kr-ko/think/topics/man-in-the-middle"><strong>중간자 공격(Man-in-the-Middle Attack)</strong></a>) 그래서 실제 서버의 호스트키를 획득해 러너가 배포를하기 위해 서버에 접속할때 러너가 갖고있는 서버의 호스트 키 지문과 일치하는지 확인 작업을 해야한다. 이때 호스트키 지문을 획득하면 러너가 사용할 수 있도록 깃허브 시크릿에 등록을 해야한다.</p>
<p>우리 서버의 호스트 키를 확인해보자.</p>
<pre><code class="language-bash">ssh-keyscan 서버의IP주소</code></pre>
<p>위의 명령어를 입력하면 호스트 키 본문이 출력된다. 이 본문을 모두 복사해서 깃헙 환경변수로 저장한다.
<img src="https://velog.velcdn.com/images/y-minion/post/93fa8bde-250e-47c9-a687-70bad6e3c5e0/image.png" alt=""></p>
<h3 id="4-cd-워크플로우yml실행-단계">4. <strong>CD 워크플로우(.yml)실행 단계</strong></h3>
<p>이제 진짜 YAML 파일을 통해 CD 워크플로우를 만들 차례다.
해당 GitHub Actions 워크플로우는 main 브랜치에 코드가 푸시될 때마다 원격 EC2 인스턴스에 접속하여 최신 코드를 받아와 빌드하고 서비스를 재시작하는 지속적 배포(CD) 파이프라인이다.</p>
<h4 id="⚙️-이름--트리거-설정">⚙️ 이름 &amp; 트리거 설정</h4>
<p>main 브랜치에 push 이벤트가 발생하면 배포가 실행되도록 한다.</p>
<pre><code class="language-yaml">name: Deploy to EC2

#트리거 설정
on:
  push:
    branches:
      - main</code></pre>
<hr>
<h4 id="⚙️-jobs--러너-실행-환경">⚙️ jobs &amp; 러너 실행 환경</h4>
<pre><code class="language-yaml">jobs:
# job의 이름은 deploy 로 설정
  deploy:
    runs-on: ubuntu-latest #러너의 os는 우분투로 설정</code></pre>
<hr>
<h4 id="⚙️-스텝-설정_체크아웃">⚙️ 스텝 설정_체크아웃</h4>
<pre><code class="language-yaml">    steps:
      - name: Checkout code
        uses: actions/checkout@v5</code></pre>
<p>해당 워크플로우의 전략은 ssh로 EC2 인스턴스에서 직접 코드를 Pull받아와서 실행하는 구조다. 얼핏 보기에는 굳이 체크아웃이 필요할까...? 라는 생각을 할 수 있다. 맞다. 지금 당장은 굳이 체크아웃을 할 필요가 없다. 그리고 리소스 낭비일 수 있다.
하지만 추후에 워크플로우에서 서버에 접속하기 전에 배포스크립트를 먼저 읽어야 하거나, 추후에 다른 작업들이 생길 수 있다. 이때는 반드시 체크아웃을 해야한다. 그래서 유지보수성 및 다른 추후에 생길 다른 액션과의 호환성을 위해 실행한다.</p>
<hr>
<h4 id="⚙️-스텝-설정_호스트-키-셋업--ssh-접속-준비">⚙️ 스텝 설정_호스트 키 셋업 &amp; SSH 접속 준비</h4>
<p>러너가 서버에 접속할때 신뢰할 만한 서버인지 확인할 수 있도록 서버의 호스트 키를 러너의 디스크에 저장해야한다.
그리고 해당 파일의 권한 설정을 해줘야 서버에 접속할때 안전하게 접속 할 수 있다.</p>
<pre><code class="language-yaml">      - name: Setup Known Hosts
        run: |
          mkdir -p ~/.ssh
          echo &quot;${{ secrets.KNOWN_HOSTS }}&quot; &gt; ~/.ssh/known_hosts
          chmod 644 ~/.ssh/known_hosts</code></pre>
<p>러너의 디스크에 .ssh 디렉토리를 생성한다.(<code>.shh</code> 디렉토리에 호스트 키가 있어야 ssh 접속시 해당 디렉토리에서 호스트 키를 찾아 확인 할 수 있다.)</p>
<p>다음 스텝에는 러너가 ssh연결을 하도록 한다. 이때 ssh연결을 하려면 클라이언트 서버의 디스크에 프라이빗 키가 있어야한다. 하지만 이때 러너의 디스크에 직접 프라이빗 키를 넣게되면 러너 환경에 이 프라이빗 키가 남아 보안 위험이 발생할 수 있다.
그래서 <strong>키를 파일로 저장하는 대신 메모리에서만 작동하는 ssh-agent라는 프로그램에 키를 로드한다.</strong> 이렇게 되면 키는 메모리에 안전하게 저장되며, 작업이 끝나면 자동으로 제거된다.</p>
<pre><code class="language-yaml">      - name: Setup SSH
        uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}</code></pre>
<p>이제 ssh 연결할 준비도 끝났다.</p>
<hr>
<h4 id="⚙️-스텝-설정_ec2에-배포하기">⚙️ 스텝 설정_EC2에 배포하기</h4>
<p>이 스텝에서는 직접 러너가 서버에 ssh연결을 한다. 그 이후에 서버에서 실행할 명령어들을 <code>run</code>을 통해 전달한다.
이때 깃허브 환경변수에 <code>USERNAME</code>에는 ssh 접속 유저네임을 등록하고, <code>HOST</code>에는 실제로 접속할 서버의 IP 주소를 등록해야한다.</p>
<pre><code class="language-yaml">      - name: Deploy to EC2
        run: |
          ssh ${{ secrets.USERNAME }}@${{ secrets.HOST }} &quot;
            cd ~/{서버에 존재하는 프로젝트 디렉토리} &amp;&amp;
            git pull &amp;&amp;
            npm install &amp;&amp;
            npm run build &amp;&amp;
            pm2 reload 0
          &quot;</code></pre>
<hr>
<p>전체 yaml 파일은 다음과 같다.</p>
<pre><code class="language-yaml">name: Deploy to EC2

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v5

      - name: Setup Known Hosts
        run: |
          mkdir -p ~/.ssh
          echo &quot;${{ secrets.KNOWN_HOSTS }}&quot; &gt; ~/.ssh/known_hosts
          chmod 644 ~/.ssh/known_hosts

      - name: Setup SSH
        uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - name: Deploy to EC2
        run: |
          ssh ${{ secrets.USERNAME }}@${{ secrets.HOST }} &quot;
            cd ~/aws-playground &amp;&amp;
            git pull &amp;&amp;
            npm install &amp;&amp;
            npm run build &amp;&amp;
            pm2 reload 0
          &quot;
</code></pre>
<h3 id="🚨-에러">🚨 에러</h3>
<p>하지만 위의 스크립트를 실행하면 에러가 발생한다.
<img src="https://velog.velcdn.com/images/y-minion/post/09f3218e-98a0-4f96-9e2a-99bc67c414eb/image.png" alt="">
npm과 pm2 명령어를 스크립트로 전달했지만 ssh 연결시 해당 파일을 못찾고 있다. 분명 서버에 직접 ssh 연결해서 실행하면 잘 돌아갔지만 깃허브 액션을 통해 실행하면 파일을 못 찾고 있다.</p>
<blockquote>
<p>⭐️ 이 문제를 해결하려면 사용자가 직접 SSH로 접속할 때와 다르게  <code>appleboy/ssh-action</code>을 통해 SSH로 접속하는 경우에는 <code>.bashrc</code>나 <code>.profile</code> 같은 파일을 로드하지 않는 비대화형 셸 환경으로 접속한다는 것을 알아야한다.</p>
</blockquote>
<h4 id="☑️-사용자가-직접-ssh로-접속하는-경우">☑️ 사용자가 직접 SSH로 접속하는 경우</h4>
<p>사용자가 SSH로 접속하면, <strong>서버는 $HOME/.bashrc나 $HOME/.profile 같은 파일을 로드하여</strong> Node.js/NVM의 경로(예: /home/ec2-user/.nvm/versions/node/v20.10.0/bin/)를 $PATH에 자동으로 추가해 준다. 따라서 npm만 입력해도 경로를 찾을 수 있다.
즉, 서버가 알아서 파일들을 로드해 줘서 사용자는 npm이나 pm2의 실제 파일 경로를 입력하지 않고 단지 <code>pm2</code>, <code>npm</code>만 입력해도 사용할 수 있던 것이다.</p>
<h4 id="☑️-github-actions-원격-실행-시">☑️ GitHub Actions 원격 실행 시</h4>
<p>GitHub Actions의 ssh 명령은 비대화형 셸로 실행된다. 이 셸은 사람이 타이핑할 필요가 없으므로 속도와 효율성을 위해 위의 .bashrc나 .profile 파일(사용자 정의 설정 파일)을 자동으로 로드하지 않는다. 이때 만약 Node.js나 NVM, PM2 같은 도구가 표준 시스템 경로에 설치 되지 않고 사용자 홈 디렉토리에 설치 되어 있다면,pm2나 npm과 같은 명령들의 실행 경로를 서버가 찾을 수 없어 에러를 띄운다.
<img src="https://velog.velcdn.com/images/y-minion/post/3b06b8af-a537-4d79-9d92-e6c343050a22/image.png" alt="">
-&gt; 모두 사용자 디렉토리에 설치되어있음을 확인 할 수 있다.</p>
<h3 id="✅-해결-시도">✅ 해결 시도</h3>
<p>GitHub Actions의 script 블록에서 npm과 pm2 명령어 대신, 확인된 절대 경로를 직접 사용하여 문제를 해결한다.</p>
<pre><code class="language-yaml">          script: |
            NVM_BIN_PATH=&quot;/home/ubuntu/.nvm/versions/node/v22.20.0/bin&quot;

            cd ~/aws-playground || exit 1
            git pull origin main
            $NVM_BIN_PATH/usr/bin/npm install
            $NVM_BIN_PATH/usr/bin/npm run build
            $NVM_BIN_PATH/usr/bin/pm2 reload 0
</code></pre>
<p><img src="https://velog.velcdn.com/images/y-minion/post/4f52a797-227b-4ab6-8d76-59b0dfb0f68a/image.png" alt="">
정상적으로 명령어들을 찾아서 실행됨을 확인 할 수 있다...</p>
<p>...하지만 시간이 너무 오래 걸려 timeout 에러가 발생했다.
<img src="https://velog.velcdn.com/images/y-minion/post/c171a859-1806-459e-9980-4f43afea3db7/image.png" alt=""></p>
<h2 id="🚨-큰-문제">🚨 큰 문제</h2>
<h3 id="1-낮은-사양의-ec2-인스턴스에서-발생하는-빌드-과부하">1. 낮은 사양의 EC2 인스턴스에서 발생하는 &#39;빌드 과부하&#39;</h3>
<p>지금 ec2서버에 빌드를 직접 실행하고 있다. 이렇게 되면 만약 실제 서비스를 운영하고 있는데 빌드까지 하게되면서 서버에 많은 부하가 가해진다. 이렇게 되면 빌드 순간에는 서버가 느려지면서 사용자들이 불편함을 겪을 수 있다.</p>
<h3 id="2-비효율적인-npm-install-및-환경-초기화">2. 비효율적인 npm install 및 환경 초기화</h3>
<p>수동 배포와 달리, GitHub Actions는 매번 새로운 비대화형 SSH 세션을 시작한다.</p>
<ul>
<li><code>npm install</code>의 비효율: 수동배포시에는 사실 시간이 오래걸리진 않았다. 이는 기존 <code>node_modules</code> 캐시를 활용해 빠르게 완료가 되었던 것이다. 하지만 자동화된 비대화형 환경에서는 캐시 활용이 완벽하지 않거나, 매번 패키지 무결성 검사에 시간을 더 소모할 수 있다.</li>
</ul>
<h3 id="3-긴-네트워크-왕복-시간">3. 긴 네트워크 왕복 시간</h3>
<p>GitHub Actions Runner는 전 세계 데이터 센터(주로 미국)에 위치하며, EC2 인스턴스는 사용자가 설정한 리전(서울)에 있다.
    - 명령 수행 지연: Runner에서 EC2로 npm install 명령을 전송하고, 명령이 실행되고, 그 결과가 다시 Runner로 전송될 때까지 <strong>네트워크 왕복 시간(Latency)</strong>이 계속해서 누적된다.</p>
<p>다음 글에서 <strong>빌드 결과물 배포</strong> 전략에 대해서 알아보자...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[FE가 낋여주는 배포 일지(1)_Ec2의 인스턴스로 HTTPS를 적용한 서비스를 운영해보자]]></title>
            <link>https://velog.io/@y-minion/SupabaseAppRouter%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EB%B0%B0%ED%8F%AC%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@y-minion/SupabaseAppRouter%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EB%B0%B0%ED%8F%AC%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sat, 11 Oct 2025 07:45:45 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가기에-앞서">들어가기에 앞서...</h2>
<p>해당 글은 AWS의 EC2를 통해 전체적인 배포 플로우에 대해 기록하기 위한 글로 프로젝트 코드는 Next.js with-supabase 템플릿을 사용한다.</p>
<h2 id="전체-플로우">전체 플로우</h2>
<p><strong>[인프라 설정 및 EC2 준비]</strong></p>
<ol>
<li>AWS EC2 인스턴스 생성</li>
<li>키 페어 생성 및 보관</li>
<li>보안 그룹 설정</li>
<li>탄력적 IP 할당</li>
</ol>
<p><strong>[서버 환경 구성]</strong></p>
<ol>
<li>서버 접속</li>
<li>패키지업데이트 및 Node.js 설치</li>
<li>Git설치 및 프로젝트 클론</li>
<li>환경변수 파일 직접 생성</li>
</ol>
<p><strong>[프로젝트 빌드 및 실행 (Node.js 서버 실행)]</strong></p>
<ol>
<li>종속성 설치</li>
<li>프로젝트 빌드</li>
<li>프로세스 관리자 설치(pm2)</li>
<li>프로젝트 실행</li>
<li>웹 서버 설정 -&gt; NGINX 사용</li>
<li>도메인 연결 및 HTTPS 설정</li>
</ol>
<hr>
<h2 id="앱라우터와-supabase-연결">[앱라우터와 Supabase 연결]</h2>
<p>수파베이스에서 프로젝트를 생성하면 아래와 같이 프로젝트 URL과 API키를 발급 받을 수 있다.
<img src="https://velog.velcdn.com/images/y-minion/post/0af5e705-5784-4582-9b1e-d4d32f70f003/image.png" alt=""></p>
<p>아래와 같이 .env.local에 넣어주자. 이때 반드시 .gitignore에 해당 파일은 제외하도록 작성해야한다.
<img src="https://velog.velcdn.com/images/y-minion/post/b3400058-c079-4118-a207-052a34351c5d/image.png" alt=""></p>
<p>이렇게 되면 프로젝트 코드의 준비는 끝났다.</p>
<h2 id="인프라-설정-및-ec2-준비">[인프라 설정 및 EC2 준비]</h2>
<h3 id="aws-ec2-인스턴스-생성--키-페어-생성-및-보관--보안-그룹-설정">AWS EC2 인스턴스 생성 + 키 페어 생성 및 보관 + 보안 그룹 설정</h3>
<p>이제 인스턴스를 만들자.
대부분 기본 선택사항은 건들지 말고 우리가 필요한 사항들만 설정해주자.</p>
<ul>
<li><p>인스턴스의 이름과 사용할 OS를 선택한다.
<img src="https://velog.velcdn.com/images/y-minion/post/145c5b2f-b0bc-4fb0-9c81-9a7b2fbe308f/image.png" alt=""></p>
</li>
<li><p>Key pair도 만들어주자.
<img src="https://velog.velcdn.com/images/y-minion/post/4fff034a-5c29-4a25-bd1f-0cd2407933d2/image.png" alt=""></p>
</li>
<li><p>보안그룹을 따로 만들어줘야한다.
  아웃바운드 규칙은 따로 만들지 않고 인바운드(-&gt;외부에서 해당 인스턴스로 접근할때 적용할 보안 규칙) 규칙만 만들면 된다.</p>
<ol>
<li><p>ssh로 접속해서 인스턴스를 조작해야 한다.</p>
</li>
<li><p>http접속을 통해 유저가 접근할 경우 NginX가 HTTPS로 리다이렉트 시켜줘야한다 + 80포트를 통해 Certbot도구를 활용해 인증서를 발급 받아야한다.</p>
</li>
<li><p>최종적으로 클라이언트는 https를 통해 해당 웹 사이트를 이용할 수 있어야한다.</p>
<p>다음과 같은 이유들로 아래와 같이 보안 그룹을 설정한다.
<img src="https://velog.velcdn.com/images/y-minion/post/cccdc147-9739-43d7-b71b-17ddac3ec0b9/image.png" alt=""></p>
</li>
</ol>
</li>
</ul>
<p>필요한 인스턴스 구성은 끝냈으니 이제 인스턴스를 생성한다.</p>
<h3 id="탄력적-ip-할당">탄력적 IP 할당</h3>
<p>아래 사진과 같이 새로운 인스턴스가 만들어졌다. 하지만 지금 인스턴스의 IP는 고정된 IP가 아니다. 즉 인스턴스가 만약 꺼졌다가 다시 실행되면 완전히 새로운 IP로 설정된다. 이렇게 되면 나중에 우리 페이지를 도메인에 등록할 텐데 IP가 바뀌면서 도메인에 등록한 IP와 현재의 인스턴스 IP가 일치하지 않는 문제가 발생한다. 
<img src="https://velog.velcdn.com/images/y-minion/post/98819631-6064-4435-924f-d81beb8dc34d/image.png" alt=""></p>
<p>그래서 탄력적 IP를 할당해야한다.
<img src="https://velog.velcdn.com/images/y-minion/post/fb1a35c4-60cd-47c9-9bc1-9cca8907d52d/image.png" alt=""></p>
<p>이제 발급 받은 이 IP를 우리의 인스턴스에 등록하자.
<img src="https://velog.velcdn.com/images/y-minion/post/9cb8ef29-c621-4489-a944-564398180ac2/image.png" alt=""></p>
<p>인스턴스 탭에서 우리가 만든 인스턴스를 선택하자.<img src="https://velog.velcdn.com/images/y-minion/post/075c1e63-b5eb-4f4e-9b73-88a7bb8b3d16/image.png" alt=""></p>
<p>다시 인스턴스 대시보드에서 우리가 만든 인스턴스의 IP를 보면 일라스틱IP가 제대로 할당됨을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/y-minion/post/d422fc77-3a6b-4f9b-83e9-064695203d0d/image.png" alt=""></p>
<p>이제 직접 인스턴스에 접속해서 우리의 프로젝트를 인스턴스에 넣어주자.</p>
<h2 id="서버-환경-구성">[서버 환경 구성]</h2>
<p>인스턴스에 접속한다.
<img src="https://velog.velcdn.com/images/y-minion/post/8a37b93d-3fc9-49cd-a6ca-176cd922eb58/image.png" alt=""></p>
<h3 id="node-설치">Node 설치</h3>
<p>먼저 node.js 환경에서 프로젝트를 실행시켜야해서 node.js를 설치해준다.
공식문서에 나와있는 방법대로 설치 해주면 된다.
<img src="https://velog.velcdn.com/images/y-minion/post/0fec9c09-c9f4-4a17-8a8a-1749ed471c73/image.png" alt="">
이때 nvm을 설치한뒤에 nvm 환경 변수를 현재 쉘에 적용해줘야한다.
<img src="https://velog.velcdn.com/images/y-minion/post/54c8c591-50a4-48b9-b55d-175f9c395652/image.png" alt=""></p>
<h3 id="git설치-및-프로젝트-클론">Git설치 및 프로젝트 클론</h3>
<p>Git 도 공식 문서에 나와있는 방법으로 설치한다.
<img src="https://velog.velcdn.com/images/y-minion/post/58f7a9cc-69e7-4a7b-bb95-e83f9b3be71b/image.png" alt="">
-&gt; 각 인스턴스의 운영체제에 맞는 방법으로 설치하면 된다.</p>
<p>이제 깃허브에서 실제로 배포할 프로젝트를 클론하자.
<img src="https://velog.velcdn.com/images/y-minion/post/575bf4b2-c617-4c1f-8768-9c38105a59a3/image.png" alt=""></p>
<h3 id="환경변수-파일-직접-생성">환경변수 파일 직접 생성</h3>
<p>클론한 디렉토리를 살펴보면 환견변수 파일이 빠져있음을 확인 할 수 있다. 이제 우리가 직접 수동으로 환경변수를 추가해 주면 된다.
<img src="https://velog.velcdn.com/images/y-minion/post/582642ff-9e92-48df-b1c2-898981d9415d/image.png" alt=""></p>
<pre><code class="language-bash"># 환경변수 파일 생성
vi .env.local</code></pre>
<pre><code class="language-bash">#-------vi 편집기 사용
NEXT_PUBLIC_SUPABASE_URL=슈퍼베이스 url 입력
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY=api키 입력</code></pre>
<p>이제 인스턴스의 프로젝트 코드 설정은 모두 끝났다.</p>
<h2 id="도메인-연결">도메인 연결</h2>
<p>기존의 도메인의 호스팅 영역에서 새로운 레코드를 생성한다. 이때 원하는 경로 이름을 설정하고, ec2의 IP를 설정해준다.</p>
<p><img src="https://velog.velcdn.com/images/y-minion/post/5be40b2b-a983-4024-8cce-0ee0223402c1/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/y-minion/post/2801a196-6f2f-4927-ad69-bb46ecc84095/image.png" alt=""></p>
<h2 id="프로젝트-빌드-및-실행-nodejs-서버-실행">[프로젝트 빌드 및 실행 (Node.js 서버 실행)]</h2>
<h3 id="종속성-설치">종속성 설치</h3>
<p>인스턴스에서 프로젝트를 실행시키기 위한 종속성을 설치해준다.</p>
<pre><code class="language-bash">npm i</code></pre>
<h3 id="프로젝트-빌드">프로젝트 빌드</h3>
<p>해당 앱라우터 프로젝트를 빌드해준다.</p>
<pre><code class="language-bash">npm run build</code></pre>
<p>성공적으로 빌드가 실행됨을 알 수 있다.
<img src="https://velog.velcdn.com/images/y-minion/post/fa241519-cfb6-4cb8-a7f2-756f27e74c37/image.png" alt=""></p>
<h3 id="프로세스-관리자-설치--프로젝트-실행">프로세스 관리자 설치 + 프로젝트 실행</h3>
<p>해당 프로젝트를 관리하는 도구를 이용해 서비스를 운영하게 될텐데 우리는 pm2를 이용해 해당 프로세스를 백그라운드에서 관리한다. 이렇게 되면 더욱 안정적으로 프로세스를 관리할 수 있다.</p>
<p>pm2공식문서를 가이드를 따라서 설치하자.</p>
<pre><code class="language-bash">npm install pm2 -g
</code></pre>
<p>이제 우리는 직접 <strong>npm start</strong> 와 같은 명령어를 직접 입력하는게 아니라 pm2 에 이런 명령어를 전달해서 pm2가 해당 프로세스를 관리하도록 해야한다.</p>
<pre><code class="language-bash">pm2 start npm  --name &quot;my-supabase-server&quot; -- start</code></pre>
<p> 위의 명령어의 뜻은 다음과 같다.</p>
<ul>
<li><p><em><strong>pm2 start npm</strong></em>: pm2이 실행할 주 프로그램은 npm이다.</p>
</li>
<li><p><strong><em>--name &quot;my-supabase-server&quot;</em></strong> : 실행하는 프로세스의 이름을 전달한다.</p>
</li>
<li><p><strong><em>-- start</em></strong> : npm 프로그램에 전달할 인자는 start이다.</p>
<p><img src="https://velog.velcdn.com/images/y-minion/post/73615900-5842-4980-bf89-fa82e7fa2ff3/image.png" alt="">
성공적으로 실행이 되면 status에 online으로 적혀 있다.</p>
</li>
</ul>
<h3 id="웹-서버-설정---nginx-사용">웹 서버 설정 -&gt; NGINX 사용</h3>
<p>이제 우리가 할 작업은 유저가 직접 우리의 넥스트
서버에 접근하도록 하지 않고 항상 NginX 웹서버를 통해서 접근하도록 해야한다.</p>
<p><img src="https://velog.velcdn.com/images/y-minion/post/94e71193-9c2a-4a92-86f3-4537775ed449/image.png" alt=""></p>
<p>nginx를 먼저 설치한다.</p>
<pre><code class="language-bash">$ sudo apt update
$ sudo apt install nginx</code></pre>
<p>이때 nginx의 설정파일을 조작해 리버스 프록시를 설정해야한다. 설정파일은 최상위 경로인 루트 디렉토리의 /etc에 존재한다.
우리는 다음과 같은 목적을 수행하기 위해 Nginx의 설정파일을 수정해야한다.</p>
<ul>
<li>외부에서 도메인의 80포트(http)로 접근시 Nginx는 이 요청을 내부의 3000번 포트로 연결해줘야한다.</li>
<li>인증서 발급 목적: 우리는 Certbot을 통해 인증서 발급을 하게 된다. 이때 <a href="http://example_domain.com">http://example_domain.com</a> 으로 접속해 인증 절차를 하게되는데, 이 nginx설정이 있어야만 certbot의 확인 요청이 우리의 서버에 도달할 수 있다. (80포트의 요청이 내부의 3000번 포트로 이어지지 않으면 인증 작업이 성공 할 수 없다.)</li>
</ul>
<p>그래서 다음과 같이 nginx의 설정파일을 열고 아래와 같이 내용을 추가한다.</p>
<pre><code class="language-bash">sudo vi /etc/nginx/sites-available/default</code></pre>
<p><img src="https://velog.velcdn.com/images/y-minion/post/13d774f7-7fe3-4e8b-b131-84387b36a735/image.png" alt=""></p>
<ul>
<li><em><strong>listen 80</strong></em> : 80포트(HTTP)로 들어오는 요청을 받는다.</li>
<li><em><strong>server_name supabase.y-minion.link</strong></em> : supabase.y-minion.link주소로 들어온 요청만 처리한다.</li>
<li><em><strong>proxy_pass <a href="http://127.0.0.1:3000">http://127.0.0.1:3000</a></strong></em> : 들어온 요청을 내부에서 실행중인 앱으로 전달한다.</li>
</ul>
<p>설정을 완료하면 설정 파일 문법 검사를 한뒤 Nginx를 재시작 한다.</p>
<pre><code class="language-bash"># 설정 파일 문법 검사
sudo nginx -t

# Nginx 재시작
sudo systemctl restart nginx</code></pre>
<p>이제 설정한 우리 인스턴스에 등록한 도메인인 <a href="http://supabase.y-minion.link">http://supabase.y-minion.link</a> 에 접속해보자.</p>
<p>리버스 프록시를 설정하기 전에는 아래와 같이 Nginx의 초기 화면이 나왔지만,
<img src="https://velog.velcdn.com/images/y-minion/post/a87f8cca-5d27-4d32-9404-0f314dc0afc0/image.png" alt=""></p>
<p>설정을 완료하고 Nginx를 재시작하면 아래와 같이 올바르게 리버스 프록시가 설정되어 인스턴스 내부의 우리 웹 프로젝트로 연결해준다.
<img src="https://velog.velcdn.com/images/y-minion/post/a36bc918-042a-4f1a-b28f-603f85f9fd71/image.png" alt=""></p>
<h3 id="certbot-실행-및-자동-설정">Certbot 실행 및 자동 설정</h3>
<p>우선 들어가기에 앞서 <a href="https://certbot.eff.org/">Certbot 공식 레퍼런스</a>에 들어가서 현재 상황에 맞게 항목들을 선택하면 어떻게 해야 인증서를 발급받을 수 있는지 친절하게 설명해준다.
<img src="https://velog.velcdn.com/images/y-minion/post/d0669028-ef2d-4be6-baf2-1a5bbc37ed57/image.png" alt=""></p>
<h4 id="certbot-설치">Certbot 설치</h4>
<p>우리는 Nginx의 설정파일을 수정해 80포트로 접근하면 내부에서 실행중인 어플리케이션으로 접근하도록 조작을 완료 했다.
Certbot을 사용해 Let&#39;s Encrypt 서버에 해당 도메인으로 인증서를 발급받아야한다. <em><strong>이때 앞에서 설정한 80포트를 통해 인증절차가 실행된다.</strong></em></p>
<p>다음과 같은 절차를 실행하여 Certbot을 설치한다.</p>
<pre><code class="language-bash"># snapd 설치 및 업데이트
sudo snap install core; sudo snap refresh core

# 기존 certbot-auto 또는 OS 패키지 삭제 (설치한 적이 있다면)
sudo apt-get remove certbot

# Certbot 설치
sudo snap install --classic certbot

# Certbot 명령어 링크 생성
sudo ln -s /snap/bin/certbot /usr/bin/certbot</code></pre>
<h4 id="certbot-실행-및-인증서-발급">Certbot 실행 및 인증서 발급</h4>
<p>아래 명령어를 실행하면 Certbot이 앞에서 우리가 설정한 Nginx 설정 파일을 분석하여 server_name에 명시된 도메인에 대한 인증서 발급을 시도하고, 성공하면 Nginx 설정에 HTTPS 관련 내용을 자동으로 추가해 준다.</p>
<pre><code class="language-bash">sudo certbot --nginx</code></pre>
<p>위 명령어를 입력하면 다음과 같은 질문이 등장한다.
<img src="https://velog.velcdn.com/images/y-minion/post/29cc5673-bff1-492d-bcf2-89846aa617b1/image.png" alt="">
 읽어보고 알맞게 대답을 하면 된다.</p>
<p> 위의 과정을 모두 완료하면 웹 서비스의 도메인에 접속시 HTTPS 연결이 활성화된다.
 <img src="https://velog.velcdn.com/images/y-minion/post/698415b4-e7c8-4cc3-83be-cf1936b8a93c/image.png" alt="">
 성공적으로 https로 접근이 완료된 것을 확인 할 수 있다.</p>
<h3 id="http---https-리다이렉트-설정">HTTP -&gt; HTTPS 리다이렉트 설정</h3>
<p> 만약 사용자가 http로 접근하는 경우가 발생할 수 있다. 이때 우리는 Nginx가 해당 유저의 요청을 https로 리다이렉트 하도록 설정해 줘야한다.(보안 이슈 목적)</p>
<p>그래서 nginx의 설정파일에서 80포트(HTTP)로 요청이 오면 HTTPS로 리다이렉트 한다는 옵션을 추가해주면 된다.</p>
<pre><code class="language-bash"># Nginx의 사이트 설정파일을 열자.
sudo vi /etc/nginx/sites-available/default</code></pre>
<p>하지만 이미 Certbot이 올바르게 http의 요청을 https로 리다이렉트 하도록 설정했음을 확인 할 수 있다.
<img src="https://velog.velcdn.com/images/y-minion/post/259d374a-4ef4-4fe8-a79c-da1e64fd6a73/image.png" alt=""></p>
<p>이렇게 모든 과정을 수행하고 <a href="https://your_domain.com%EC%9C%BC%EB%A1%9C">https://your_domain.com으로</a> 접속하면 자물쇠가 걸린 안전한 사이트가 완성됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Supabase]이메일 인증 회원가입 진행시 인증은 성공했으나, 루트 레이아웃에서 세션을 불러오지 못하는 버그]]></title>
            <link>https://velog.io/@y-minion/Supabase%EC%9D%B4%EB%A9%94%EC%9D%BC-%EC%9D%B8%EC%A6%9D-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EC%A7%84%ED%96%89%EC%8B%9C-%EC%9D%B8%EC%A6%9D%EC%9D%80-%EC%84%B1%EA%B3%B5%ED%96%88%EC%9C%BC%EB%82%98-%EB%A3%A8%ED%8A%B8-%EB%A0%88%EC%9D%B4%EC%95%84%EC%9B%83%EC%97%90%EC%84%9C-%EC%84%B8%EC%85%98%EC%9D%84-%EB%B6%88%EB%9F%AC%EC%98%A4%EC%A7%80-%EB%AA%BB%ED%95%98%EB%8A%94-%EB%B2%84%EA%B7%B8</link>
            <guid>https://velog.io/@y-minion/Supabase%EC%9D%B4%EB%A9%94%EC%9D%BC-%EC%9D%B8%EC%A6%9D-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EC%A7%84%ED%96%89%EC%8B%9C-%EC%9D%B8%EC%A6%9D%EC%9D%80-%EC%84%B1%EA%B3%B5%ED%96%88%EC%9C%BC%EB%82%98-%EB%A3%A8%ED%8A%B8-%EB%A0%88%EC%9D%B4%EC%95%84%EC%9B%83%EC%97%90%EC%84%9C-%EC%84%B8%EC%85%98%EC%9D%84-%EB%B6%88%EB%9F%AC%EC%98%A4%EC%A7%80-%EB%AA%BB%ED%95%98%EB%8A%94-%EB%B2%84%EA%B7%B8</guid>
            <pubDate>Sun, 05 Oct 2025 07:48:59 GMT</pubDate>
            <description><![CDATA[<h2 id="현재-문제-상황">현재 문제 상황</h2>
<p>회원가입 관련 페이지에서 회원가입 진행시 이메일전송이 되고, 사용자가 이메일 인증을 클릭시 supabase에서 승인까지는 정상적으로 동작을 한다.</p>
<p><img src="https://velog.velcdn.com/images/y-minion/post/fa77e69c-a4cc-4afe-9ffb-318756c87eab/image.png" alt=""></p>
<p>다음은 supabase에서 가입을 시도한 회원 json 데이터.
<img src="https://velog.velcdn.com/images/y-minion/post/4fbf38d0-d0dd-4807-aa58-90c1210ce006/image.png" alt=""></p>
<blockquote>
<p>❗️계속해서 supabase에서 회원생성까지 잘 되지만, 어플리케이션에서 세션의 값이 null로 오는 문제가 발생한다.</p>
</blockquote>
<h2 id="🔎-문제지점-파악하기">🔎 문제지점 파악하기</h2>
<p>일단 인지한 문제사항은 루트 레이아웃에서 session을 사용하려고 해도 계속해서 null이 반환된다는 사실. 여기서 정확하게 어느 부분에서 문제가 발생되는지 찾아야 한다.</p>
<h3 id="문제-지점-좁히기">문제 지점 좁히기</h3>
<ul>
<li>사용자가 회원가입 버튼 클릭시 이메일이 잘 전송되는가? -&gt; OK</li>
<li>사용자가 이메일의 인증버튼을 클릭하면 내가 원하는 주소로 리다이렉트가 잘 되는가? -&gt; OK<blockquote>
<p>위의 질의사항이 문제없이 동작하므로 SignUp 페이지는 문제가 없다.</p>
</blockquote>
</li>
</ul>
<p>그 이후의 회원가입의 플로우는 다음과 같다.</p>
<ul>
<li>리다이렉트된 signup/comfirm의 GET요청에서 route.ts로직이 정상적으로 작동을 하는가?
  -&gt; ❗️정상적으로 동작하는지 확신할 수 없다!</li>
</ul>
<h3 id="signupcomfirm-의-routets를-추적한다">signup/comfirm 의 route.ts를 추적한다.</h3>
<p>route.ts의 로직이 모두 정상적으로 동작하는지 확실할 수 없기에, 해당 로직을 디버깅을 했다.
각 플로우에 로그를 출력하도록 해 각 단계별로 살펴보자.</p>
<pre><code class="language-typescript">export async function GET(request: Request) {
  console.log(&quot;\n--- [1] /signup/confirm 라우트 핸들러 시작 ---&quot;);

  //리퀘스트를 url 객체로 만들어 준다.
  const requestUrl = new URL(request.url);

  //쿼리 파람 추출
  const code = requestUrl.searchParams.get(&quot;code&quot;);

  if (!code) {
    console.log(&quot;--- [!] URL에 code 파라미터가 없습니다. ---&quot;);
    return null;
  }

  console.log(
    `--- [2] URL에서 인증 code 추출 완료: ${code.substring(0, 10)}... ---`
  );

  //쿼리파람에 code가 존재하면 수파베이스의 세션에 코드를 등록한다.
  if (code) {
    const supabase = await createServerSupabaseClient();

    try {
      console.log(&quot;--- [3] exchangeCodeForSession 호출 시도 ---&quot;);

      await supabase.auth.exchangeCodeForSession(code);
      console.log(&quot;--- [4] ✅ exchangeCodeForSession 성공! (에러 없음) ---&quot;);
    } catch (error) {
      console.error(
        &quot;--- [!] ❌ exchangeCodeForSession 에서 에러 발생! ---&quot;,
        error
      );

      return null;
    }
  }

  // --- 👇 여기가 가장 중요한 검증 단계 ---
  try {
    // Supabase 클라이언트를 &quot;새로&quot; 만들어서 현재 쿠키 저장소의 상태를 다시 읽어옵니다.
    const newSupabase = await createServerSupabaseClient();
    const {
      data: { session },
    } = await newSupabase.auth.getSession();

    if (session) {
      console.log(
        &quot;--- [5] ✅✅✅ 세션 쿠키가 서버 측 저장소에 성공적으로 설정되었습니다! ---&quot;
      );
      console.log(&quot;       -&gt; 사용자 ID:&quot;, session.user.id);
    } else {
      console.log(
        &quot;--- [5] ❌❌❌ E_COOKIE_NOT_SET: exchangeCode 이후에도 서버 측 세션이 null입니다. ---&quot;
      );
      console.log(
        &quot;       -&gt; 이 로그가 보인다면, 미들웨어 설정 문제일 확률이 100%입니다.&quot;
      );
    }
  } catch (e) {
    console.log(&quot;--- [!] 세션 확인 중 예외 발생 ---&quot;, e);
  }

  console.log(
    `--- [6] 최종적으로 메인 페이지(${requestUrl.origin})로 리디렉션합니다. ---`
  );

  //위의 모든 로직이 성공적으로 마무리 되면 url객체의 origin(메인 페이지)로 리다이렉트 시킨다.
  return NextResponse.redirect(requestUrl.origin);
}</code></pre>
<p>위의 결과는 다음과 같다.</p>
<pre><code class="language-bash">--- [1] /signup/confirm 라우트 핸들러 시작 ---
--- [2] URL에서 인증 code 추출 완료: c66a841b-0... ---
--- [3] exchangeCodeForSession 호출 시도 ---
(node:91681) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
--- [4] ✅ exchangeCodeForSession 성공! (에러 없음) ---
--- [5] ❌❌❌ E_COOKIE_NOT_SET: exchangeCode 이후에도 서버 측 세션이 null입니다. ---
       -&gt; 이 로그가 보인다면, 미들웨어 설정 문제일 확률이 100%입니다.
--- [6] 최종적으로 메인 페이지(http://localhost:3000)로 리디렉션합니다. ---
 GET /signup/confirm?code=c66a841b-0eed-4782-bdaf-8d6ea7ade36d 307 in 510ms</code></pre>
<h3 id="원인-발견">원인 발견</h3>
<p>사용자의 메일인증링크 클릭으로 전달된 code를 통해 슈퍼베이스에서 세션을 발급받는 로직인 exchangeCodeForSession까지는 정상적으로 작동하는 것을 확인할 수 있다.
하지만 그 이후에 요청 헤더의 쿠키에서 세션을 조회하는 로직에서 버그가 발생하고 있다.
즉, 우리는 세션을 쿠키에 넣고, <strong>통신할때마다 헤더의 쿠키에서 세션을 꺼내써야</strong> 하지만 현재 헤더의 쿠키에는 사용할 수 있는 세션이 없다.
분명 세션을 발급받았는데... 왜 세션을 꺼내 쓸 수 없을까..?
내가 잘못 생각하고 있는 부분이 있었다.</p>
<blockquote>
<p> ⚠️ 발급 받은 세션이 요청의 메인 페이지로 리다이렉트시 헤더의 쿠키에 포함되지 않고있다.</p>
</blockquote>
<p>기존에 exchangeCodeForSession을 통해 슈퍼베이스에서 세션을 발급 받으면 자동으로 헤더의 쿠키에 세션이 저장된다고 생각했다.
지금 상태로는 세션을 발급 받아도 메모리 상에만 존재하고 내부적으로 헤더의 쿠키에 기록을 하긴 한다. <em><strong>하지만 route.ts의 마지막에 메인페이지로 리다이렉트 시킬때 새로운 응답 객체가 반환되면서 우리가 사용해야할 세션은 어디에도 기록되지 않고 사라져 버린다.</strong></em> 그래서 route.ts에서 메인페이지로 리다이렉트시킨 후  메인페이지에서 getSession()을 하면 null인 값이 반환되는 것이였다.</p>
<h2 id="🛠️-해결방법">🛠️ 해결방법</h2>
<h3 id="middleware-도입">middleware 도입</h3>
<p>요청을 가로채서 하나의 실행 컨텍스트에서 동일한 요청 객체를 사용하는 기능을 하는 미들웨어를 도입한다.</p>
<p>기존에는 쿠키에 세션을 저장해도, 메인페이지로 리다이렉트 시킬때 새로운 응답 객체를 만들어서 세션이 전달되지 않는 문제가 있었다.
하지만 미들웨어를 통해서 route.ts에 도달하기 전에 먼저 하나의 실행 컨텍스트에서 응답 객체를 만들어 놓으면, route.ts에서 세션을 저장할때 미리 만들어 놓은 응답 객체의 쿠키에 저장을 한다. 그리고 메인 페이지로 리다이렉트를 시킬때 해당 응답 객체를 전달하면서 메인페이지에서도 route.ts에서 만들어진 세션을 사용할 수 있게 된다.</p>
<p><strong>middleware.ts</strong></p>
<pre><code class="language-typescript">export async function middleware(request: NextRequest) {
  let response = NextResponse.next({
    request: {
      headers: request.headers,
    },
  });

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return request.cookies.get(name)?.value;
        },
        set(name: string, value: string, options: CookieOptions) {
          request.cookies.set({ name, value, ...options });
          response = NextResponse.next({
            request: { headers: request.headers },
          });
          response.cookies.set({ name, value, ...options });
        },
        remove(name: string, options: CookieOptions) {
          request.cookies.set({ name, value: &quot;&quot;, ...options });
          response = NextResponse.next({
            request: { headers: request.headers },
          });
          response.cookies.set({ name, value: &quot;&quot;, ...options });
        },
      },
    }
  );

  // 이 미들웨어의 핵심: 모든 요청에 대해 사용자의 세션을 갱신한다.
  await supabase.auth.getSession();

  return response;
}
</code></pre>
<p>이렇게 되면 미들웨어 덕분에 회원가입 직후에 바로 로그인으로 이어지는 로직도 구현이 가능해진다.
그리고 <strong>await supabase.auth.getSession();</strong> 덕분에 모든 페이지 요청마다 세션의 유효기간을 검증하고, 유효기간이 지나도 새로운 세션을 갱신하기 때문에 사용자는 세션의 유효기간이 지나도 로그인을 하지 않아도 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[도커파일 작성해서 이미지를 빌드해보자]]></title>
            <link>https://velog.io/@y-minion/%EB%8F%84%EC%BB%A4%ED%8C%8C%EC%9D%BC-%EC%9E%91%EC%84%B1%ED%95%B4%EC%84%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A5%BC-%EB%B9%8C%EB%93%9C%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@y-minion/%EB%8F%84%EC%BB%A4%ED%8C%8C%EC%9D%BC-%EC%9E%91%EC%84%B1%ED%95%B4%EC%84%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A5%BC-%EB%B9%8C%EB%93%9C%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Fri, 12 Sep 2025 09:07:49 GMT</pubDate>
            <description><![CDATA[<h2 id="요구사항">요구사항</h2>
<ol>
<li>node:18 이미지를 사용한다.</li>
<li>작업 디렉토리를 /test으로 설정한다.</li>
<li>npm install express명령으로 의존성을 패치한다.</li>
<li>3000번 포트를 사용다는 것을 명시한다.</li>
<li>[“node”, “app.js”]를 컨테이너 ENTRYPOINT로 등록한다.</li>
<li>실행하여 잘 동작하는지 확인한다.</li>
<li>마지막으로 docker hub에 업로드를 수행한다.</li>
</ol>
<p>-&gt; 위의 요구사항을 만족하는 이미지를 VM에서 빌드하여 도커 허브에 업로드 해보자.</p>
<h2 id="작업-내역--설명">작업 내역 &amp; 설명</h2>
<h3 id="준비물">준비물</h3>
<p>최종적으로 빌드된 이미지를 통해 컨테이너가 실행되면 돌아갈 익스프레스 서버를 돌려야 하므로 간단한 Express코드를 작성해주자.</p>
<pre><code class="language-javascript">const express = require(&#39;express&#39;);
const app = express();
const port = 3000;

app.get(&#39;/&#39;, (req, res) =&gt; {
  res.send(&#39;Hello, Docker World!&#39;);
});

app.listen(port, () =&gt; {
  console.log(`App listening at http://localhost:${port}`);
});</code></pre>
<h3 id="👀-전체-플로우-미리보기">👀 전체 플로우 미리보기</h3>
<p>이미지를 빌드하기까지의 과정을 간단하게 살펴보자</p>
<h4 id="1-작업-디렉토리-만들기">1. 작업 디렉토리 만들기</h4>
<p>제일 먼저 이미지를 빌드하기 위해서 이미지 안에 돌리고? 싶은 코드도 있어야 하고, dockerfile도 있어야한다. 이 모든걸 갖고 있는 작업 디렉토리를 만들어 줘야 한다.</p>
<h4 id="2-애플리케이션-코드-생성">2. 애플리케이션 코드 생성</h4>
<p>이미지가 실행될때 실제로 돌아갈 애플리케이션 코드를 작업 디렉토리 안에 생성한다.</p>
<h4 id="3-도커파일-생성">3. 도커파일 생성</h4>
<p>작업 디렉토리 안에서 도커파일을 만들어줘야한다. </p>
<h5 id="도커파일이란">도커파일이란?</h5>
<pre><code>- 이미지가 빌드될때 지켜야할 작업 지침서와 같은 것임. 우리가 이미지로 만들고 싶은 디렉토리를 빌드할때 지켜야하는 옵션들을 해당 도커파일에 작성하면 된다.</code></pre><h4 id="4-해당-디렉토리-빌드">4. 해당 디렉토리 빌드</h4>
<p>이미지로 만들고 싶은 디렉토리의 구성이 끝났으면 이제 빌드하자.</p>
<h4 id="5-빌드된-이미지-도커-허브에-push">5. 빌드된 이미지 도커 허브에 Push</h4>
<p>만들어진 이미지를 다른 사람들과 나누고 싶지 않은가? -&gt; 도커 허브에 올려라. 허접한 이미지를 올려도 이상하게 누군가가 계속 Pull한다.... 그러니까 중요한 이미지를 팀원들과 공유하고 싶으면 도커허브보다는 하버가 좋은 선택지일듯 하다.</p>
<h3 id="실습">실습</h3>
<h4 id="작업-디렉토리-생성">작업 디렉토리 생성</h4>
<p>VM의 루트 디렉토리에 test 라는 디렉토리를 생성한다.</p>
<pre><code class="language-bash">cd ~
mkdir test
cd test</code></pre>
<h4 id="어플리케이션app--dockerfile-생성">어플리케이션(app) &amp; dockerfile 생성</h4>
<p>익스프레스 서버 코드를 만들어준다.</p>
<p>vim을 통해 app.js코드를 생성했다.</p>
<pre><code class="language-bash">vim app.js</code></pre>
<p>아래 코드를 vim편집기에 입력한다.</p>
<pre><code class="language-javascript">const express = require(&#39;express&#39;);
const app = express();
const port = 3000;

app.get(&#39;/&#39;, (req, res) =&gt; {
  res.send(&#39;Hello, Docker World!&#39;);
});

app.listen(port, () =&gt; {
  console.log(`App listening at http://localhost:${port}`);
});</code></pre>
<p>이제 도커파일을 만들어준다.</p>
<pre><code class="language-bash">vim dockerfile</code></pre>
<pre><code class="language-dockerfile"># 1. node:18 이미지를 사용한다.
FROM node:18
# 2. 작업 디렉토리를 /test으로 설정한다. -&gt; 생성될 이미지 내부의 작업 디렉토리 이름을 명시하는 것임.
WORKDIR /test
# 3. npm install express명령으로 의존성을 패치한다.
RUN npm install express
# 4. 로컬 컴퓨터의 작업 디렉토리에 있는 app.js를 생성될 이미지의 작업 디렉토리 /test에 app.js라고 붙여넣는다.
COPY app.js app.js
#5. 3000번 포트를 사용한다는 것을 명시.
EXPOSE 3000
#6. 이미지가 실행될때 자동으로 실행될 명령어들을 입력한다.
ENTRYPOINT [&quot;node&quot;,&quot;app.js&quot;]
</code></pre>
<p>위의 작업들을 끝내면 test디렉토리에 다음과 같이 2개의 파일이 만들어진다.</p>
<pre><code class="language-bash">vagrant@vagrant:~/test$ ls
app.js  dockerfile</code></pre>
<h4 id="빌드-실행">빌드 실행</h4>
<p>이제 빌드하는데 필요한 것들은 모두 준비가 끝났다. 빌드를 해보자.</p>
<pre><code class="language-docker">docker build -t b00gyman/my-nodejs-express-app . -f dockerfile</code></pre>
<ul>
<li><p><code>-t</code> : 실제 도커에 올라갈 이미지의 이름이다. 이때 원격(도커 허브)에 저장하려고 한다면 꼭 [저장소명]/이미지이름:[태그] 이렇게 작성해야한다. 하지만 로컬이나 그룹이 프라이밋 레포지를 갖고있다면 해당하는 경로로 작성하면 된다.</p>
</li>
<li><p>. : 현재 경로에 있는 파일들을 이미지로 만들겠다는 의미임.</p>
</li>
<li><p>-f : 빌드할때 사용되는 도커파일을 지정하는 옵션이다.</p>
</li>
</ul>
<h4 id="도커허브에-푸시">도커허브에 푸시</h4>
<p>이미지로 만들어졌다면 이제 도커 허브에 push할 차례다. 다음과 같이 명령어를 입력하면 도커허브에 원격으로 올라간다.</p>
<pre><code class="language-bash">docker push b00gyman/my-nodejs-express-app</code></pre>
<p>-&gt; 위의 과정을 모두 마치면 도커 허브에 이미지가 성공적으로 올라간다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프레임워크(myReact)재조정 설계]]></title>
            <link>https://velog.io/@y-minion/%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%ACmyReact%EC%9E%AC%EC%A1%B0%EC%A0%95-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@y-minion/%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%ACmyReact%EC%9E%AC%EC%A1%B0%EC%A0%95-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Fri, 27 Jun 2025 02:48:25 GMT</pubDate>
            <description><![CDATA[<p>diffTrees 함수의 알고리즘 생각하다보니… 일단 새롭게 리렌더링 될때 만들어지는 VDOM을 생성해 놓고, 그 다음에 비교를 해야 하는데…?</p>
<h2 id="전체-흐름-구상">전체 흐름 구상</h2>
<ul>
<li><p>diffTrees를 통해 달라진 점만 찾아서 노드를 교체할 계획인데 그럴려면 실제 리렌더링시에 새롭게 만들어지는 가상 돔이 필요하다.</p>
</li>
<li><p>꼭 지켜야할 규칙! → 기존의 rootDOM을 기준으로 ‘하나의 노드’ 씩 그려나가야한다.
<img src="https://velog.velcdn.com/images/y-minion/post/58dd884b-ba56-4813-ba4e-3ee28cfde2ce/image.png" alt=""></p>
</li>
<li><p>위가 rootDOM이라고 하면 새롭게 만들어지는 가상 DOM은 이 돔을 기반으로 그려져야 한다.</p>
</li>
<li><p>만약 여기서 3에서 리렌더링이 발생하면 1부터 다시 그려질게 아니라 3의 노드 부터 그려져야한다.</p>
</li>
<li><p>그러면 어떻게 해야할까?</p>
</li>
<li><p>우선은 rootDOM으로 부터 리렌더링이 발생한 VNode를 리렌더링 함수에 입력을 한다.</p>
<ul>
<li>이렇게 되면 업데이트된 최신의 상태값은 유지가 된다.</li>
</ul>
</li>
<li><p><strong>하지만 하위(=4,5 번 노드)의 노드들은 새롭게 노드가 그려지므로 기존의 상태(=hookMetaData필드)가 유지되지 않은채 완전히 새로운 노드가 만들어져 버린다.</strong></p>
</li>
</ul>
<p>최선의 방법은 reRender 가 실행되는 순간에 각각의 Node에 접근해서 생성을 하는데 각 Node에 진입하는 순간에!!! (<strong>type 이 함수일 경우 함수를 호출 하기 전에!!</strong>)</p>
<ul>
<li>기존의 rootVNode와 비교를 해주는 함수를 사용해 비교를 한뒤</li>
<li>동기화 작업이 필요하면 동기화를 해주는 <strong>synchronizeNode함수를 사용해 동기화를 시켜준다.</strong></li>
<li>그러면 유지된 최신의 상태 필드를 얻는다.</li>
<li>업데이트된 Node를 바탕으로 함수 컴포넌트를 호출한다.</li>
<li>위의 과정을 재귀로 반복한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/y-minion/post/e7e1b8f4-f0e2-4c44-82e9-c0cb6ac02f5e/image.png" alt=""></p>
<ul>
<li>위의 흐름을 잘 살펴보면 지금 상황에서 필요한 함수가 두개 보인다.<ol>
<li>OldVNode와 NewVNode를 비교하는함수 (비교만 한다!!_역할 분리 집중)</li>
<li>데이터의 동기화가 필요한 상황에서(즉 1번 함수에서 동일하다고 판단이 된 상황) 노드의 데이터를 동기화 해주는 함수.</li>
</ol>
</li>
</ul>
<p>위의 함수를 만들고 흐름대로 구현을 완료했다면 이제는 올바르게 ‘가상 돔’을 만들 수 있다. </p>
<ul>
<li>다음 단계로는 만들어진 VDOM과 실제 rootDOM을 비교해 실제로 바뀐 부분의 노드만 변경합니다.(DOM변경 최소화)</li>
</ul>
<p>위의 작업의 전체 흐름은 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/y-minion/post/a953fad2-91fd-4ed0-bb67-e63512084981/image.png" alt=""></p>
<p>우선은 첫번째 작업부터 진행 해보겠다.</p>
<h2 id="디테일-설계-아이디어">디테일 설계 아이디어</h2>
<h3 id="synchronizetree-함수"><strong>synchronizeTree 함수</strong></h3>
<p>특정 노드에서 setter 함수 호출시 발생하는 함수다. 각 노드를 DFS로 순환하면서 동기화 작업을 실행한다. 최종적으로 실제로 화면에 그려져야할 DOM을 반환한다. </p>
<ul>
<li><p>매개변수 → setter 함수가 발생한 컴포넌트 함수를 재 호출된 결과값 : 최신 VNode</p>
<ul>
<li>type의 typeof === function 일 경우해당 type 재 호출(매개변수는 최신화된 VNode의 prop)</li>
<li>→ 기존의 VNode를 넣으면 안된다.. 왜? → 상태 관련 필드는 최신화 상태지만, 만약 해당 컴포넌트가 조건부 렌더링 로직이 있다면? 기존의 VNode의 children필드에는 해당 노드가 포함되어 있지 않다. 그래서 컴포넌트를 일단 실행 시켜야 한다…</li>
</ul>
</li>
<li><p>매개변수를 넣었으면 기존의 innternalRender 함수와 동일하게 DFS 순회를 진행한다.</p>
</li>
<li><p>이때 각 노드에 접근 할때마다 rootDOM트리도 동시에 순환한다. 같이 순환하면서 노드단위로 비교작업을 한다. → // TODO DFS 순회 함수 구현</p>
</li>
<li><p>각 노드 단위로 비교를 진행한다. 비교 기준은 다음과 같다.</p>
</li>
</ul>
<p>비교는 DFS 의 후위 순회한다는 전제로 진행한다…</p>
<ul>
<li><strong>비교조건</strong><ol>
<li>호스트 엘리먼트일 경우 비교 중단. 새로 만들어진 Node를 사용한다.</li>
<li>함수형 컴포넌트일 경우 두개의 Node(root트리의 Node 와 새롭게 만들어지는 Node)를 비교한다.<ol>
<li>컴포넌트 이름을 기준으로  비교합니다.</li>
</ol>
<ul>
<li>이름이 같으면 동기화 작업을 진행합니다.</li>
<li>이름이 다르면 새로운 컴포넌트로 인식하고 동기화 작업을 진행하지 않고 건너 뛴다.</li>
</ul>
</li>
</ol>
</li>
</ul>
<p>위의 작업을 각 Node에 집입할때 진행하고 다음 Node로 이동한다.</p>
<ul>
<li><p><strong>동기화 작업</strong></p>
<ul>
<li>rootDOM과 새롭게 만들어지는 VDOM을 Node단위로 비교한다.</li>
<li>위의 비교 조건에서 동기화 작업이 필요하다고 판단되면 동기화를 한다.</li>
<li>동기화 작업은 목적은 해당 컴포넌트가  최신 hookMetaData필드를 갖고  호출되는 것.<ul>
<li>이래야 해당 컴포넌트 안의 useState함수가 다시 초깃값을 갖고 호출되지 않고, 기존의 업데이트된 상태값을 기준으로 호출된다.</li>
</ul>
</li>
</ul>
<ol>
<li>기존의 rootDOM의 노드에서 hookMetaData를 복사해 새롭게 만들어질 Node에 주입한다.</li>
</ol>
</li>
<li><p>이러면 동기화 작업은 완료된다.</p>
</li>
<li><p>위의 작업이 모두 완료되면 업데이트된 상태값을 갖고있는 VDOM이 만들어진다.</p>
</li>
<li><p>이 VDOM을 rootDOM으로 변경한다.</p>
</li>
</ul>
<h2 id="수정사항">수정사항</h2>
<ul>
<li>근데 이렇게 트리를 순회하려면 트리가 있어야 하는데 지금 root에는 트리가 들어있는 상태가 아님…</li>
</ul>
<h2 id="설계-진입">설계 진입</h2>
<p>우선 기존의 rootDOM의 rootVnode에서 리렌더링이 발생한다.</p>
<p>이때는 기존의 hookMetaData필드가 VNode안에 있으므로 최신 상태를 갖고 리렌더링이 된다.</p>
<p>하지만 문제는 새롭게 리렌더링을 할때 생기는 하위 노드에서 발생한다. 하위 노드가 새롭게 생기면서 기존의 노드의 hookMetaData를 유지하지 않고 초깃값으로 생겨난다.
<img src="https://velog.velcdn.com/images/y-minion/post/85ed6ecf-7383-41b8-a431-fc698fe45686/image.png" alt=""></p>
<p>→ 그래서 본질적으로는 기존 rootNode와 새롭게 리렌더링되는 newNode와 비교해서 hookMetaData를 넣어줘야한다.</p>
<h3 id="본질적-문제-발견">본질적 문제 발견</h3>
<ul>
<li>하지만 위의 설계 처럼 트리 순환을 하려면 Node객체로 이루어진 tree구조가 있어야한다.(기준점 필요) 하지만 지금은 노드를 순환하면서 바로 DOM으로 변환해 버려서 남아있지를 않다. 그렇게되면..? → 비교를 할 수 있는 대상이 없다. <em><strong>즉 위 사진의 빨간 tree구조가 없다.</strong></em></li>
</ul>
<h3 id="node객체-tree-생성__rendercomponenttree">Node객체 tree 생성.__renderComponentTree</h3>
<p>결국 객체로 이루어진 전체 트리구조가 필요하다. 그래야 빠르게 비교를 해 바뀐 부분을 찾을 수 있다. 해당 트리 생성은 다음과 같은 상황에서 필요하다.</p>
<ol>
<li>초기 렌더링:<ol>
<li>초기에 전체 트리구조를 만들어야 이후에 다시 렌더링이 될때 어떤 기준으로 변경해야할 기준점이 필요하다. 그래서 초기에 전체 트리를 만들어야 한다.</li>
</ol>
</li>
<li>상태 또는 속성 변경 시:<ol>
<li>상태 변경시 새롭게 트리를 그려야 변한 상태를 기준으로 그려진 최신 노드 정보로 이루어진 트리를 얻을 수 있다. 그리고 이 트리를 기준으로 바뀐 부분만 DOM에 적용해야한다.</li>
</ol>
</li>
</ol>
<p>이 트리 구조는 최종적으로 중첩 객체로 이루어져있다. 자식으로 컴포넌트가 있다면 자식 컴포넌트를 실행시켜서 새로운 객체로 풀어버린다. 이런 과정을 재귀적으로 실행시키면 하위의 모든 노드들이 객체로 풀어져 있다.</p>
<p>→ 그 이후에는 위의 로직 사용한다.</p>
<hr>
<p>→ 일단 아래는 무시</p>
<h2 id="문제-발견_새로운-트리를-만드는-함수를-따로-구현-하지-않는다중복-로직">문제 발견_새로운 트리를 만드는 함수를 따로 구현 하지 않는다.(중복 로직)</h2>
<p>기존의 internalRender 함수를 리팩토링하여 Node순환을 한번만 한다.</p>
<h2 id="트리-생성-설계">트리 생성 설계</h2>
<h3 id="기존-로직의-오류_-잘못된-생각-수정사항">기존 로직의 오류_ 잘못된 생각 수정사항</h3>
<p>일단 계속해서 내가 헷갈려 하는 부분을 찾았다. 내가 생각하는 기본적인 트리 구조는 각 노드들이 동일한 데이터 구조(ex. 객체)를 갖고 있고, 만약 자식이 있다면 각 노드들이 자식으로 중첩된 객체 구조로 이루어져 있다고 생각했다. 그리고 지금 만드려는 트리 구조도 계속 이런 생각으로 설계를 시도했다. 하지만 여기서 이상한 부분이 있다.</p>
<blockquote>
<p>✅ 함수형 컴포넌트 Vnode와 호스트 엘리먼트 VNode</p>
</blockquote>
<p>이 두개는 internalRender 함수를 실행 하면 다른 결과가 나온다. 우선 이 두개를 다시 정의해 보자.</p>
<ul>
<li>노드의 종류:<ul>
<li>호스트 엘리먼트 VNode(<code>type: string</code>, 예: <code>&lt;div&gt;,</code> <code>&lt;button&gt;</code> ): 실제 DOM 엘리먼트에 직접 대응 되는 노드. 이 노드는 children 속성을 통해 다른 vnode를 자식을 갖는다. → 내가 생각하는 일반적이 노드.</li>
<li>함수형 컴포넌트 VNode (<code>type: function</code>, 예: <code>&lt;App&gt;</code>, <code>&lt;Btn&gt;</code>): 이 노드는 그 자체가 DOM엘리먼트가 아니다. 즉 이 노드 단독으로는 DOM을 그릴수 없다. 해당 컴포넌트 함수를 호출해 해소해줘야 한다.(함수형 컴포넌트를 실행 했을때의 반환값은 하위 VNode를 반환하기에…) 보통 호스트 엘리먼트 VNode를 반환한다. → <em><strong>자기 자신을 실행하면 다른 VNode를 ‘생산’한다.</strong></em></li>
</ul>
</li>
<li>부모 - 자식 관계:<ul>
<li>호스트 엘리먼트 VNode: children 속성을 통해 자식 VNode (호스트 엘리먼트 VNode 또는 함수형 컴포넌트 VNode)를 가진다.</li>
<li>함수형 컴포넌트 VNode: 이 노드의 자식은 <code>props.children</code> 이 아니다. 이건 컴포넌트에 전달되는 prop일 뿐이다. 이 노드의 자식은 함수형 컴포넌트가 반환한 결과의 Vnode이다.</li>
</ul>
</li>
<li>진짜 과제: 함수형 컴포넌트는 실제 DOM 트리에 직접 나타나지 않는다. (반환된 VNode가 나타나는 것일 뿐) 어떻게 이 함수형 컴포넌트와 해소된 결과를 VNode트리에 연결할지를 해결 해야한다.</li>
</ul>
<h3 id="해결책생각의-흐름">해결책(생각의 흐름)</h3>
<blockquote>
<p>✅ type이 함수형 컴포넌트라도 일단 type이 호스트 엘리먼트가 되도록 모두 풀어서 중첩 객체로 표현할까?</p>
</blockquote>
<p>이 생각처럼 하려면 2단계가 필요하다.</p>
<ol>
<li>✅ 함수 해소 : 함수형 컴포넌트는 함수를 호출해서 새로운 하위 VNode를 얻어야한다.(VNode로 해소해야한다)</li>
<li>✅ 트리 연결: 해소된 VNode도 그냥 두는게 아니라 다시 렌더링을 하게된다. 이때의 결과인 <code>RenderdeVNode</code> 가, 원래의 상위 함수형 컴포넌트 RenderedVnode의 <code>_renderedChildVNode</code>속성으로 중첩되야한다. 그리고 해소된 Vnode의 internalRender 함수의 결과값인 RenderedVnode 는 그 자식들을 <code>_renderedChildren</code> 으로 중첩 시킨다.
<img src="https://velog.velcdn.com/images/y-minion/post/2d2f6a61-b5e2-4ad7-ac5a-514fac1aeded/image.png" alt=""></li>
</ol>
<h3 id="rendervnode-구조-설계">RenderVNode 구조 설계</h3>
<p>기존의 internaRender 함수의 반환값은 없었지만 트리구조를 그리기 위해서는 RenderVNode를 반환 해야한다. RenderVNode의 인터페이스는 다음과 같다.</p>
<ol>
<li><p>기존의 VNode를 확장</p>
</li>
<li><p>_renderedChildVNode 필드</p>
<ol>
<li>입력받은 VNode가 컴포넌트 함수일 경우 해당 함수를 <strong><em>해소</em></strong> 할때의 값.</li>
</ol>
</li>
<li><p>_renderedChildren</p>
<ol>
<li>호스트 엘리먼드가 internalRender실행시 반환하는 Vnode 자식들</li>
</ol>
</li>
<li><p>domRef</p>
<ol>
<li><p>각각의 노드(RenderVNode)들이 생성될때 실제 DOM에 그려지는 노드를 참조한다.</p>
</li>
<li><p>해당 필드가 있어야 실제 변화가 발생했을 때 정확히 어디를 변경해야할 지 알 수 있다.</p>
</li>
<li><p>각 internalRender 마다 </p>
<pre><code class="language-javascript">const rootNode: HTMLElement = document.createElement(vnode.type);</code></pre>
<p>위처럼 생성되는 노드를 참조한다. 이러면 최종적으로 만들어지는 트리만 갖고도 실제 DOM에 접근 할 수 있다.</p>
<h3 id="internalrender-함수에-적용">internalRender 함수에 적용</h3>
<p>이제 기존의 internalRender 함수를 수정해보자.</p>
<p>기존의 함수는 반환값이 없어서 아무것도 할 수 없다. 단지 사이드 이팩트로 DOM만 드리는 작업을 한다. 하지만 이제는 DOM 그리기 + 트리 생성 까지 할 수 있도록 수정해 보자.</p>
</li>
</ol>
<hr>
<h4 id="작업-로그">작업 로그</h4>
<ul>
<li><p>internalRender가 반환하는 RenderVNode의 인터페이스를 정의한다.</p>
<pre><code class="language-tsx">interface RenderVNode extends VNode{
 _renderedChildVNode?:VNode,
 _renderedChildren?:VNode[],
 domRef?:HTMLElement
}</code></pre>
</li>
<li><p>함수형 컴포넌트일 경우 해소하는 과정. → 해당 컴포넌트를 호출해 해소한다.</p>
<pre><code class="language-tsx">const resolvedComponent: VNode = vnode.type(vnode.props); //컴포넌트 해소
  internalRender(resolvedComponent, parent);</code></pre>
</li>
</ul>
</li>
</ol>
<ul>
<li>풀어진 VNode를 internalRender 함수에 넣는다. 이때 internalRender 함수가 반환하는 RenderedVNode를 상위노드(= RenderedVNode)의 _renderedChildVNode 필드가 참조하도록 한다.<ul>
<li>이렇게 해야 tree의 연결 구조를 만들 수 있다.</li>
</ul>
</li>
</ul>
<pre><code class="language-tsx">// 해소된 Vnode를 internalRender 함수로 재귀한 값인 RenderedVNode를 상위노드와 연결해야 트리 구조가 만들어 진다.
    renderedVNode._renderedChildVNode = internalRender(
      resolvedComponent,
      parent
    );</code></pre>
<ul>
<li>함수형 컴포넌트가 아닐경우 해당 로직이 실행 된다.</li>
<li>각 노드를 처리할때마다 상위 renderedVNode에 계속해서 연결을 해줘야 한다!</li>
</ul>
<pre><code class="language-tsx">Object.entries(vnode.props).forEach(([prop, value]) =&gt; {
    if (prop === &quot;children&quot; &amp;&amp; value != null &amp;&amp; Array.isArray(value)) {
      childrenHandler(rootNode, value as ChildElementType[], renderedVNode);

//...

function childrenHandler(
  rootNode: HTMLElement,
  value: ChildElementType[],
  renderedVNode: RenderedVNode
) {
  value.forEach((child: ChildElementType) =&gt; {
    // 1) 문자열 또는 숫자면 텍스트 노드
    if (typeof child === &quot;string&quot; || typeof child === &quot;number&quot;) {
      const textNode = document.createTextNode(String(child));
      rootNode.appendChild(textNode);
      //🔎 리프노드일 경우 텍스트 노드전용 RenderedVNode를 만들어 tree구조에 연결해준다.
      const textRenderedVNode: RenderedVNode = {
        type: &quot;#text&quot;,
        props: { nodeValue: String(child) },
        domRef: textNode,
        key: null,
        ref: null,
      };
      //🔎 상위 renderedVNode에 연결한다. -&gt; tree의 연결고리 생성
      renderedVNode._renderedChildren!.push(textRenderedVNode);
      return;
    } else if (child !== null &amp;&amp; child !== undefined) {
    // 🔎 호스트 엘리먼트일 경우 재귀로 internalRender를 실행하는데 이때 생기는 반환 노드를 상위의 노드(renderedVNode)에 연결한다.
      renderedVNode._renderedChildren!.push(internalRender(child, rootNode));
      return;
    }
  });
}</code></pre>
<h3 id="최종-로직-시각화">최종 로직 시각화</h3>
<ul>
<li>위의 작업들은 아래 사진과 같다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/y-minion/post/2d2f6a61-b5e2-4ad7-ac5a-514fac1aeded/image.png" alt=""></p>
<hr>
<h3 id="메모장">메모장</h3>
<p>모든 VNode는 intnernalRender 함수를 통해 렌더링이 된다. 이때 이 함수는 어떤 VNode를 받던 최종적으로는 VNode의 <strong><em>렌더링된 결과</em></strong> 를 나타내는 RenderedVNode를 반환 해야한다. 그리고 이 renderedVNode객체들이 메모리 상의 트리를 구성하는 실제 ‘노드’ 들이다.</p>
<p>최종적으로 internalRender함수로 만즐어진 트리는 internalRender함수의 외부인 render 함수에서 변수로 받아야 할듯?</p>
<hr>
<h2 id="재조정-과정_-생각의-흐름-설계">재조정 과정_ (생각의 흐름 설계)</h2>
<blockquote>
<p>✅ 재조정 과정은 리렌더링 되는 새로운 노드 트리 → diff 비교 이렇게 순차적으로 이러나는게 아니라 동시에 발생한다. 리렌더링 되면서 새로운 노드가 만들어기 전에 diff 비교를 하고 결과에 따라 노드가 만들어 진다.</p>
</blockquote>
<h3 id="diff-알고리즘을-위한-상태-비교-이전-알고리즘">Diff 알고리즘을 위한 상태 비교, 이전 알고리즘</h3>
<ul>
<li><p>리액트팀의 휴리스틱 알고리즘</p>
<ul>
<li><p>기존의 노드와 리렌더링으로 만들어지는 새로운 노드를 비교할때는 다음과 같은 기준으로 비교한다.</p>
<ol>
<li><p>서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.</p>
</li>
<li><p>개발자는 <code>key</code> prop을 통해, 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시할 수 있다. → 최적화의 핵심!</p>
<p>하지만 key를 사용하지 않아도 버그는 발생하지 않는다. 하지만 개발자의 의도대로 동작하지 않는 위험이 존재한다.</p>
</li>
</ol>
</li>
</ul>
</li>
</ul>
<h3 id="diff-과정">diff 과정</h3>
<p>만약 개발자가 key prop을 사용하지 않고 개발을 하는 상황을 전개 해본다. 이때 내가 만든 프레임워크는 어떻게 노드끼리 비교를 해 상태를 이전할 수 있을까?</p>
<p><img src="https://velog.velcdn.com/images/y-minion/post/5aef60bb-77c6-41c2-9363-0e49d2e3516b/image.png" alt=""></p>
<p>위와 같은 노드 트리의 List노드에서 리렌더링이 발생한다.</p>
<p>그러면 자식의 Item컴포넌트 노드들이 생성된다. 이때 기존의 노드 인스턴스와 동일한 노드가 생성되지 않는다. List 컴포넌트가 싱행되면서 새로운 자식 노드 인스턴스들이 생성된다. 하지만 이대로 만들어진 노드들로 이루어진 tree를 이용해 DOM을 그리면 안된다. 이렇게 되면 기존의 상태들은 동기화 되지 못하고 초기화된 상태를 갖고 있기 때문에 업데이트된 최신 상태를 이용 할 수 없다.(위의 사진처럼 초기화된 state:0을 갖고 있다.)</p>
<blockquote>
<p>✅ 동기화 작업 등장!</p>
</blockquote>
<p>그래서 이때 동기화 작업을 생각하게 된다.</p>
<pre><code class="language-jsx">  &quot;각 노드들을 비교하면서 해당 노드 인스턴스가 리렌더링 전의 인스턴스와 동일한 노드일까?&quot;
    ? &quot;상태 필드 동기화 시작!&quot;
    : &quot;새롭게 생성된 노드 인스턴스 그래도 사용.&quot;</code></pre>
<ul>
<li><p>각 노드 단계에서 리렌더링 전의 노드와 리렌더링 후의 노드를 비교해본다.</p>
<ul>
<li>일단 key의 유무부터 확인한다. 있으면 동일한 key를 가진 노드 끼리 비교한다. (이때 휴리스틱 알고리즘사용)</li>
<li>key가 없으면 해당하는 index끼리 비교를 진행 한다.</li>
</ul>
</li>
<li><p>만약 동일하지 않으면 리렌더링으로 새롭게 생성된 노드를 교체한다.</p>
<ul>
<li>이때 교체가 진행된 노드는 표식을 남겨야 한다.</li>
</ul>
</li>
<li><p><del>만약 동일한 노드(type과 index가 동일)하면 리렌더링 전의 상태 필드를 새로운 노드에 주입한다. 그리고 리렌더링 된 노드를 기존 노드 트리에 교체한다.</del>  → 잘못된 생각!</p>
</li>
<li><p>만약 동일한 노드(type과 index가 동일)하면 리렌더링 전의 VNode를 재사용 한다. 그리고 새롭게 생기는 VNode의 Props필드는 최신값이므로 덮어 쓴다. → 이렇게 되면 업데이트된 상태를 계속해서 사용할 수 있다.</p>
<ul>
<li>이때도 마찬가지로 교체가 완료됐다는 표식을 남긴다.</li>
</ul>
</li>
</ul>
<p>→ 위의 과정을 추상화 하면 다음 사진과 같다.</p>
<p><img src="https://velog.velcdn.com/images/y-minion/post/8fe927f0-daf1-4743-ab9e-618f7e5d93a3/image.png" alt=""></p>
<ul>
<li>하지만 이 과정에는 이상한 흐름이 있다. 일단 전체 흐름을 다시 보자.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/y-minion/post/c5e1da2d-8938-4b37-bd73-bbcf9e35fcd6/image.png" alt=""></p>
<p>사실 개발자가 원하는 결과는 위와 같지 않다. 파란색Item노드와 빨강색 Item노드의 순서가 바뀌면서 상태 또한 빨강색 노드의 상태인 <code>state:1</code> 은 그대로 리렌더링 후 인덱스 1인 노드(빨강색 노드) 에 바인딩 되고, 파랑색 노드의 상태인 <code>state:2</code> 는 리렌더링 후 인덱스 0 인 노드(파랑색 노드) 에 바인딩이 된는게 개발자가 원하는 결과이다.</p>
<p>하지만 key가 없기 때문에 index를 기준으로 비교를 하는데 이때 type이 동일해 동일한 인스턴스 노드라고 판단하여 엉뚱한 VNode를 재사용 하게된다. 그래서 실제로 리액트 팀에서는 key의 사용을 강력하고 권하고 있다. </p>
<blockquote>
<p>(사실 공식문서로 key 가 중요하다~ 이런 말 계속 봤는데 실제로 이렇게 뜯어서 확인하니까 엄청 중요한 수준이 아니라 무조건 써야한다…)</p>
</blockquote>
<h2 id="재조정-과정-디테일-설계">재조정 과정 디테일 설계</h2>
<blockquote>
<p>✅ 렌더 단계와 Diffing의 동시적 진행</p>
</blockquote>
<h3 id="1-리렌더링-시작-및-컴포넌트-함수-호출">1. 리렌더링 시작 및 컴포넌트 함수 호출:</h3>
<ul>
<li><code>setState</code> 등으로 리렌더링이 트리거되면, 변경이 발생한 컴포넌트(예: <code>List</code>)의 렌더링 함수(<code>vnode.type(vnode.props)</code> 호출)가 다시 실행된다.</li>
<li>이 함수 호출 결과로 <strong>새로운 VNode 객체(<code>currentVNode</code>)가 반환된다</strong>. 이 VNode 객체는 이전 VNode 객체와는 다른 독립적인 JavaScript 객체임.</li>
</ul>
<h3 id="2-노드-단위의-즉각적인-diffing-및-인스턴스-관리-렌더-단계-안에서"><strong>2. 노드 단위의 즉각적인 Diffing 및 인스턴스 관리 (렌더 단계 안에서):</strong></h3>
<ul>
<li><code>internalRender</code> 함수가 이 새로운 VNode를 받아서 처리할 때, 단순히 DOM을 만들거나 트리를 구축하는 것을 넘어, <strong>이 새로운 VNode를 이전 렌더링의 해당 위치에 있던 <code>RenderedVNode</code> (oldVNode)와 즉각적으로 비교(diff)한다.</strong></li>
</ul>
<blockquote>
<p>✅ diff 비교 과정에서는root의 VNode가 비교 되는게 아니라 root intenralRender 함수의 반환값인 <strong><code>RenderedVNode</code> 를 비교한다는걸 계속 인지 해야 한다. →</strong> (<strong><code>RenderedVNode</code></strong> 로 이루어진 트리를 순회하면서 비교 하는거니까…)</p>
</blockquote>
<p>→ root의 RenderedVNode와 새로운 VNode 비교</p>
<ul>
<li><p>이 비교 과정이 매우 중요하다.</p>
<ul>
<li><p><strong><code>type</code> 비교:</strong> <code>oldVNode</code>와 <code>currentVNode</code>의 <code>type</code>이 같은지 확인한다.</p>
</li>
<li><p><strong><code>key</code> 비교 (리스트인 경우):</strong> <code>type</code>이 같고 리스트 안에 있다면 <code>key</code>를 비교한다.</p>
</li>
<li><p><strong>인스턴스 재사용 또는 생성:</strong></p>
<ul>
<li><p>일단 type 이 있는지 먼저 확인하고, 없으면 index를 기준으로 비교 대상을 정한다. 이때 <code>type</code>과 <code>key</code> (또는 인덱스)가 일치하여 <strong>동일한 논리적 컴포넌트</strong>라고 판단되면, 해당 컴포넌트의 <strong>기존 인스턴스(상태를 가진)</strong>를 재사용한다. 즉, 이전에 만들어져 상태를 가지고 있던 그 컴포넌트 인스턴스를 버리지 않고 그대로 둔다.</p>
<p>  <code>internalRender.ts</code>에서 <code>pushCurrentVNode</code>와 <code>popCurrentVNode</code>를 사용하는 <code>hookManager</code> 부분이 바로 이 컴포넌트 인스턴스와 훅 상태를 연결하는 로직과 매우 중요한 연관이 있다. → 위의 함수들이 useState가 지금 어떤 노드에서 실행되는지 알 수 있게 해준다.</p>
</li>
<li><p>이 재사용된 인스턴스에 새로운 VNode의 <code>props</code>를 전달하여 컴포넌트 함수를 다시 실행한다. 이때 <code>useState</code>는 이 인스턴스에 연결된 <strong>기존 상태 값을 반환</strong>하므로, 상태가 유지된다. → 기존 상태 값을 반환 할 수 있는 이유는 전적으로 <code>hookManager</code> 덕분…</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="2-1-중요한점">2-1. 중요한점!</h3>
<p> 나는 2번 과정에서 계속 기존의 상태를 <strong>주입</strong> 한다고 생각했는데 그게 아니다! 일단 사진을 전체 재조정 과정은 아래의 사진과 같다.</p>
<p><img src="https://velog.velcdn.com/images/y-minion/post/1ab554c6-b81e-4638-b3ba-dc18812e0b43/image.png" alt=""></p>
<ul>
<li>diff 비교를 해서 만약에 같으면 root의 Tree의 RenderedVNode(VNode)의 Props필드를 업데이트 한다.</li>
<li>그리고 해당 함수(컴포넌트)를 업데이트된 최신 props을 넣어 호출한다.</li>
<li>호출된 자식 노드들도 위의 과정을 반복해서 실행.</li>
</ul>
<h3 id="3-자식-노드로-재귀적-진행">3. 자식 노드로 재귀적 진행:</h3>
<ul>
<li>현재 노드에 대한 인스턴스 결정 및 상태 처리 후, 그 노드의 자식 VNode들에 대해서도 위 2번의 과정을 재귀적으로 반복한다.</li>
</ul>
<h3 id="핵심">핵심!</h3>
<ul>
<li><p><code>diff</code> 를 렌더 단계와 분리해서 생각하면 안된다. 렌더 단계에서 새로운 VNode 트리를 구축하는 순간에 노드 단위에서 각 VNode를 이전 트리의 해당 노드와 diff (즉시 비교) 한다.</p>
</li>
<li><p>결국 기존에 최초에 생성된 root트리를 계속해서 수정한다.</p>
</li>
<li><p><strong>이때 실제 DOM 조작은 발생하지 않는다</strong>: 해당 과정에서는 실제 브라우저 DOM 을 직접 조작 하면 안된다.</p>
<p>  변경된 사항을 ‘기록’ 하거나, <code>RenderedVNode</code> 트리 내부적으로 <code>domRef</code>를 통한 연결 상태를 업데이트할 뿐입니다. → 실제 DOM을 조작하는 행위는 위의 모든 작업이 끝난 뒤에, 변경사항이 발생한 표식을 추적해서 변경된 부분만 DOM을 수정해 최소한으로 DOM에 접근해야 한다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프레임워크 제작]환경 구성 연대기(최종)]]></title>
            <link>https://velog.io/@y-minion/MyReact-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-core%EC%99%80-jsx-demo-%ED%99%98%EA%B2%BD-%EB%B6%84%EB%A6%AC-%EB%B0%8F-Classic-JSX-%EB%9F%B0%ED%83%80%EC%9E%84-%EC%A0%81%EC%9A%A9%EA%B8%B0</link>
            <guid>https://velog.io/@y-minion/MyReact-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-core%EC%99%80-jsx-demo-%ED%99%98%EA%B2%BD-%EB%B6%84%EB%A6%AC-%EB%B0%8F-Classic-JSX-%EB%9F%B0%ED%83%80%EC%9E%84-%EC%A0%81%EC%9A%A9%EA%B8%B0</guid>
            <pubDate>Mon, 16 Jun 2025 14:58:33 GMT</pubDate>
            <description><![CDATA[<ul>
<li>사실 부끄럽지만 그동안 코드를 짜면서 config 파일은 전혀 건드리지 않고 (vite가 만들어주는거 그대로 사용하는...) 코딩을 해왔어서 꼭 해야 할 것 같다고 생각했다.</li>
<li>내가 만든 프레임워크랑 프레임워크를 테스트하는 환경을 분리했기에, 이에 맞는 환경설정을 내가 직접 해야만 했다.</li>
</ul>
<h2 id="🎯-초기-목표">🎯 초기 목표</h2>
<ol>
<li><p><strong>프레임워크 코드</strong> (<code>/core</code>)  </p>
<ul>
<li>순수 TypeScript(.ts)로만 작성  </li>
<li>TS 컴파일러는 타입 검사만 수행 (JSX 변환 불필요)</li>
</ul>
</li>
<li><p><strong>데모/테스트 코드</strong> (<code>/jsx-demo/*.jsx</code>)  </p>
<ul>
<li>Vite(ESBuild)로만 <code>.jsx → .js</code> 트랜스파일  </li>
<li>Classic 런타임 방식 적용  <ul>
<li>팩토리 함수: <code>MyReact.createElement</code>  </li>
<li>프래그먼트: <code>MyReact.Fragment</code>  </li>
<li>자동 import 주입: <code>jsxInject</code></li>
</ul>
</li>
</ul>
</li>
<li><p><strong>별칭(alias)</strong>  </p>
<ul>
<li><code>@core/*</code>, <code>@/*</code> 등이 VSCode와 Vite 양쪽에서 정상 작동</li>
</ul>
</li>
<li><p><strong>개발 서버(root)</strong>  </p>
<ul>
<li>웹 루트는 <code>/jsx-demo</code>  </li>
<li><code>/src</code> (프레임워크 코드)와 <code>/jsx-demo</code> 모두 파일 시스템에서 서빙 허용</li>
</ul>
</li>
</ol>
<hr>
<h2 id="🔧-초기-설정">🔧 초기 설정</h2>
<h3 id="1-tsconfigjson">1) tsconfig.json</h3>
<pre><code class="language-javascript">{
  &quot;compilerOptions&quot;: {
    &quot;allowJs&quot;: true,
    &quot;jsx&quot;: &quot;preserve&quot;,        // JSX를 보존만 – 변환은 ESBuild에 위임
    &quot;target&quot;: &quot;ES2020&quot;,
    &quot;module&quot;: &quot;ESNext&quot;,
    &quot;lib&quot;: [&quot;ES2020&quot;, &quot;DOM&quot;, &quot;DOM.Iterable&quot;],
    &quot;baseUrl&quot;: &quot;src&quot;,
    &quot;paths&quot;: {
      &quot;@/*&quot;:       [&quot;*&quot;],
      &quot;@shared/*&quot;: [&quot;shared/*&quot;],
      &quot;@core/*&quot;:   [&quot;core/*&quot;]  // bare &quot;@core&quot; 매핑 누락
    },
    &quot;moduleResolution&quot;: &quot;bundler&quot;,
    &quot;noEmit&quot;: true,
    &quot;strict&quot;: true,
    &quot;types&quot;: [&quot;vitest&quot;]
  },
  &quot;include&quot;: [&quot;src&quot;, &quot;jsx-demo&quot;]
}</code></pre>
<h3 id="2-viteconfigts">2) vite.config.ts</h3>
<pre><code class="language-javascript">
import { defineConfig } from &quot;vite&quot;;
import path from &quot;path&quot;;
import tsconfigPaths from &quot;vite-tsconfig-paths&quot;;

export default defineConfig({
  // ▶ 개발 서버 루트: 데모 폴더만 호스팅
  root: path.resolve(__dirname, &quot;jsx-demo&quot;),

  plugins: [
    // ▶ 최상위 tsconfig.json 경로 직접 지정
    tsconfigPaths({
      projects: [path.resolve(__dirname, &quot;tsconfig.json&quot;)]
    }),
    /*
    // Babel 기반 React 플러그인 대신 ESBuild 사용
    react({
      jsxRuntime: &quot;classic&quot;,
    }),
    */
  ],

  esbuild: {
    // ▶ Classic JSX 런타임 지정
    jsxFactory:  &quot;MyReact.createElement&quot;,
    jsxFragment: &quot;MyReact.Fragment&quot;,
    jsxInject:  `import MyReact from &#39;@/core&#39;;`,
    // ▶ include 패턴 누락으로 초기 .jsx 처리 실패
    // include: [/\.jsx$/],
  },

  server: {
    fs: {
      // ▶ /src + /jsx-demo 모두 서빙 허용
      allow: [
        path.resolve(__dirname, &quot;src&quot;),
        path.resolve(__dirname, &quot;jsx-demo&quot;),
      ],
    },
    port: 3000,
    open: true,
  },

  build: {
    outDir: &quot;jsx-demo/dist&quot;,
    sourcemap: true,
  },
});</code></pre>
<p>⸻</p>
<h2 id="🐞-주요-이슈--해결">🐞 주요 이슈 &amp; 해결</h2>
<h3 id="미리보기">미리보기</h3>
<p><img src="https://velog.velcdn.com/images/y-minion/post/2fdacf38-bb7a-46c4-b565-d52fdc6ce200/image.png" alt=""></p>
<h3 id="✅-tsconfig-별칭alias-미인식"><strong>✅ TSConfig 별칭(alias) 미인식</strong></h3>
<ul>
<li><p><strong>증상</strong></p>
<p>  src/core/index.ts 같은 프레임워크 코드에서는 import createElement from &quot;@core/createElement&quot;가 동작하지만, 테스트용 jsx-demo 폴더에서는 별칭이 인식되지 않음.</p>
</li>
<li><p><strong>원인</strong></p>
<ul>
<li>Vite 설정의 root가 jsx-demo여서, vite-tsconfig-paths 플러그인이 jsx-demo/tsconfig.json만 찾도록 동작했기 때문.</li>
<li>실제 프레임 워크의 코드는 root 경로가 아닌 다른 경로에 있어서 따로 경로를 명시해줘야한다.</li>
</ul>
</li>
<li><p><strong>해결</strong></p>
<p>  vite.config.ts에서 플러그인 호출부를 이렇게 수정:</p>
</li>
</ul>
<pre><code class="language-jsx">// vite.config.ts
import tsconfigPaths from &quot;vite-tsconfig-paths&quot;;

export default defineConfig({
  root: path.resolve(__dirname, &quot;jsx-demo&quot;),
  plugins: [
    tsconfigPaths({
      // 프로젝트 최상위의 tsconfig.json을 직접 지정
      projects: [ path.resolve(__dirname, &quot;tsconfig.json&quot;) ]
    }),
    // (plugin-react는 잠시 주석 처리)
  ],
  // …그 외 설정…
});</code></pre>
<ul>
<li><blockquote>
<p>vite-tsconfig-paths가 올바른 paths 설정을 읽어와, 데모 환경에서도 별칭이 적용됨.</p>
</blockquote>
</li>
</ul>
<hr>
<h3 id="✅-vite-root-와-스크립트-로딩-경로"><strong>✅ Vite root 와 스크립트 로딩 경로</strong></h3>
<ul>
<li><strong>설정</strong></li>
</ul>
<pre><code class="language-jsx">root: path.resolve(__dirname, &quot;jsx-demo&quot;),</code></pre>
<ul>
<li>→ 개발 서버의 웹 루트를 프로젝트 내 jsx-demo/ 폴더로 지정.(프레임 워크 테스트 전용 폴더)</li>
<li><strong>index.html 수정</strong></li>
</ul>
<pre><code class="language-jsx">&lt;!-- bundle.js 대신 ES 모듈 직통 로드 --&gt;
&lt;script type=&quot;module&quot; src=&quot;app.jsx&quot;&gt;&lt;/script&gt;</code></pre>
<ul>
<li>→ dev 모드에서는 빌드된 dist가 아니라, Vite가 메모리에서 제공하는 app.jsx를 직접 불러와야 함.</li>
</ul>
<hr>
<h3 id="✅-404--fs-allow-에러"><strong>✅ 404 &amp; FS-Allow 에러</strong></h3>
<ol>
<li><strong>404 에러</strong></li>
</ol>
<pre><code>GET /dist/bundle.js 404</code></pre><ol>
<li>→ dev 모드엔 bundle.js가 없어서 404 발생.</li>
<li><strong>FS-Allow 에러</strong></li>
</ol>
<pre><code>… outside of Vite serving allow list</code></pre><ol>
<li>→ Vite는 기본적으로 src와 node_modules/vite/dist/client만 서빙 허용.</li>
</ol>
<ul>
<li><strong>해결</strong></li>
</ul>
<pre><code class="language-jsx">server: {
  fs: {
    allow: [
      path.resolve(__dirname, &quot;src&quot;),         // 기존 프레임워크 코드
      path.resolve(__dirname, &quot;jsx-demo&quot;)     // 테스트 데모 코드
    ]
  }
}</code></pre>
<hr>
<h3 id="✅-esbuild-include-패턴-문제"><strong>✅ esbuild include 패턴 문제</strong></h3>
<ul>
<li><strong>증상</strong></li>
</ul>
<pre><code>Pre-transform error: Failed to load url /app.jsx</code></pre><ul>
<li><p>→ esbuild 변환 대상이 아니라고 인식됨.</p>
</li>
<li><p><strong>원인</strong></p>
<p>  esbuild.include가 /jsx-demo/.*.jsx$/로만 정의돼, /app.jsx 경로와 매칭되지 않음.</p>
</li>
<li><p><strong>해결</strong></p>
</li>
</ul>
<pre><code class="language-jsx">esbuild: {
  loader: &quot;jsx&quot;,
  include: [/\.jsx$/],       // .jsx로 끝나는 모든 파일을 처리
  jsxFactory: &quot;MyReact.createElement&quot;,
  jsxFragment: &quot;MyReact.Fragment&quot;,
  jsxInject: `import MyReact from &#39;@core&#39;;`,
}</code></pre>
<hr>
<h3 id="✅-베어alias-only-core-미인식"><strong>✅ 베어(alias-only) @core 미인식</strong></h3>
<ul>
<li><strong>증상</strong></li>
</ul>
<pre><code>Failed to resolve import &quot;@core&quot; from &quot;jsx-demo/app.jsx&quot;</code></pre><ul>
<li><p><strong>원인</strong></p>
<p>  tsconfig.json의 paths에 &quot;@core/*&quot;만 있고 &quot;@core&quot; 단독 매핑이 없었음.</p>
</li>
<li><p><strong>해결</strong></p>
</li>
</ul>
<pre><code class="language-jsx">// vite.config.json
 esbuild: {
    // 오직 데모 폴더의 .jsx 만 처리
    // include: [/jsx-demo\/.*\.jsx$/],

    // Classic 런타임 프래그마 지정

    jsxFactory: &quot;MyReact.createElement&quot;,

    jsxFragment: &quot;MyReact.Fragment&quot;,

//별칭 경로 수정
    jsxInject: `import MyReact from &#39;@/core&#39;;`,
  },</code></pre>
<hr>
<h3 id="✅-중복-선언-오류"><strong>✅ 중복 선언 오류</strong></h3>
<ul>
<li><strong>증상</strong></li>
</ul>
<pre><code>Identifier &#39;MyReact&#39; has already been declared</code></pre><ul>
<li><p><strong>원인</strong></p>
<p>  app.jsx에 수동 import MyReact from &#39;@core&#39;;와, esbuild.jsxInject에 의한 자동 주입이 중복.</p>
</li>
<li><p><strong>해결</strong></p>
<p>  app.jsx에서 수동 import MyReact 구문 제거.</p>
<p>  → jsxInject가 빌드 시 자동으로 맨 위에 추가.</p>
</li>
</ul>
<hr>
<h3 id="✅-ts-파일-파싱-오류"><strong>✅ TS 파일 파싱 오류</strong></h3>
<ul>
<li><strong>증상</strong></li>
</ul>
<pre><code>Unexpected token &#39;{&#39; at createElement.ts</code></pre><ul>
<li><p><strong>원인</strong></p>
<p>  esbuild loader: &quot;jsx&quot;가 지정돼 .ts 파일은 전혀 트랜스폼되지 않고, 브라우저가 원본 TypeScript를 만나 문법 오류 발생.</p>
<p>  exbuild가 기존의 프레임워크 코드인 .ts 코드들을 트랜스폼하지 않았음…</p>
</li>
<li><p><strong>해결</strong></p>
<p>  esbuild.loader 옵션 제거(또는 .ts 제외).</p>
<p>  → Vite 기본 TS 트랜스폼 로더가 .ts/.tsx 파일을 정상 처리.</p>
</li>
</ul>
<hr>
<h3 id="🤔의문점">🤔의문점</h3>
<p>-&gt; 근데 vite.config 파일에 root 경로를 테스트 환경인 jsx-demo 를 지정했는데 읽어 올 수 없었을까?</p>
<h3 id="우선-옵션-설명-부터">우선 옵션 설명 부터</h3>
<ul>
<li>→ root 옵션과 server.fs.allow 옵션은 완전히 다른 역할을 수행한다.</li>
</ul>
<ol>
<li><p><strong>root: path.resolve(__dirname, &quot;jsx-demo&quot;)</strong></p>
<p> 이건 “Vite가 <strong>어느 폴더</strong>를 웹 루트(= / URL)로 삼을지”를 결정해 준다.</p>
<ul>
<li>jsx-demo/index.html 이 웹의 엔트리 포인트가 되고,</li>
<li>정적 에셋이나 모듈 경로를 /app.jsx, /src/foo.js 처럼 해석할 때 기준이 되는 폴더임.</li>
</ul>
</li>
<li><p><strong>server.fs.allow: [ … ]</strong></p>
<p> 이건 “Vite 개발 서버가 <strong>파일 시스템</strong> 상에서 <strong>어느 폴더</strong>까지 읽어와도 안전한지”를 <strong>화이트리스트</strong>로 지정해 주는 보안 설정이다.</p>
<ul>
<li>기본적으로 Vite는 프로젝트 루트(= root)와 node_modules/vite/dist/client만 허용한다.</li>
<li>하지만 이때 allow를 지정하면 “기본 허용 범위 대신” 이 배열 안의 절대경로만 서빙을 허락한다.</li>
<li>즉 allow: [path.resolve(__dirname, &quot;src&quot;)] 만 쓰면, jsx-demo 폴더는 <strong>허용되지 않아서</strong> 404나 FS-ALLOW 에러가 발생한다..</li>
</ul>
</li>
</ol>
<p>그래서 allow 리스트에 <strong>jsx-demo</strong> 경로를 <strong>명시적으로</strong> 추가해야만,</p>
<ul>
<li>Vite가 jsx-demo/app.jsx 를 “이제 파일 시스템에서 읽어도 된다!”</li>
<li>그러면서 Pre-transform이나 404 에러 없이 정상 제공할 수 있게 된다.</li>
</ul>
<p>정리하자면:</p>
<ul>
<li>root는 “웹상의 URL → 실제 폴더 매핑 기준”</li>
<li>fs.allow는 “파일 시스템 보안상 이 폴더도 읽어와라”</li>
</ul>
<hr>
<h2 id="최종-설정-요약"><strong>최종 설정 요약</strong></h2>
<h3 id="tsconfigjson"><strong>tsconfig.json</strong></h3>
<pre><code class="language-jsx">{
  &quot;compilerOptions&quot;: {
    &quot;baseUrl&quot;: &quot;src&quot;,
    &quot;paths&quot;: {
      &quot;@/*&quot;:       [&quot;*&quot;],
      &quot;@shared/*&quot;:[&quot;shared/*&quot;],
      &quot;@core/*&quot;:   [&quot;core/*&quot;],
      &quot;@core&quot;:     [&quot;core/index.ts&quot;]
    },
    &quot;jsx&quot;: &quot;preserve&quot;,
    /* …strict, bundler 모드 옵션… */
  },
  &quot;include&quot;: [&quot;src&quot;, &quot;jsx-demo&quot;]
}</code></pre>
<h3 id="viteconfigts"><strong>vite.config.ts</strong></h3>
<pre><code class="language-jsx">import { defineConfig } from &quot;vite&quot;;
import path from &quot;path&quot;;
import tsconfigPaths from &quot;vite-tsconfig-paths&quot;;

export default defineConfig({
  root: path.resolve(__dirname, &quot;jsx-demo&quot;),
  plugins: [
    tsconfigPaths({ projects: [ path.resolve(__dirname, &quot;tsconfig.json&quot;) ] })
  ],
  esbuild: {
    include: [/\.jsx$/],
    jsxFactory: &quot;MyReact.createElement&quot;,
    jsxFragment: &quot;MyReact.Fragment&quot;,
    jsxInject: `import MyReact from &#39;@core&#39;;`,
  },
  server: {
    fs: {
      allow: [
        path.resolve(__dirname, &quot;src&quot;),
        path.resolve(__dirname, &quot;jsx-demo&quot;)
      ]
    },
    port: 3000,
    open: true
  },
  build: {
    outDir: &quot;jsx-demo/dist&quot;,
    sourcemap: true
  }
});</code></pre>
<h3 id="indexhtml"><strong>index.html</strong></h3>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
  &lt;meta charset=&quot;UTF-8&quot; /&gt;
  &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;/&gt;
  &lt;title&gt;JSX Demo&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
  &lt;script type=&quot;module&quot; src=&quot;/app.jsx&quot;&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<h3 id="appjsx"><strong>app.jsx</strong></h3>
<pre><code class="language-jsx">function App() {
  return (
    &lt;div&gt;
      &lt;h1&gt;JSX Demo&lt;/h1&gt;
      &lt;p&gt;이것은 JSX 데모입니다.&lt;/p&gt;
    &lt;/div&gt;
  );
}

const root = document.getElementById(&quot;root&quot;);
MyReact.render(&lt;App /&gt;, root);</code></pre>
<p>⸻</p>
<h3 id="✅-최종-개선된-설정">✅ 최종 개선된 설정</h3>
<pre><code class="language-javascript">tsconfig.json

{
  &quot;compilerOptions&quot;: {
    &quot;baseUrl&quot;: &quot;src&quot;,
    &quot;paths&quot;: {
      &quot;@/*&quot;:       [&quot;*&quot;],
      &quot;@shared/*&quot;: [&quot;shared/*&quot;],
      &quot;@core/*&quot;:   [&quot;core/*&quot;],
      &quot;@core&quot;:     [&quot;core/index.ts&quot;]  // bare import 매핑 추가
    },
    &quot;jsx&quot;: &quot;preserve&quot;,                // JSX 보존
    &quot;moduleResolution&quot;: &quot;bundler&quot;,
    &quot;strict&quot;: true,
    &quot;noEmit&quot;: true,
    &quot;types&quot;: [&quot;vitest&quot;]
  },
  &quot;include&quot;: [&quot;src&quot;, &quot;jsx-demo&quot;]
}

vite.config.ts

import { defineConfig } from &quot;vite&quot;;
import path from &quot;path&quot;;
import tsconfigPaths from &quot;vite-tsconfig-paths&quot;;

export default defineConfig({
  root: path.resolve(__dirname, &quot;jsx-demo&quot;),

  plugins: [
    tsconfigPaths({
      projects: [path.resolve(__dirname, &quot;tsconfig.json&quot;)]
    }),
  ],

  esbuild: {
    include: [/\.jsx$/],                   // 모든 .jsx 처리
    jsxFactory:  &quot;MyReact.createElement&quot;,  // Classic runtime
    jsxFragment: &quot;MyReact.Fragment&quot;,
    jsxInject:  `import MyReact from &#39;@core&#39;;`
  },

  server: {
    fs: {
      allow: [
        path.resolve(__dirname, &quot;src&quot;),
        path.resolve(__dirname, &quot;jsx-demo&quot;)
      ]
    },
    port: 3000,
    open: true,
  },

  build: {
    outDir: &quot;jsx-demo/dist&quot;,
    sourcemap: true,
  },
});</code></pre>
<p>⸻</p>
<h2 id="🔑-핵심-정리">🔑 핵심 정리</h2>
<ul>
<li>프레임워크 코드(/core): tsc --noEmit로 타입 체크만</li>
<li>JSX 코드(/jsx-demo): ESBuild Classic 런타임으로 변환</li>
<li>Alias: bare @core 포함, VSCode &amp; Vite 모두 인식</li>
<li>서빙 권한: fs.allow로 /src + /jsx-demo 허용</li>
<li>중복 import 제거 &amp; include 패턴 명시로 트랜스폼 누락 방지</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[React에서의 자식 평탄화 이해하기: Babel, JSX, createElement, Virtual DOM 전환까지 전부 정리]]></title>
            <link>https://velog.io/@y-minion/React%EC%97%90%EC%84%9C%EC%9D%98-%EC%9E%90%EC%8B%9D-%ED%8F%89%ED%83%84%ED%99%94-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-Babel-JSX-createElement-Virtual-DOM-%EC%A0%84%ED%99%98%EA%B9%8C%EC%A7%80-%EC%A0%84%EB%B6%80-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@y-minion/React%EC%97%90%EC%84%9C%EC%9D%98-%EC%9E%90%EC%8B%9D-%ED%8F%89%ED%83%84%ED%99%94-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-Babel-JSX-createElement-Virtual-DOM-%EC%A0%84%ED%99%98%EA%B9%8C%EC%A7%80-%EC%A0%84%EB%B6%80-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 10 Jun 2025 16:00:45 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>JSX에서 <code>map()</code>으로 만든 배열은 가상돔인가요? Babel은 중첩 구조를 어떻게 처리하나요? 중첩 배열을 그대로 두면 왜 안 되나요?</p>
</blockquote>
<hr>
<h2 id="✅-핵심-정리">✅ 핵심 정리</h2>
<ul>
<li>Babel은 JSX를 중첩된 <code>React.createElement</code> 호출로 번역함 (계층 구조 유지)</li>
<li>중첩 배열은 JSX에서 <code>map()</code>을 쓸 때 발생하며, 이 배열은 <strong>가상돔이 아님</strong></li>
<li>React는 <code>createElement()</code> 내부에서 이를 평탄화하여 가상돔 트리에 정규화함</li>
<li>평탄화를 안 하면 렌더링, 비교, 유지보수가 복잡해짐</li>
</ul>
<hr>
<h2 id="1-babel은-계층-구조를-어떻게-처리하는가">1. Babel은 계층 구조를 어떻게 처리하는가?</h2>
<p>JSX 구조:</p>
<pre><code class="language-jsx">&lt;div&gt;
  &lt;p&gt;
    &lt;span&gt;Hello&lt;/span&gt;
  &lt;/p&gt;
&lt;/div&gt;</code></pre>
<p>Babel 변환 결과:</p>
<pre><code class="language-js">React.createElement(
  &#39;div&#39;,
  null,
  React.createElement(
    &#39;p&#39;,
    null,
    React.createElement(&#39;span&#39;, null, &#39;Hello&#39;)
  )
);</code></pre>
<ul>
<li>중첩된 구조 = <strong>중첩된 함수 호출</strong></li>
<li>각 자식은 부모의 <code>children</code> 세 번째 인자로 들어감</li>
<li>이 구조 자체는 평탄화 대상이 아님</li>
</ul>
<hr>
<h2 id="2-중첩-배열이-생기는-상황-jsx--map">2. 중첩 배열이 생기는 상황: JSX + <code>map()</code></h2>
<pre><code class="language-jsx">&lt;ul&gt;
  {[&lt;li&gt;1&lt;/li&gt;, [&lt;li&gt;2&lt;/li&gt;, &lt;li&gt;3&lt;/li&gt;], &lt;li&gt;4&lt;/li&gt;]}
&lt;/ul&gt;</code></pre>
<p>Babel 변환 시:</p>
<pre><code class="language-js">React.createElement(
  &#39;ul&#39;,
  null,
  [
    React.createElement(&#39;li&#39;, null, &#39;1&#39;),
    [
      React.createElement(&#39;li&#39;, null, &#39;2&#39;),
      React.createElement(&#39;li&#39;, null, &#39;3&#39;)
    ],
    React.createElement(&#39;li&#39;, null, &#39;4&#39;)
  ]
)</code></pre>
<ul>
<li>이 구조는 가상돔 객체들(VNode) 을 담고 있는 일반적인 JavaScript 배열임. 즉, 가상돔으로 구성된 JS 배열.</li>
<li>👉 하지만 여기서 중요한 건 “중첩 배열 구조” 에 있다는 것임. 가상돔이 맞긴 하지만, React 내부의 처리 흐름에선 이걸 그대로 쓰기엔 번거롭다.</li>
<li>렌더링 전까지는 배열로 남아 있음</li>
<li><code>createElement</code>에서 이를 <strong>평탄화</strong>하여 1차원 배열로 정리함</li>
</ul>
<hr>
<h2 id="3-왜-중첩-배열을-그대로-두면-안-되는가">3. 왜 중첩 배열을 그대로 두면 안 되는가?</h2>
<h3 id="📌-복잡한-렌더링-로직-유발">📌 복잡한 렌더링 로직 유발</h3>
<pre><code class="language-js">// 평탄화 안 하면 이렇게 복잡해짐
function render(children) {
  children.forEach(child =&gt; {
    if (Array.isArray(child)) {
      render(child); // 재귀적으로 호출
    } else {
      mount(child);
    }
  });
}</code></pre>
<h3 id="✅-평탄화-후엔-이렇게-단순해짐">✅ 평탄화 후엔 이렇게 단순해짐</h3>
<pre><code class="language-js">const flatChildren = children.flat(Infinity);
flatChildren.forEach(mount);</code></pre>
<ul>
<li>배열 타입인지 확인 필요 없음</li>
<li>재귀 X → 선형 반복으로 DOM 생성 가능</li>
</ul>
<h3 id="📌-비교-알고리즘-비효율성">📌 비교 알고리즘 비효율성</h3>
<ul>
<li>중첩 배열: 변경 추적 시 구조까지 비교해야 해서 느림</li>
<li>평평한 배열: key 기반 선형 비교 가능 → 빠름</li>
</ul>
<h3 id="📌-실제-dom-구조와-일치시키기">📌 실제 DOM 구조와 일치시키기</h3>
<ul>
<li>DOM은 직속 자식만 평평하게 가짐</li>
<li>가상돔도 이를 모사해야 실제 렌더링과 정확히 일치함</li>
</ul>
<hr>
<h2 id="4-createelement에서의-평탄화-처리-방식">4. <code>createElement</code>에서의 평탄화 처리 방식</h2>
<pre><code class="language-ts">function createElement(
  type: string | Function,
  props: HTMLAttributes | null,
  ...children: (Child | Child[])[]
): Readonly&lt;VNode&gt; {
  const { ref, key, ...rest } = props ?? {};
  const normalizedChildren: Children = Array.isArray(children)
    ? (children.flat(Infinity) as Child[])
    : null;</code></pre>
<ul>
<li><code>flat(Infinity)</code>로 중첩 배열 모두 펼침</li>
<li>결과: children이 항상 1차원 배열로 정리됨</li>
</ul>
<hr>
<h2 id="✅-결론-평탄화는-미리-정리하기이다">✅ 결론: 평탄화는 &#39;미리 정리하기&#39;이다</h2>
<blockquote>
<p>중첩 배열은 가상돔이 아니며, <code>createElement</code>가 이를 가상돔으로 변환하는 과정에서 평탄화가 발생합니다. 이 과정을 통해 React는 렌더링, 비교, 업데이트 등 모든 작업을 단순한 규칙에 따라 빠르게 처리할 수 있게 됩니다.</p>
</blockquote>
<ul>
<li>JSX 구조는 중첩 함수 호출로 → 평탄화 필요 없음</li>
<li><code>map()</code>으로 생긴 배열은 실제 배열이므로 → 반드시 평탄화 필요</li>
<li>React는 이를 <code>createElement</code> 안에서 처리하여 모든 자식을 1차원 가상돔 배열로 정규화</li>
<li>그 결과, 렌더링과 diff 알고리즘이 단순화되고, DOM 구조와 일관성 유지 가능</li>
</ul>
<blockquote>
<p>이 원칙은 바닐라 JS로 리액트 렌더러를 직접 만들 때도 반드시 따라야 할 핵심입니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[DOM 구조는 왜 '평평한 목록'인가? 그리고 React는 왜 이를 '평탄화(flatten)'해야 하는가?]]></title>
            <link>https://velog.io/@y-minion/DOM-%EA%B5%AC%EC%A1%B0%EB%8A%94-%EC%99%9C-%ED%8F%89%ED%8F%89%ED%95%9C-%EB%AA%A9%EB%A1%9D%EC%9D%B8%EA%B0%80-%EA%B7%B8%EB%A6%AC%EA%B3%A0-React%EB%8A%94-%EC%99%9C-%EC%9D%B4%EB%A5%BC-%ED%8F%89%ED%83%84%ED%99%94flatten%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94%EA%B0%80</link>
            <guid>https://velog.io/@y-minion/DOM-%EA%B5%AC%EC%A1%B0%EB%8A%94-%EC%99%9C-%ED%8F%89%ED%8F%89%ED%95%9C-%EB%AA%A9%EB%A1%9D%EC%9D%B8%EA%B0%80-%EA%B7%B8%EB%A6%AC%EA%B3%A0-React%EB%8A%94-%EC%99%9C-%EC%9D%B4%EB%A5%BC-%ED%8F%89%ED%83%84%ED%99%94flatten%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94%EA%B0%80</guid>
            <pubDate>Tue, 10 Jun 2025 15:41:08 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>HTML 구조는 계층적인데, DOM은 왜 &#39;평평한 목록&#39;으로 자식들을 저장할까? React는 왜 굳이 자식 노드를 평탄화해야 할까?</p>
</blockquote>
<hr>
<h2 id="1-dom은-계층적tree-구조인가">1. DOM은 계층적(Tree) 구조인가?</h2>
<p>-&gt; 맞습니다. HTML 문서를 작성하거나 DOM을 머릿속으로 떠올릴 때 우리는 <strong>부모-자식-손자-증손자</strong> 식의 <strong>트리(Tree) 구조</strong>를 상상합니다.</p>
<pre><code class="language-html">&lt;div id=&quot;grandparent&quot;&gt;
  &lt;p id=&quot;parent&quot;&gt;
    &lt;span id=&quot;child&quot;&gt;내용&lt;/span&gt;
  &lt;/p&gt;
&lt;/div&gt;</code></pre>
<ul>
<li><code>&lt;div&gt;</code>는 최상위 부모</li>
<li><code>&lt;p&gt;</code>는 <code>&lt;div&gt;</code>의 자식</li>
<li><code>&lt;span&gt;</code>은 <code>&lt;p&gt;</code>의 자식, 즉 <code>&lt;div&gt;</code>의 <strong>자손(descendant)</strong></li>
</ul>
<p>이러한 구조는 <strong>개념적인 계층 구조</strong>를 표현한 것으로, 브라우저는 이 구조를 기반으로 DOM 트리를 만듭니다.
(-&gt; 앞의 내용과 반복되는 부분입니다.)</p>
<hr>
<h2 id="2-실제-dom의-데이터-구조-평평한-자식-목록-flat-children-list">2. 실제 DOM의 데이터 구조: 평평한 자식 목록 (Flat Children List)</h2>
<p>하지만 DOM 노드 내부에서는 이 계층 구조가 <strong>중첩 배열</strong>로 표현되지 않습니다. 즉, 다음과 같은 구조는 존재하지 않습니다:</p>
<pre><code class="language-js">[ &lt;p&gt;, [&lt;span&gt;] ] // ❌</code></pre>
<p>대신 각 부모 노드는 <strong>직속 자식만을 담은 1차원적인 목록</strong>을 가집니다:</p>
<pre><code class="language-js">const grandparent = document.getElementById(&#39;grandparent&#39;);
console.log(grandparent.children); // HTMLCollection [&lt;p&gt;]

const parent = document.getElementById(&#39;parent&#39;);
console.log(parent.children); // HTMLCollection [&lt;span&gt;]</code></pre>
<ul>
<li><code>div</code>는 <code>p</code>만 자식으로 가짐</li>
<li><code>p</code>는 <code>span</code>만 자식으로 가짐</li>
<li>손자나 그 이하 자손은 <strong>직접적인 접근 불가</strong> → 자식을 통해 단계적으로 접근</li>
</ul>
<p>📌 DOM은 전체 트리 구조를 <strong>각 노드가 자식 목록 + 참조로 연결된 네트워크</strong> 형태로 표현합니다.</p>
<hr>
<h2 id="3-queryselector와-children의-차이-관찰-방식-vs-데이터-구조">3. <code>querySelector()</code>와 <code>.children</code>의 차이: 관찰 방식 vs 데이터 구조</h2>
<pre><code class="language-js">const body = document.querySelector(&#39;body&#39;);
console.log(body); // 콘솔에서는 계층적으로 펼쳐보임
console.log(body.children); // HTMLCollection of direct children only</code></pre>
<ul>
<li><code>querySelector()</code>는 CSS 선택자 기반으로 DOM의 특정 노드를 찾음 (도구)</li>
<li><code>.children</code>은 해당 노드가 가진 <strong>직속 자식들만을 반환</strong> (데이터)</li>
</ul>
<p>📌 콘솔에서 보이는 계층적 구조는 <strong>시각적 탐색 도구의 결과</strong>일 뿐, 실제 DOM 노드는 자식들을 1차원 목록으로 저장합니다.</p>
<hr>
<h2 id="4-dom은-왜-평평한-자식-구조를-사용하는가">4. DOM은 왜 &#39;평평한 자식 구조&#39;를 사용하는가?</h2>
<p><strong>1. 책임 분리 (Separation of Concerns)</strong></p>
<ul>
<li>각 노드는 자신의 <strong>직속 자식만</strong>을 관리함</li>
<li>손자, 증손자를 포함하는 중첩 배열을 관리하게 되면 수정, 탐색, 삭제 모두 복잡해짐</li>
</ul>
<p><strong>2. 효율적인 조작 (Efficient Mutation)</strong></p>
<ul>
<li><code>appendChild</code>, <code>removeChild</code> 같은 API가 단순해짐</li>
<li>자식 추가/삭제는 오직 직속 자식 목록만 수정하면 됨</li>
</ul>
<hr>
<h2 id="5-계층-구조는-어떻게-탐색되는가">5. 계층 구조는 어떻게 탐색되는가?</h2>
<p>DOM은 <strong>연결된 참조망</strong>을 통해 계층을 탐색합니다:</p>
<table>
<thead>
<tr>
<th align="center">방향</th>
<th align="left">프로퍼티</th>
<th align="left">의미</th>
</tr>
</thead>
<tbody><tr>
<td align="center">상위</td>
<td align="left"><code>parentNode</code></td>
<td align="left">자신의 부모 노드</td>
</tr>
<tr>
<td align="center">하위</td>
<td align="left"><code>childNodes</code>, <code>children</code></td>
<td align="left">자신의 직속 자식 노드들</td>
</tr>
<tr>
<td align="center">옆</td>
<td align="left"><code>nextSibling</code>, <code>previousSibling</code></td>
<td align="left">형제 노드</td>
</tr>
</tbody></table>
<p>렌더링 엔진은 이 참조 구조를 따라 다음처럼 트리를 순회합니다:</p>
<ol>
<li><code>&lt;html&gt;</code>부터 시작 → <code>childNodes</code>로 자식들 순회</li>
<li>자식 노드 재귀적으로 탐색 (예: <code>&lt;head&gt;</code> → <code>&lt;title&gt;</code> 등)</li>
<li>모든 자식이 끝나면 형제(<code>nextSibling</code>)로 이동</li>
</ol>
<p>🔁 이 방식으로 브라우저는 전체 DOM을 <strong>거미줄 타듯이</strong> 렌더링합니다.</p>
<hr>
<h2 id="6-react는-왜-평탄화-작업을-하는가">6. React는 왜 평탄화 작업을 하는가?</h2>
<p>React에서는 다음과 같이 중첩된 배열이 생성될 수 있습니다:</p>
<pre><code class="language-jsx">&lt;ul&gt;
  {[&lt;li&gt;1&lt;/li&gt;, [&lt;li&gt;2&lt;/li&gt;, &lt;li&gt;3&lt;/li&gt;], &lt;li&gt;4&lt;/li&gt;]}
&lt;/ul&gt;</code></pre>
<p>DOM은 이 중첩 구조를 이해하지 못합니다. <code>&lt;ul&gt;</code>에는 <code>[&lt;li&gt;, &lt;li&gt;, &lt;li&gt;, &lt;li&gt;]</code>처럼 <strong>평평한 목록</strong>만 들어올 수 있어야 합니다.</p>
<h3 id="🔧-react의-해결책-createelement--평탄화">🔧 React의 해결책: createElement + 평탄화</h3>
<p>React는 <code>createElement</code> 호출 시 자식 노드를 다음처럼 처리합니다:</p>
<pre><code class="language-js">React.createElement(&#39;ul&#39;, null, ...flatten(children));</code></pre>
<table>
<thead>
<tr>
<th>JSX 입력</th>
<th>평탄화 후</th>
<th>DOM 구조</th>
</tr>
</thead>
<tbody><tr>
<td><code>[ &lt;li&gt;1&lt;/li&gt;, [&lt;li&gt;2&lt;/li&gt;, &lt;li&gt;3&lt;/li&gt;] ]</code></td>
<td><code>[ &lt;li&gt;1&lt;/li&gt;, &lt;li&gt;2&lt;/li&gt;, &lt;li&gt;3&lt;/li&gt; ]</code></td>
<td><code>&lt;ul&gt; &lt;li/&gt; &lt;li/&gt; &lt;li/&gt; &lt;/ul&gt;</code></td>
</tr>
</tbody></table>
<p>📌 이 평탄화 덕분에 React는 DOM과 호환되며, 가상 DOM diffing도 더 단순하게 처리됩니다.</p>
<hr>
<h2 id="🔚-결론-정리">🔚 결론 정리</h2>
<ul>
<li>DOM은 개념적으로는 트리지만, <strong>데이터 구조는 평평한 자식 목록</strong>이다.</li>
<li>각 노드는 자신이 담당할 <strong>직속 자식만 관리</strong>하며, 참조 연결을 통해 전체 트리를 형성한다.</li>
<li>React는 이 구조에 맞추기 위해 자식 노드를 <strong>평탄화하여</strong> DOM이 이해할 수 있는 형태로 바꾼다.</li>
</ul>
<p>이 구조를 바탕으로 createElement 제작, 렌더링 엔진 설계의 기초를 다질 수 있습니다.(이 개념이 바로 서야 createElement 함수를 제대로 만들 수 있다.)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[📚 DOM 구조는 자식의 중첩된 구조를 갖지 않는가?]]></title>
            <link>https://velog.io/@y-minion/DOM-%EA%B5%AC%EC%A1%B0%EB%8A%94-%EC%9E%90%EC%8B%9D%EC%9D%98-%EC%A4%91%EC%B2%A9%EB%90%9C-%EA%B5%AC%EC%A1%B0%EB%A5%BC-%EA%B0%96%EC%A7%80-%EC%95%8A%EB%8A%94%EA%B0%80</link>
            <guid>https://velog.io/@y-minion/DOM-%EA%B5%AC%EC%A1%B0%EB%8A%94-%EC%9E%90%EC%8B%9D%EC%9D%98-%EC%A4%91%EC%B2%A9%EB%90%9C-%EA%B5%AC%EC%A1%B0%EB%A5%BC-%EA%B0%96%EC%A7%80-%EC%95%8A%EB%8A%94%EA%B0%80</guid>
            <pubDate>Tue, 10 Jun 2025 15:28:27 GMT</pubDate>
            <description><![CDATA[<h3 id="1-dom의-개념적-구조-중첩된-트리tree"><strong>1. DOM의 개념적 구조: 중첩된 트리(Tree)</strong></h3>
<p>HTML은 중첩을 통해 시각적 계층을 표현합니다:</p>
<pre><code class="language-html">&lt;div&gt;
  &lt;h1&gt;제목&lt;/h1&gt;
  &lt;p&gt;
    &lt;span&gt;내용&lt;/span&gt;
  &lt;/p&gt;
&lt;/div&gt;


- &lt;div&gt;는 &lt;h1&gt;, &lt;p&gt;의 부모
- &lt;p&gt;는 &lt;span&gt;의 부모
- **브라우저는 이를 트리 구조로 파싱**함
</code></pre>
<p>📌 이 구조는 부모 → 자식 → 자식의 자식으로 이어지는 <strong>계층적(트리)</strong> DOM 모델입니다.</p>
<hr>
<h3 id="2-dom의-실제-데이터-구조-평평한-자식-목록-flat-list"><strong>2. DOM의 실제 데이터 구조: 평평한 자식 목록 (Flat List)</strong></h3>
<p>하지만 DOM 내부에서는 다음과 같이 작동합니다:</p>
<pre><code class="language-jsx">const div = document.querySelector(&#39;div&#39;);
console.log(div.children); // =&gt; HTMLCollection(2) [ &lt;h1&gt;, &lt;p&gt; ]</code></pre>
<ul>
<li>div.children은 오직 직속 자식만 포함</li>
<li>span은 포함되지 않음 (손자이기 때문)</li>
<li>즉, <strong>자식 목록은 항상 1차원 배열 구조</strong></li>
</ul>
<p>❗ DOM은 2차원 배열 같은 중첩 구조를 갖지 않음</p>
<hr>
<h3 id="3-왜-react는-자식-노드를-평탄화flatten해야-하는가"><strong>3. 왜 React는 자식 노드를 평탄화(flatten)해야 하는가?</strong></h3>
<p>React는 JSX와 JS로 UI를 기술하는데, 이는 종종 중첩 배열을 만들어냅니다:</p>
<pre><code class="language-jsx">&lt;ul&gt;
  {[
    &lt;li&gt;1&lt;/li&gt;,
    [&lt;li&gt;2&lt;/li&gt;, &lt;li&gt;3&lt;/li&gt;],
    &lt;li&gt;4&lt;/li&gt;
  ]}
&lt;/ul&gt;</code></pre>
<ul>
<li>위 구조는 JS에서는 합법이지만, DOM에서는 비합법</li>
<li>DOM은 ul 태그의 자식이 <strong>평평한 리스트</strong>여야 함</li>
</ul>
<p>🔧 그래서 React는 createElement 호출 시 자식들을 <strong>자동으로 평탄화 처리</strong>합니다.</p>
<pre><code class="language-jsx">React.createElement(&#39;ul&#39;, null, ...flatten(children));</code></pre>
<p><img src="https://velog.velcdn.com/images/y-minion/post/c223d930-0866-415d-a52f-a853526e6ac1/image.png" alt=""></p>
<hr>
<h3 id="4-요약"><strong>4. 요약</strong></h3>
<ul>
<li>DOM은 개념적으로는 트리이지만, <strong>실제 자식 관리는 1차원 목록</strong></li>
<li>React는 JSX가 생성한 중첩 배열을 평탄화하여 DOM과 호환시킴</li>
<li>이 구조적 이해는 직접 렌더러나 가상돔을 구현할 때 핵심</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Virtual DOM에서 자식 목록을 평탄화(flatten)하는 이유]]></title>
            <link>https://velog.io/@y-minion/React-Virtual-DOM%EC%97%90%EC%84%9C-%EC%9E%90%EC%8B%9D-%EB%AA%A9%EB%A1%9D%EC%9D%84-%ED%8F%89%ED%83%84%ED%99%94flatten%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@y-minion/React-Virtual-DOM%EC%97%90%EC%84%9C-%EC%9E%90%EC%8B%9D-%EB%AA%A9%EB%A1%9D%EC%9D%84-%ED%8F%89%ED%83%84%ED%99%94flatten%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Tue, 10 Jun 2025 15:14:50 GMT</pubDate>
            <description><![CDATA[<p>React의 Virtual DOM이 자식 요소들을 중첩 배열이 아닌 1차원 배열로 &quot;평탄화(flatten)&quot;하는 이유는 단순히 구현상의 편의를 넘어서, <strong>성능 최적화와 예측 가능한 렌더링</strong>을 달성하기 위한 핵심 전략 중 하나입니다. 이 문서에서는 그 이유를 세 가지 측면에서 단계적으로 정리합니다.</p>
<hr>
<h2 id="1단계-빠르고-효율적인-비교diffing-algorithm를-위해">1단계: 빠르고 효율적인 비교(Diffing Algorithm)를 위해</h2>
<p>React는 상태(state)나 props가 변경되었을 때, 이전 Virtual DOM과 새로운 Virtual DOM을 비교(diff)하여 변경된 부분만 실제 DOM에 반영하는 <strong>재조정(Reconciliation)</strong> 과정을 거칩니다.</p>
<p>이 때 사용하는 알고리즘이 바로 <strong>Diffing Algorithm</strong>이며, 이는 다음 조건을 충족해야 성능이 극대화됩니다:</p>
<ul>
<li>순차적으로 빠르게 비교할 수 있어야 함</li>
<li>최소한의 재귀 사용</li>
<li><code>key</code>를 통한 변경 감지 가능</li>
</ul>
<h3 id="문제-중첩-배열-구조의-비교는-느림">문제: 중첩 배열 구조의 비교는 느림</h3>
<pre><code class="language-jsx">const children = [child1, [child2, child3], child4];
</code></pre>
<p>이런 구조라면 비교 알고리즘은 다음과 같이 복잡한 과정을 거쳐야 합니다:</p>
<ul>
<li><code>children[0]</code>은 단순 비교</li>
<li><code>children[1]</code>은 배열 → 다시 내부를 순회해야 함 (재귀 호출)</li>
<li>성능 비용 증가, 예측 어려움</li>
</ul>
<h3 id="해결-1차원-배열로-평탄화">해결: 1차원 배열로 평탄화</h3>
<pre><code class="language-jsx">const children = [child1, child2, child3, child4];
</code></pre>
<p>이렇게 만들면:</p>
<ul>
<li>처음부터 끝까지 한 번만 순차 비교 가능</li>
<li>재귀 필요 없음</li>
<li><code>key</code>를 기준으로 변경된 요소만 빠르게 탐지 가능</li>
</ul>
<blockquote>
<p>✅ 요약: 중첩 배열 → 재귀로 느림, 평탄 배열 → 순차 비교로 빠름</p>
</blockquote>
<hr>
<h2 id="2단계-jsx의-유연성과-실제-dom-구조의-간극-해소">2단계: JSX의 유연성과 실제 DOM 구조의 간극 해소</h2>
<p>JSX에서는 다음과 같은 유연한 표현이 자주 사용됩니다:</p>
<pre><code class="language-jsx">&lt;ul&gt;
  {[&lt;li&gt;첫째&lt;/li&gt;, items.map(item =&gt; &lt;li&gt;{item}&lt;/li&gt;)]}
&lt;/ul&gt;
</code></pre>
<p>이 경우 <code>children</code>은 다음처럼 중첩 배열이 됩니다:</p>
<pre><code>[ &lt;li&gt;첫째&lt;/li&gt;, [&lt;li&gt;둘째&lt;/li&gt;, &lt;li&gt;셋째&lt;/li&gt;] ]
</code></pre><p>하지만 <strong>실제 DOM 구조는 중첩 배열을 허용하지 않습니다.</strong> HTML의 부모-자식 관계는 항상 <strong>1차원 목록</strong>입니다. <code>&lt;ul&gt;</code> 태그 안에는 직속 자식 <code>&lt;li&gt;</code> 요소들이 평평하게 나열되어야 합니다.</p>
<p>React는 Virtual DOM이 실제 DOM 구조를 모방해야 하므로, 중첩 구조를 정규화(normalize)하여 <strong>평탄한 배열로 변환</strong>합니다.</p>
<blockquote>
<p>✅ 요약: JSX는 중첩 배열을 쉽게 만들지만, 실제 DOM은 평탄 구조만 허용함. 따라서 Virtual DOM도 이를 따라야 함</p>
</blockquote>
<hr>
<h2 id="3단계-가상돔-내부-로직의-단순화">3단계: 가상돔 내부 로직의 단순화</h2>
<p>Virtual DOM은 비교 외에도 여러 내부 처리를 수행합니다:</p>
<ul>
<li><code>null</code>, <code>false</code>, <code>undefined</code> 등의 필터링</li>
<li>문자열을 텍스트 노드로 변환</li>
<li><code>React.Fragment</code> 등 특수 케이스 처리</li>
</ul>
<p>이 모든 로직에서 children이 중첩 배열이면 다음과 같은 복잡한 분기문이 필요합니다:</p>
<pre><code>if (Array.isArray(child)) {
  child.forEach(recurse);
} else {
  process(child);
}
</code></pre><p>하지만 <code>createElement</code> 단계에서 이미 평탄화된 1차원 배열을 만들면:</p>
<ul>
<li>이후 모든 처리에서 <code>for (let child of children)</code> 같은 단순 루프로 처리 가능</li>
<li>재귀 로직 최소화</li>
<li>예측 가능한 동작 보장</li>
</ul>
<blockquote>
<p>✅ 요약: 미리 평탄화하면 이후 모든 처리 과정이 단순, 안정, 예측 가능</p>
</blockquote>
<hr>
<h2 id="결론-평탄화는-virtual-dom-최적화의-핵심-설계-원칙">결론: 평탄화는 Virtual DOM 최적화의 핵심 설계 원칙</h2>
<p>React의 Virtual DOM에서 자식 목록을 평탄화하는 것은 다음과 같은 React의 철학과 맞닿아 있습니다:</p>
<ul>
<li><strong>성능(Performance)</strong>: 선형 비교를 가능하게 함</li>
<li><strong>예측 가능성(Predictability)</strong>: 항상 일정한 구조로 동작함</li>
<li><strong>표준과 일치(DOM 구조에 부합)</strong>: 실제 브라우저 동작과 모순 없음</li>
<li><strong>유지보수성(Maintainability)</strong>: 내부 구현을 단순화함</li>
</ul>
<p>이를 통해 개발자는 JSX의 자유도를 누리면서도, React 내부에서는 고도로 최적화된 렌더링 파이프라인이 작동하게 됩니다.</p>
<hr>
<h2 id="참고-자료">참고 자료</h2>
<ol>
<li>[React] Virtual DOM 정리 - velog <a href="https://velog.io/@party3205/React-Virtual-DOM-%EC%A0%95%EB%A6%AC">https://velog.io/@party3205/React-Virtual-DOM-%EC%A0%95%EB%A6%AC</a></li>
<li>[React] 가상 돔(Virtual DOM)이 무엇이고 왜 중요할까요? <a href="https://yong-nyong.tistory.com/80">https://yong-nyong.tistory.com/80</a></li>
<li>리액트의 작동 원리와 장단점에 대해 2 - velog <a href="https://velog.io/@yesoryeseul/%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%9D%98-%EC%9E%91%EB%8F%99-%EC%9B%90%EB%A6%AC%EC%99%80-%EC%9E%A5%EB%8B%A8%EC%A0%90%EC%97%90-%EB%8C%80%ED%95%B4-2">https://velog.io/@yesoryeseul/%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%9D%98-%EC%9E%91%EB%8F%99-%EC%9B%90%EB%A6%AC%EC%99%80-%EC%9E%A5%EB%8B%A8%EC%A0%90%EC%97%90-%EB%8C%80%ED%95%B4-2</a></li>
<li>Virtual DOM 을 왜 빠르다고 하는가 <a href="https://dont-stay-hungry.tistory.com/3">https://dont-stay-hungry.tistory.com/3</a></li>
<li>[React] Virtual DOM과 리랜더링, 재조정 - 티스토리 <a href="https://uiop5809.tistory.com/155">https://uiop5809.tistory.com/155</a></li>
<li>[React] 가상돔 Virtual DOM이란? - 개발자 시니 - 티스토리 <a href="https://dev-cini.tistory.com/11">https://dev-cini.tistory.com/11</a></li>
<li>7주차 : 리액트에서의 Virtual DOM의 의미, 사용 이유 <a href="https://20002100.tistory.com/entry/7%EC%A3%BC%EC%B0%A8-%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%97%90%EC%84%9C%EC%9D%98-Virtual-DOM%EC%9D%98-%EC%9D%98%EB%AF%B8-%EC%82%AC%EC%9A%A9-%EC%9D%B4%EC%9C%A0">https://20002100.tistory.com/entry/7%EC%A3%BC%EC%B0%A8-%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%97%90%EC%84%9C%EC%9D%98-Virtual-DOM%EC%9D%98-%EC%9D%98%EB%AF%B8-%EC%82%AC%EC%9A%A9-%EC%9D%B4%EC%9C%A0</a></li>
<li>React의 Virtual DOM - 느릿느릿 개발자 - 티스토리 <a href="https://klmhyeonwooo.tistory.com/64">https://klmhyeonwooo.tistory.com/64</a></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[⚛️ 2주 리액트 렌더링 엔진 구현]]></title>
            <link>https://velog.io/@y-minion/2%EC%A3%BC-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%97%94%EC%A7%84-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@y-minion/2%EC%A3%BC-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%97%94%EC%A7%84-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Mon, 09 Jun 2025 07:06:22 GMT</pubDate>
            <description><![CDATA[<p>목표인 <strong>React 렌더링 엔진의 본질을 바닐라 JS로 직접 구현</strong>하기 위한</p>
<p><strong>최종 확정 커리큘럼</strong>을 아래와 같이 구성함.</p>
<hr>
<h1 id="🎯-목표"><strong>🎯 목표</strong></h1>
<blockquote>
<p>JSX를 제외한 Babel 이후의 단계부터</p>
</blockquote>
<blockquote>
<p>React의 렌더링 엔진이 실제로 수행하는 VDOM 생성, 비교(diff), DOM patching 과정을 직접 구현한다.</p>
</blockquote>
<hr>
<h1 id="✅-전체-로드맵-2주-완성형-렌더링-엔진-구현-커리큘럼"><strong>✅ 전체 로드맵: 2주 완성형 렌더링 엔진 구현 커리큘럼</strong></h1>
<h3 id="📅-구성-원칙"><strong>📅 구성 원칙</strong></h3>
<ul>
<li>총 14일 (1일 2~4시간 기준)</li>
<li><strong>역할 단위</strong>로 구성 (create → render → diff → patch → rerender)</li>
<li><strong>매 단계마다 반드시 얻어가야 할 개념 명시</strong></li>
</ul>
<hr>
<h2 id="🔷-1주차-렌더링-엔진-구조-설계-및-구현"><strong>🔷 1주차: 렌더링 엔진 구조 설계 및 구현</strong></h2>
<h3 id="✅-day-1-createelement--vdom-생성기-만들기"><strong>✅ Day 1: createElement – VDOM 생성기 만들기</strong></h3>
<ul>
<li>JSX 없이 UI 구조를 직접 JS 객체 트리로 만드는 함수 구현</li>
</ul>
<h3 id="✅-반드시-얻어야-할-개념"><strong>✅ 반드시 얻어야 할 개념</strong></h3>
<ul>
<li>VDOM이란? 왜 필요한가?</li>
<li>JSX는 어떻게 JS 객체로 바뀌는가?</li>
<li>VDOM의 기본 구조: { type, props, children }</li>
</ul>
<hr>
<h3 id="✅-day-2-render--vdom을-실제-dom으로-렌더링"><strong>✅ Day 2: render – VDOM을 실제 DOM으로 렌더링</strong></h3>
<ul>
<li>VDOM을 순회하며 실제 DOM 노드 생성</li>
<li>텍스트 노드 처리 (TEXT_ELEMENT)</li>
<li>속성 처리 (props, 이벤트 등)</li>
</ul>
<h3 id="✅-반드시-얻어야-할-개념-1"><strong>✅ 반드시 얻어야 할 개념</strong></h3>
<ul>
<li>선언형 UI → 명령형 DOM 조작 방식으로 어떻게 연결되는가</li>
<li>재귀적으로 트리를 순회하며 렌더링하는 패턴</li>
<li>속성과 이벤트 바인딩의 실제 작동 방식</li>
</ul>
<hr>
<h3 id="✅-day-3-수동-리렌더-시뮬레이션"><strong>✅ Day 3: 수동 리렌더 시뮬레이션</strong></h3>
<ul>
<li>기존 vdom1을 render</li>
<li>새로운 vdom2을 render</li>
<li>변화되는 부분 파악 (diff 도입 전 단계)</li>
</ul>
<h3 id="✅-반드시-얻어야-할-개념-2"><strong>✅ 반드시 얻어야 할 개념</strong></h3>
<ul>
<li>DOM 전체를 다시 그리는 것의 한계</li>
<li>“무엇이 바뀌었는지”를 추적해야 하는 이유</li>
</ul>
<hr>
<h3 id="✅-day-4-diffold-new--vdom-비교-알고리즘"><strong>✅ Day 4: diff(old, new) – VDOM 비교 알고리즘</strong></h3>
<ul>
<li>type이 다르면 replace</li>
<li>props가 다르면 update</li>
<li>자식 노드는 재귀적으로 비교</li>
</ul>
<h3 id="✅-반드시-얻어야-할-개념-3"><strong>✅ 반드시 얻어야 할 개념</strong></h3>
<ul>
<li>Virtual DOM diffing의 설계 철학</li>
<li>shallow comparison vs deep comparison</li>
<li>왜 key 기반 비교가 필요한가 (향후 확장을 위한 베이스)</li>
</ul>
<hr>
<h3 id="✅-day-5-patch--최소-dom-수정-로직-구현"><strong>✅ Day 5: patch – 최소 DOM 수정 로직 구현</strong></h3>
<ul>
<li>diff 결과에 따라 insert / remove / update 수행</li>
<li>텍스트 교체, 속성 갱신, 노드 삭제</li>
</ul>
<h3 id="✅-반드시-얻어야-할-개념-4"><strong>✅ 반드시 얻어야 할 개념</strong></h3>
<ul>
<li>DOM 변경은 비용이 크다는 점</li>
<li>“최소 DOM 변경”을 위한 의사결정 구조</li>
<li>patch가 render와 완전히 다른 책임을 가진 이유</li>
</ul>
<hr>
<h3 id="✅-day-6-테스트-시나리오-제작"><strong>✅ Day 6: 테스트 시나리오 제작</strong></h3>
<ul>
<li>카운터 UI: 버튼 클릭 → 수동 상태 변경 → render 실행</li>
<li>DOM 변경 여부 로그 출력</li>
</ul>
<h3 id="✅-반드시-얻어야-할-개념-5"><strong>✅ 반드시 얻어야 할 개념</strong></h3>
<ul>
<li>렌더링 로직의 검증 방법</li>
<li>상태 기반 렌더링과 VDOM의 연결 구조 테스트하기</li>
</ul>
<hr>
<h3 id="✅-day-7-render-흐름-단위-분리-reconcile--commit"><strong>✅ Day 7: render 흐름 단위 분리 (reconcile + commit)</strong></h3>
<ul>
<li>render = reconcile + commit</li>
<li>재귀 로직 분리, 역할 구분</li>
</ul>
<h3 id="✅-반드시-얻어야-할-개념-6"><strong>✅ 반드시 얻어야 할 개념</strong></h3>
<ul>
<li>React가 render와 commit phase를 나누는 이유</li>
<li>트리 순회 + 실제 DOM 작업의 분리 구조 이해</li>
</ul>
<hr>
<h2 id="🔷-2주차-상태-변화-흐름과-동적-리렌더링-완성"><strong>🔷 2주차: 상태 변화 흐름과 동적 리렌더링 완성</strong></h2>
<h3 id="✅-day-8-usestate--상태-저장-및-rerender-연결"><strong>✅ Day 8: useState – 상태 저장 및 rerender 연결</strong></h3>
<ul>
<li>상태 저장 구조 (hooks 배열)</li>
<li>setState → 해당 컴포넌트 rerender 트리거</li>
</ul>
<h3 id="✅-반드시-얻어야-할-개념-7"><strong>✅ 반드시 얻어야 할 개념</strong></h3>
<ul>
<li>상태 변경 → render 재실행 흐름</li>
<li>hook index를 기반으로 상태 매핑하는 구조</li>
<li>렌더 트리거와 diff 연결</li>
</ul>
<hr>
<h3 id="✅-day-9-rerendercomponent-구조-설계"><strong>✅ Day 9: rerender(Component) 구조 설계</strong></h3>
<ul>
<li>상태 변경된 컴포넌트만 새 VDOM 생성</li>
<li>이전 VDOM과 비교 → DOM patch</li>
</ul>
<h3 id="✅-반드시-얻어야-할-개념-8"><strong>✅ 반드시 얻어야 할 개념</strong></h3>
<ul>
<li>컴포넌트 단위의 독립적 리렌더링 구조</li>
<li>React가 전체 앱을 다시 그리지 않는 원리</li>
<li>렌더 엔진의 부분 업데이트 설계</li>
</ul>
<hr>
<h3 id="✅-day-10-useeffect--side-effect-구조"><strong>✅ Day 10: useEffect – side effect 구조</strong></h3>
<ul>
<li>렌더 이후 effect 실행</li>
<li>deps 배열 비교</li>
</ul>
<h3 id="✅-반드시-얻어야-할-개념-9"><strong>✅ 반드시 얻어야 할 개념</strong></h3>
<ul>
<li>effect는 DOM 작업이 끝난 뒤 실행되어야 함</li>
<li>deps 배열로 성능 최적화</li>
<li>mount vs update 판단 기준</li>
</ul>
<hr>
<h3 id="✅-day-11-key-기반-리스트-diff-선택-과제"><strong>✅ Day 11: key 기반 리스트 diff (선택 과제)</strong></h3>
<ul>
<li>자식 배열 비교에 key 적용</li>
<li>insert/remove 최적화</li>
</ul>
<h3 id="✅-반드시-얻어야-할-개념-10"><strong>✅ 반드시 얻어야 할 개념</strong></h3>
<ul>
<li>key의 필요성 (순서 변경 vs 내용 변경)</li>
<li>리스트 재조합이 diff에 미치는 영향</li>
</ul>
<hr>
<h3 id="✅-day-12-간단한-scheduler-도입-선택-과제"><strong>✅ Day 12: 간단한 scheduler 도입 (선택 과제)</strong></h3>
<ul>
<li>batching: setTimeout or requestIdleCallback</li>
<li>render 큐 처리</li>
</ul>
<h3 id="✅-반드시-얻어야-할-개념-11"><strong>✅ 반드시 얻어야 할 개념</strong></h3>
<ul>
<li>React가 “지금 바로 렌더링 안 하는 이유”</li>
<li>연산 분할과 성능 최적화 구조</li>
</ul>
<hr>
<h3 id="✅-day-13-통합-시나리오-테스트"><strong>✅ Day 13: 통합 시나리오 테스트</strong></h3>
<ul>
<li>Counter</li>
<li>TodoList</li>
<li>조건부 렌더링</li>
</ul>
<h3 id="✅-반드시-얻어야-할-개념-12"><strong>✅ 반드시 얻어야 할 개념</strong></h3>
<ul>
<li>전체 렌더링 흐름을 손으로 trace 가능해야 함</li>
<li>상태 → VDOM → diff → patch의 완전한 연결</li>
</ul>
<hr>
<h3 id="✅-day-14-문서화-및-리팩터링"><strong>✅ Day 14: 문서화 및 리팩터링</strong></h3>
<ul>
<li>/core/ 디렉토리 정리</li>
<li>흐름도 그리기</li>
<li>README.md: 구현 목표, 핵심 개념, 구현 방법 정리</li>
</ul>
<h3 id="✅-반드시-얻어야-할-개념-13"><strong>✅ 반드시 얻어야 할 개념</strong></h3>
<ul>
<li>내가 만든 구조를 누가 봐도 이해 가능하도록 설명할 수 있어야 함</li>
<li>문서화를 통해 전체 설계 감각 정리</li>
</ul>
<hr>
<h2 id="📁-예상-디렉토리-구조"><strong>📁 예상 디렉토리 구조</strong></h2>
<pre><code>/react-render-core/
├── index.html
├── main.js
├── core/
│   ├── createElement.js
│   ├── render.js
│   ├── diff.js
│   ├── patch.js
│   ├── state.js          ← useState 구현
│   ├── effect.js         ← useEffect 구현
│   └── scheduler.js      ← 선택
├── components/
│   ├── App.js
│   └── Counter.js
├── README.md</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[📘 React에서 비동기 로직과 useEffect를 사용하는 이유와 원칙
]]></title>
            <link>https://velog.io/@y-minion/React%EC%97%90%EC%84%9C-%EB%B9%84%EB%8F%99%EA%B8%B0-%EB%A1%9C%EC%A7%81%EA%B3%BC-useEffect%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0%EC%99%80-%EC%9B%90%EC%B9%99</link>
            <guid>https://velog.io/@y-minion/React%EC%97%90%EC%84%9C-%EB%B9%84%EB%8F%99%EA%B8%B0-%EB%A1%9C%EC%A7%81%EA%B3%BC-useEffect%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0%EC%99%80-%EC%9B%90%EC%B9%99</guid>
            <pubDate>Tue, 13 May 2025 06:05:12 GMT</pubDate>
            <description><![CDATA[<h2 id="🧠-1-왜-useeffect가-필요한가">🧠 1. 왜 useEffect가 필요한가?</h2>
<p>React는 렌더링을 아주 빠르고 예측 가능하게 유지하기 위해, 다음의 원칙을 강하게 지킨다:</p>
<p>“컴포넌트 함수는 순수해야 한다.”</p>
<p>🔍 순수함이란?
    •    props와 state가 같으면 항상 같은 결과를 반환해야 한다.
    •    컴포넌트 안에서 API 요청, 타이머 설정, 콘솔 출력, DOM 조작 등을 직접 하면 안 된다.</p>
<p>⸻</p>
<h2 id="🚫-2-비동기-로직을-컴포넌트-함수-밖에서-실행하면-안-되는-이유">🚫 2. 비동기 로직을 컴포넌트 함수 밖에서 실행하면 안 되는 이유</h2>
<p>❌ 예시 (잘못된 코드)</p>
<pre><code>function MyComponent() {
  const res = await fetch(&#39;/api&#39;); // ❌ 오류 발생
  return &lt;div&gt;{res}&lt;/div&gt;;
}</code></pre><p>문제점
    •    React는 MyComponent()를 호출해서 즉시 결과(JSX)를 받아야 하는데, await 때문에 멈춘다.
    •    React는 렌더링 중 Promise를 기다릴 수 없다.
→ 결과적으로 오류가 발생하거나, 예측 불가능한 동작이 발생함.</p>
<p>⸻</p>
<h2 id="✅-3-그래서-등장한-useeffect">✅ 3. 그래서 등장한 useEffect</h2>
<pre><code>function MyComponent() {
  const [data, setData] = useState(null);

  useEffect(() =&gt; {
    const fetchData = async () =&gt; {
      const res = await fetch(&#39;/api&#39;);
      const json = await res.json();
      setData(json);
    };
    fetchData();
  }, []);

  return &lt;div&gt;{data ? data.title : &quot;로딩 중...&quot;}&lt;/div&gt;;
}</code></pre><p>✅ 이 방식이 올바른 이유
    •    useEffect()는 렌더링이 끝난 후에 실행된다.
    •    React의 렌더링 흐름을 방해하지 않고, 안전하게 비동기 로직을 실행할 수 있다.
    •    setData()가 실행되면 상태가 바뀌고, 컴포넌트는 다시 렌더링된다.</p>
<p>⸻</p>
<h2 id="🔄-4-언제-useeffect-안에-async-함수를-넣어야-하나">🔄 4. 언제 useEffect 안에 async 함수를 넣어야 하나?</h2>
<p><img src="https://velog.velcdn.com/images/y-minion/post/89e6d14b-ceef-4355-81ce-0384c1ac5890/image.png" alt=""></p>
<p>⸻</p>
<h2 id="🧠-5-비동기-로직은-언제든지-useeffect-안에서-선언">🧠 5. 비동기 로직은 언제든지 useEffect 안에서 선언</h2>
<pre><code>useEffect(() =&gt; {
  async function fetchData() {
    const res = await fetch(url);
    const data = await res.json();
    setData(data);
  }
  fetchData();
}, [url]);
</code></pre><p>주의: useEffect 자체에 async 키워드를 붙이면 안 된다.
이유는 useEffect가 Promise를 반환하면 cleanup 함수를 구분하지 못하기 때문이다.</p>
<p>⸻</p>
<h2 id="💡-6-심화-개념-요약">💡 6. 심화 개념 요약</h2>
<p>•    React는 동기 렌더링을 강제한다.
→ 컴포넌트 함수는 순수하게 동작해야 함.
    •    useEffect는 렌더링 이후에 비동기 코드나 부수효과를 실행하기 위한 안전한 공간.
    •    컴포넌트 외부에서 fetch하거나 await을 쓰면 렌더링을 멈추게 되어 오류가 생긴다.
    •    React는 비동기를 기다리지 않는다. 로딩 상태를 먼저 보여주고, 데이터 도착 후 다시 렌더링하는 방식이 UX적으로도 더 좋다.</p>
<p>⸻</p>
<p>📚 참고자료
    •    React 공식문서 - useEffect
    •    Dan Abramov 블로그 - A Complete Guide to useEffect
    •    React 공식 가이드 - 데이터 가져오기</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React]📘 ProgressBar 렌더링 리팩터링]]></title>
            <link>https://velog.io/@y-minion/ProgressBar-%EB%A0%8C%EB%8D%94%EB%A7%81-%EB%A6%AC%ED%8C%A9%ED%84%B0%EB%A7%81</link>
            <guid>https://velog.io/@y-minion/ProgressBar-%EB%A0%8C%EB%8D%94%EB%A7%81-%EB%A6%AC%ED%8C%A9%ED%84%B0%EB%A7%81</guid>
            <pubDate>Thu, 10 Apr 2025 02:35:34 GMT</pubDate>
            <description><![CDATA[<hr>
<h2 id="배경">배경</h2>
<p>→ 기존에는 <strong>width를 $prop 으로 직접 조절했으나 ❗️</strong>layout 변화 발생&amp;리플로우 발생❗️</p>
<p>이를 해결 하고자 리팩토링 실행</p>
<h2 id="들어가기에-앞서">들어가기에 앞서…</h2>
<h3 id="✅-scalex란"><strong>✅ scaleX()란?</strong></h3>
<blockquote>
<p>transform: scaleX(n)은</p>
<p><strong>요소의 가로 길이(width)를 n배로 확대/축소</strong></p>
</blockquote>
<p>•    n = 1 → 원래 크기</p>
<p>•    n = 0.5 → 가로 길이가 절반</p>
<p>•    n = 2 → 가로 길이 2배</p>
<p>•    n = 0 → 가로 길이 0 (안 보임)</p>
<hr>
<h3 id="✅-시각적으로-보면"><strong>✅ 시각적으로 보면:</strong></h3>
<pre><code class="language-css">transform: scaleX(0.5);</code></pre>
<p>이건 해당 요소를 <strong>왼쪽 기준으로 가로만 50%로 줄이는 효과.</strong></p>
<p>✔️ <strong>중요한 점: 실제 width를 바꾸는 게 아니라 “보여지는 크기”만 바뀜</strong></p>
<p>→ 그래서 GPU(그래픽 가속)로 애니메이션이 훨씬 부드럽다.</p>
<hr>
<h3 id="✅-왜-width보다-좋냐"><strong>✅ 왜 width보다 좋냐?</strong></h3>
<table>
<thead>
<tr>
<th><strong>기준</strong></th>
<th>width</th>
<th>transform: scaleX()</th>
</tr>
</thead>
<tbody><tr>
<td>동작</td>
<td>layout 변화 발생</td>
<td>layout 변화 없음</td>
</tr>
<tr>
<td>리플로우</td>
<td>생김 (느림)</td>
<td>없음 (빠름)</td>
</tr>
<tr>
<td>성능</td>
<td>CPU 기반</td>
<td>GPU 가속</td>
</tr>
<tr>
<td>부드러움</td>
<td>중간</td>
<td>✅ 훨씬 부드러움</td>
</tr>
<tr>
<td>추천</td>
<td>간단할 땐 OK</td>
<td>고성능 UI엔 강력 추천</td>
</tr>
</tbody></table>
<hr>
<h3 id="📦-결론"><strong>📦 결론</strong></h3>
<blockquote>
<p>scaleX()는 눈에 보이는</p>
<p><strong>길이만 축소/확대</strong></p>
</blockquote>
<blockquote>
<p>애니메이션 최적화할 때 자주 쓰이는 핵심 속성.</p>
</blockquote>
<hr>
<h2 id="♻️리팩토링">♻️리팩토링</h2>
<p>현재 코드에서 ProgressBar를 transform: scaleX() 기반으로 개선한 내용과,</p>
<p>기존 width 방식과의 차이점, 장단점, 최종적으로 왜 이렇게 바꿨는지를 포함한 문서.</p>
<h3 id="✅-목표"><strong>✅ 목표</strong></h3>
<p>기존 width 기반 진행바에서</p>
<p><strong>더 부드럽고 성능 좋은 transform: scaleX() 기반 진행바</strong>로 개선함.</p>
<p>이 문서는 해당 리팩터링의 <strong>동기, 기술 개념, 차이점, 장점, 주의사항</strong>을 모두 문서화한 자료다.</p>
<hr>
<h3 id="🎯-개선-목적"><strong>🎯 개선 목적</strong></h3>
<table>
<thead>
<tr>
<th><strong>기존 방식 (</strong>width<strong>)</strong></th>
<th><strong>문제점</strong></th>
</tr>
</thead>
<tbody><tr>
<td>width: ${progress}%</td>
<td>브라우저 레이아웃(Layout Reflow) 발생 가능성</td>
</tr>
<tr>
<td>transition 적용 시 살짝 끊김</td>
<td>부드럽지 않음, 애니메이션 성능 떨어짐</td>
</tr>
<tr>
<td>DOM 사이즈 직접 조절</td>
<td>브라우저 연산 비용 큼 (CPU)</td>
</tr>
</tbody></table>
<hr>
<h3 id="✅-개선된-방식-transform-scalex"><strong>✅ 개선된 방식: transform: scaleX()</strong></h3>
<p><strong>✅ 개념 요약</strong></p>
<pre><code class="language-css">transform: scaleX(n);</code></pre>
<p>•    요소의 <strong>가로 방향 스케일</strong>을 조절</p>
<p>•    n = 0 → 가로 길이 0%</p>
<p>•    n = 1 → 가로 길이 100%</p>
<p>•    n = 0.5 → 가로 길이 50%</p>
<hr>
<p><strong>✅ 추가 속성</strong></p>
<pre><code class="language-css">transform-origin: left;</code></pre>
<p>•    왼쪽을 기준으로 커지거나 줄어듦</p>
<p>•    진행 바처럼 <strong>왼쪽 → 오른쪽</strong> 자연스러운 확장 가능</p>
<hr>
<p><strong>🔍 전체 리팩터링 코드</strong></p>
<pre><code class="language-jsx">const ProgressBarWrapper = styled.div`
  position: absolute;
  bottom: 0;
  left: 0;
  height: 100%;
  width: 100%;
  z-index: 1;
`;

const ProgressFill = styled.div`
  height: 100%;
  width: 100%;
  background-color: ${({ theme }) =&gt; theme.surface.brandDefault};
  transform-origin: left;
  transform: scaleX(${({ $progress }) =&gt; $progress / 100});
  transition: transform 0.5s linear;
  will-change: transform;
  opacity: 0.8;
`;</code></pre>
<pre><code class="language-jsx">return (
  &lt;ProgressBarWrapper&gt;
    &lt;ProgressFill $progress={progress} /&gt;
  &lt;/ProgressBarWrapper&gt;
);</code></pre>
<hr>
<h3 id="🆚-기존-방식과-개선-포인트-비교"><strong>🆚 기존 방식과 개선 포인트 비교</strong></h3>
<table>
<thead>
<tr>
<th><strong>항목</strong></th>
<th><strong>기존 방식 (</strong>width<strong>)</strong></th>
<th><strong>개선 방식 (</strong>scaleX<strong>)</strong></th>
</tr>
</thead>
<tbody><tr>
<td>렌더링 성능</td>
<td>CPU 중심</td>
<td>✅ GPU 중심 (transform)</td>
</tr>
<tr>
<td>애니메이션</td>
<td>끊김 있음</td>
<td>✅ 매우 부드러움</td>
</tr>
<tr>
<td>DOM layout 영향</td>
<td>layout 재계산 발생</td>
<td>✅ layout 영향 없음</td>
</tr>
<tr>
<td>리소스 비용</td>
<td>높음</td>
<td>✅ 낮음</td>
</tr>
<tr>
<td>CSS transition</td>
<td>width transition</td>
<td>transform transition (더 부드러움)</td>
</tr>
<tr>
<td>시각적 제어</td>
<td>비교적 직관적</td>
<td>초기 학습 필요하지만 효과 우수</td>
</tr>
</tbody></table>
<hr>
<h3 id="📌-실전에서-주의할-점"><strong>📌 실전에서 주의할 점</strong></h3>
<table>
<thead>
<tr>
<th><strong>항목</strong></th>
<th><strong>설명</strong></th>
</tr>
</thead>
<tbody><tr>
<td>transform-origin</td>
<td>반드시 left로 설정해야 진행바처럼 동작</td>
</tr>
<tr>
<td>transition</td>
<td>transform에만 적용해야 자연스러움</td>
</tr>
<tr>
<td>width: 100% 유지</td>
<td>스케일 조정이기 때문에 실제 박스는 항상 100%여야 함</td>
</tr>
<tr>
<td>will-change: transform</td>
<td>브라우저에 사전 힌트 줘서 성능 향상</td>
</tr>
</tbody></table>
<hr>
<h3 id="🧠-왜-이렇게-바꿨는가"><strong>🧠 왜 이렇게 바꿨는가?</strong></h3>
<p>•    width 기반은 간단하지만, 부드럽지 않고 성능 최적화 어려움</p>
<p>•    transform 기반은 GPU 가속 → 프레임 드랍 없이 부드러움</p>
<p>•    특히 저사양 디바이스, 모바일에서 체감 차이 큼</p>
<p>•    시각적으로도 훨씬 매끄러운 UX 제공 가능</p>
<hr>
<h3 id="✅-최종-요약"><strong>✅ 최종 요약</strong></h3>
<table>
<thead>
<tr>
<th><strong>내용</strong></th>
<th><strong>설명</strong></th>
</tr>
</thead>
<tbody><tr>
<td>사용 속성</td>
<td>transform: scaleX(), transform-origin: left</td>
</tr>
<tr>
<td>성능</td>
<td>✅ 부드럽고 최적화됨</td>
</tr>
<tr>
<td>쓰임</td>
<td>진행바, 로딩바, 그래픽 시각화</td>
</tr>
<tr>
<td>리액트 연동 방식</td>
<td>상태값 → 스케일 비율로 반영</td>
</tr>
</tbody></table>
]]></description>
        </item>
    </channel>
</rss>