<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>jaemin_ve.log</title>
        <link>https://velog.io/</link>
        <description>안녕하세요</description>
        <lastBuildDate>Wed, 11 Mar 2026 06:35:25 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>jaemin_ve.log</title>
            <url>https://velog.velcdn.com/images/jaemin_ve/profile/8d008563-0d0d-46ba-820d-6038249a5480/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. jaemin_ve.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jaemin_ve" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[스펙주도개발 + TDD로 서비스 구축하기]]></title>
            <link>https://velog.io/@jaemin_ve/%EC%8A%A4%ED%8E%99%EC%A3%BC%EB%8F%84%EA%B0%9C%EB%B0%9C-TDD%EB%A1%9C-AI-%EB%8D%94%EB%B9%99-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jaemin_ve/%EC%8A%A4%ED%8E%99%EC%A3%BC%EB%8F%84%EA%B0%9C%EB%B0%9C-TDD%EB%A1%9C-AI-%EB%8D%94%EB%B9%99-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 11 Mar 2026 06:35:25 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jaemin_ve/post/646853a3-8321-4fc8-a7c5-dd6106f87e37/image.png" alt=""></p>
<h2 id="시작하며-두-번째-도전">시작하며: 두 번째 도전</h2>
<p>저는 <strong>YAPP</strong>(국내 IT 연합동아리)에서 진행한 <strong>모잇이</strong> 프로젝트에서 처음으로 spec-kit을 도입해봤습니다. 하지만 솔직히 말하면 결과가 만족스럽지 않았습니다.</p>
<p>명세 문서는 작성했지만 테스트 없이 구현만 뒤따랐고, 에이전트는 종종 컨텍스트를 잃었으며, &quot;스펙과 코드가 일치하는가?&quot;를 검증할 수단이 없었습니다. 처음 도입하는 것이라 워크플로우에 익숙하지 않은 탓도 있었지만, 근본적으로 <strong>스펙주도개발만으로는 부족했습니다.</strong> 에이전트가 명세를 읽고 코드를 쓰더라도, 그 코드가 명세를 실제로 만족하는지 자동으로 증명할 방법이 없었습니다.</p>
<p>이번 프로젝트에서는 <strong>TDD(테스트 주도 개발)를 결합</strong>했습니다. 모잇이에서 배운 교훈을 바탕으로, 명세의 Acceptance Scenario가 곧 테스트 케이스가 되고, 테스트가 통과하기 전까지 태스크가 완료되지 않는 구조를 만들었습니다.</p>
<p>이 글은 그 두 번째 도전의 기록입니다.</p>
<hr>
<h2 id="바이브-코딩의-함정">바이브 코딩의 함정</h2>
<p>AI 코딩 에이전트를 처음 쓰면 이런 흐름이 됩니다.</p>
<pre><code class="language-text">&quot;AI야, 로그인 기능 만들어줘&quot;
  → AI가 코드를 만들어줌
  → &quot;어, 이건 내가 원한 게 아닌데?&quot;
  → &quot;OAuth로 만들어줘&quot;
  → AI가 다시 만들어줌
  → &quot;Google 로그인이어야 해&quot;
  → &quot;화이트리스트도 있어야 해&quot;
  → ...</code></pre>
<p>막연한 프롬프트를 던지고 나온 코드를 수정하는 반복. 이걸 업계에서는 <strong>&quot;바이브 코딩(Vibe Coding)&quot;</strong>이라고 부릅니다. 결과물이 나오긴 하지만, 아키텍처는 일관성이 없고, 에이전트는 컨텍스트를 잃어가고, 코드 리뷰는 수천 줄을 한꺼번에 봐야 합니다.</p>
<p>GitHub이 이 문제를 해결하기 위해 2025년 오픈소스로 공개한 것이 <strong><a href="https://github.com/github/spec-kit">spec-kit</a></strong>입니다.</p>
<hr>
<h2 id="스펙주도개발spec-driven-development이란">스펙주도개발(Spec-Driven Development)이란?</h2>
<p>스펙주도개발의 핵심 철학은 하나입니다.</p>
<blockquote>
<p><strong>&quot;코드를 쓰기 전에 먼저 명확하게 정의하라.&quot;</strong></p>
</blockquote>
<p>전통적인 AI 보조 개발은 AI가 요구사항을 추측해야 합니다. 스펙주도개발은 그 추측을 없앱니다. 명세(Spec)가 계약서가 되고, 그 계약서를 AI가 실행합니다.</p>
<p>GitHub의 공식 블로그에서는 이렇게 표현합니다:</p>
<blockquote>
<p><em>&quot;AI makes specifications executable, fundamentally changing how intent becomes code.&quot;</em>
— <a href="https://github.blog/ai-and-ml/generative-ai/spec-driven-development-with-ai-get-started-with-a-new-open-source-toolkit/">GitHub Blog: Spec-driven development with AI</a></p>
</blockquote>
<h3 id="바이브-코딩-vs-스펙주도개발-비교">바이브 코딩 vs 스펙주도개발 비교</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>바이브 코딩</th>
<th>스펙주도개발</th>
</tr>
</thead>
<tbody><tr>
<td>시작점</td>
<td>막연한 아이디어</td>
<td>검증된 명세 문서</td>
</tr>
<tr>
<td>AI의 역할</td>
<td>요구사항을 추측</td>
<td>명세를 실행</td>
</tr>
<tr>
<td>코드 리뷰</td>
<td>수천 줄 한꺼번에</td>
<td>작은 태스크 단위</td>
</tr>
<tr>
<td>아키텍처 일관성</td>
<td>기능마다 달라짐</td>
<td>헌법(Constitution)으로 강제</td>
</tr>
<tr>
<td>변경사항 추적</td>
<td>히스토리 파악 어려움</td>
<td>spec.md에 변경 이유 기록</td>
</tr>
</tbody></table>
<hr>
<h2 id="github-spec-kit을-claude-code-커맨드로--speckits">GitHub spec-kit을 Claude Code 커맨드로 — Speckits</h2>
<p>spec-kit은 Claude Code, GitHub Copilot, Gemini CLI, Cursor 등 다양한 AI 에이전트를 지원합니다. 저는 Claude Code 환경에서 사용하기 위해 spec-kit의 워크플로우를 Claude Code 슬래시 커맨드로 포팅한 <strong>Speckits</strong>를 만들었습니다.
<img src="https://velog.velcdn.com/images/jaemin_ve/post/d7a0eeda-28dc-46bb-ba13-6388a09a470c/image.png" alt=""></p>
<pre><code class="language-text">/speckits:specify  →  /speckits:plan  →  /speckits:tasks  →  /speckits:implement
      ①                    ②                   ③                     ④
  기능 명세 작성       구현 계획 수립       실행 태스크 생성        코드 구현</code></pre>
<p>각 커맨드는 <code>specs/feat/NNN-feature-name/</code> 디렉토리에 문서 산출물을 자동으로 생성합니다.</p>
<hr>
<h2 id="무엇을-만들었나-ai-더빙-서비스">무엇을 만들었나: AI 더빙 서비스</h2>
<p>이 워크플로우를 적용해 만든 프로젝트를 소개합니다.</p>
<p><strong>오디오/비디오 파일 업로드 → 원하는 언어로 자동 더빙</strong>해주는 Next.js 웹 서비스입니다.</p>
<pre><code class="language-text">오디오/비디오 업로드
    ↓
STT (ElevenLabs) — 음성을 텍스트로 변환
    ↓
번역 (Google Gemini) — 타겟 언어로 번역
    ↓
TTS (ElevenLabs) — 번역된 텍스트를 음성으로 합성
    ↓
더빙 결과 재생 / 다운로드</code></pre>
<p>기술 스택은 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th>분류</th>
<th>기술</th>
</tr>
</thead>
<tbody><tr>
<td>프레임워크</td>
<td>Next.js 16 (App Router), React 19, TypeScript 5</td>
</tr>
<tr>
<td>스타일링</td>
<td>Tailwind CSS 4</td>
</tr>
<tr>
<td>인증</td>
<td>Auth.js v5 (Google OAuth + 화이트리스트)</td>
</tr>
<tr>
<td>데이터베이스</td>
<td>Turso (libSQL)</td>
</tr>
<tr>
<td>AI API</td>
<td>ElevenLabs (STT/TTS), Google Gemini (번역)</td>
</tr>
<tr>
<td>배포</td>
<td>Vercel (GitHub 푸시 → 자동 배포)</td>
</tr>
<tr>
<td>아키텍처</td>
<td>FSD (Feature-Sliced Design)</td>
</tr>
</tbody></table>
<hr>
<h2 id="speckits-워크플로우-실전-적용-ai-더빙-코어-기능-구현">Speckits 워크플로우 실전 적용: AI 더빙 코어 기능 구현</h2>
<p>말보다 실제 예시가 설득력 있습니다. AI 더빙 파이프라인(STT → 번역 → TTS)을 구현한 과정을 단계별로 살펴보겠습니다.</p>
<h3 id="step-1-speckitsspecify--무엇을과-왜를-먼저-정의">Step 1. <code>/speckits:specify</code> — &quot;무엇을&quot;과 &quot;왜&quot;를 먼저 정의</h3>
<p>Claude Code에 이렇게 입력했습니다:</p>
<pre><code class="language-bash">/speckits:specify 오디오/비디오 파일을 업로드하면 원하는 언어로 더빙해주는 기능</code></pre>
<p>커맨드가 실행되면 에이전트가 <strong>자동으로 모호한 부분을 질문</strong>합니다. 개발자가 구현부터 시작했다면 나중에 발견했을 엣지 케이스를 사전에 드러냅니다.</p>
<pre><code class="language-text">❓ 명확화가 필요한 항목 (자동 생성):

Q1. 번역 AI를 어떤 서비스로 사용할까요?
   → A: Google Gemini API (무료 플랜)

Q2. 비디오 파일은 서버에서 오디오를 추출해야 하나요?
   → A: ElevenLabs STT가 mp4/mov/webm 직접 지원 → ffmpeg 불필요

Q3. 더빙 결과를 저장/이력 관리해야 하나요?
   → A: 세션 내 유지만, 페이지 떠나면 사라짐

Q4. 지원 언어 범위는?
   → A: 한국어 + 영어 2개</code></pre>
<p>이 질문에 답하고 나면 <code>spec.md</code>가 자동 생성됩니다. <strong>구현 기술 없이 &quot;무엇을 만들지&quot;만 기술</strong>하는 것이 spec의 원칙입니다.</p>
<pre><code class="language-markdown"># Feature Specification: AI 더빙 코어 기능

## User Story 2 - 음성 전사(STT)

**Acceptance Scenarios**:

1. Given 사용자가 유효한 오디오 파일을 업로드하고 더빙을 요청,
   When STT 처리 진행 중,
   Then 시스템이 &quot;음성을 텍스트로 변환 중...&quot; 상태를 표시한다.

2. Given STT API 오류 발생,
   When 전사 실패,
   Then 시스템이 &quot;음성 인식에 실패했습니다&quot; 메시지와
   재시도 옵션을 표시한다.

## Edge Cases (자동 식별)

- 무음 파일: &quot;음성을 감지하지 못했습니다&quot; 메시지
- API 할당량 초과: &quot;크레딧이 부족합니다&quot; 차단
- 동일 언어 선택: Gemini 호출 없이 TTS로 바로 진행 (최적화)</code></pre>
<p>Given/When/Then 형식 덕분에 구현 완료 후 &quot;이게 맞게 동작하는 건가?&quot; 의문이 생겼을 때 spec을 기준으로 즉시 판단할 수 있습니다.</p>
<hr>
<h3 id="step-2-speckitsplan--어떻게를-기술-관점에서-결정">Step 2. <code>/speckits:plan</code> — &quot;어떻게&quot;를 기술 관점에서 결정</h3>
<pre><code class="language-bash">/speckits:plan</code></pre>
<p>plan.md는 구현 기술 결정과 그 이유를 기록합니다. 단순히 &quot;이렇게 만들겠다&quot;가 아니라 <strong>왜 그 기술을 선택했는지</strong>까지 남깁니다.</p>
<p>여기서 독특한 기능이 등장합니다. <strong>Constitution Check</strong>(아키텍처 원칙 준수 검증)입니다:</p>
<pre><code class="language-markdown">## Constitution Check

| Gate               | Status  | Notes                                                                 |
| ------------------ | ------- | --------------------------------------------------------------------- |
| FSD 레이어 준수    | ✅ PASS | Route Handlers(app), 파이프라인 훅(features), API 함수(entities) 분리 |
| 배럴 패턴 금지     | ✅ PASS | 모든 import는 개별 파일 경로 사용                                     |
| 단방향 의존성      | ✅ PASS | shared → entities → features → app                                    |
| 환경변수 서버 전용 | ✅ PASS | NEXT*PUBLIC* 없음, shared/config/env.ts 경유                          |</code></pre>
<p>&quot;Constitution&quot;은 다음 섹션에서 자세히 설명합니다. 이것이 아키텍처 일관성을 유지하는 핵심입니다.</p>
<hr>
<h3 id="step-3-speckitstasks--병렬-실행-가능한-태스크로-분해">Step 3. <code>/speckits:tasks</code> — 병렬 실행 가능한 태스크로 분해</h3>
<pre><code class="language-bash">/speckits:tasks</code></pre>
<p>생성된 <code>tasks.md</code>는 단순한 할 일 목록이 아닙니다. <strong>병렬 실행 가능한 태스크는 <code>[P]</code>로 표시</strong>하고, 태스크 간 의존 관계를 명시합니다:</p>
<pre><code class="language-markdown">## Phase 4: STT 레이어 (Priority: P1)

- [x] T008 [P] Write RED test: stt.route.test.ts
      → 파일 없음 → 400, 유효 파일 → 200 { text, languageCode }
      → ElevenLabs 429 → 429 &quot;크레딧이 부족합니다&quot;
- [x] T009 [P] Write RED test: transcribeFile.test.ts

## Phase 5: 번역 레이어 (Priority: P1) ← Phase 4와 병렬 실행 가능!

- [x] T012 [P] Write RED test: translate.route.test.ts
      → 같은 언어(en→en) → 200 { wasSkipped: true } ← 최적화 자동 발견
- [x] T013 [P] Write RED test: translateText.test.ts

## 의존성 그래프

Phase 4 (STT) ──┐
├──→ Phase 6 (파이프라인 훅)
Phase 5 (번역) ──┘</code></pre>
<p>Phase 4와 Phase 5는 서로 다른 파일을 수정하므로 Claude Code가 두 에이전트를 동시에 실행해 개발 시간을 단축할 수 있습니다.</p>
<p>또한 TDD가 자연스럽게 강제됩니다. tasks.md 구조가 &quot;테스트 먼저(RED) → 구현(GREEN)&quot; 순서를 명시하기 때문에, 구현부터 작성하는 유혹이 구조적으로 차단됩니다.</p>
<hr>
<h3 id="step-4-speckitsimplement--코드-구현">Step 4. <code>/speckits:implement</code> — 코드 구현</h3>
<pre><code class="language-bash">/speckits:implement</code></pre>
<p>에이전트가 <code>tasks.md</code>의 체크박스를 순서대로 처리하며 실제 코드를 작성합니다. 완료된 태스크는 <code>- [x]</code>로 표시됩니다.</p>
<p>이 단계에서 개발자가 하는 일은 <strong>각 태스크 완료 후 코드 리뷰</strong>뿐입니다. 수천 줄 한꺼번에 보는 게 아니라, 태스크 단위의 작은 변경사항을 검토합니다.</p>
<hr>
<h2 id="스펙주도개발--tdd-ai-개발에서-이-조합이-강력한-이유">스펙주도개발 + TDD: AI 개발에서 이 조합이 강력한 이유</h2>
<p>스펙주도개발과 TDD는 따로 봐도 좋은 방법론이지만, AI 코딩 에이전트와 함께할 때 <strong>시너지가 극대화</strong>됩니다.</p>
<h3 id="ai가-코드를-쓸-때-tdd가-반드시-필요한-이유">AI가 코드를 쓸 때 TDD가 반드시 필요한 이유</h3>
<p>AI 코딩 에이전트는 놀랍도록 빠르게 코드를 생성하지만, 근본적인 약점이 있습니다.</p>
<blockquote>
<p><strong>AI는 &quot;그럴듯한&quot; 코드를 만들어내지만, &quot;맞는&quot; 코드를 보장하지 않습니다.</strong></p>
</blockquote>
<p>실제로 AI 에이전트가 코드를 생성할 때 발생할 수 있는 문제들:</p>
<ul>
<li><strong>할루시네이션(Hallucination)</strong>: 존재하지 않는 API를 호출하거나, 잘못된 타입을 사용하는 코드를 자신있게 작성</li>
<li><strong>회귀(Regression)</strong>: 새 기능을 추가하면서 기존에 잘 동작하던 기능을 깨뜨림</li>
<li><strong>엣지 케이스 누락</strong>: 명세에 없는 케이스는 처리하지 않는 코드를 생성</li>
</ul>
<p>테스트가 없으면 이 문제들이 언제 발생했는지 알기 어렵습니다. 테스트가 있으면 <strong>AI가 스스로 코드를 검증하고 수정</strong>합니다.</p>
<h3 id="tdd가-ai-개발의-가드레일이-되는-방식">TDD가 AI 개발의 &quot;가드레일&quot;이 되는 방식</h3>
<pre><code class="language-text">개발자가 spec.md로 &quot;무엇을&quot; 정의
    ↓
tasks.md에서 테스트 먼저 작성 (RED)
    ↓
에이전트가 테스트를 통과하는 구현 작성 (GREEN)
    ↓
테스트 실패 → 에이전트 자동 자기수정 반복
    ↓
모든 테스트 통과 → 구현 완료</code></pre>
<p>이 프로젝트의 STT Route Handler 테스트가 실제로 이 역할을 했습니다:</p>
<pre><code class="language-typescript">// src/__tests__/app/api/stt.route.test.ts (RED 단계 — 구현 전에 먼저 작성)
describe(&#39;POST /api/stt&#39;, () =&gt; {
  it(&#39;파일 없으면 400 반환&#39;, async () =&gt; {
    const response = await POST(createRequest({}));
    expect(response.status).toBe(400);
  });

  it(&#39;ElevenLabs 429 → 429 &quot;크레딧이 부족합니다&quot;&#39;, async () =&gt; {
    mockElevenLabs.mockRejectedValue({ status: 429 });
    const response = await POST(createRequest({ file: mockFile }));
    const body = await response.json();
    expect(response.status).toBe(429);
    expect(body.error).toContain(&#39;크레딧이 부족합니다&#39;);
  });

  it(&#39;빈 전사 텍스트 → 400 &quot;음성을 감지하지 못했습니다&quot;&#39;, async () =&gt; {
    mockElevenLabs.mockResolvedValue({ text: &#39;&#39; });
    const response = await POST(createRequest({ file: mockFile }));
    expect(response.status).toBe(400);
  });
});</code></pre>
<p>이 테스트들이 먼저 존재했기 때문에, 에이전트가 <code>stt/route.ts</code>를 구현할 때 각 에러 케이스를 반드시 처리해야 했습니다. 테스트를 통과하기 전까지 태스크가 완료되지 않습니다.</p>
<h3 id="스펙주도개발이-tdd의-무엇을-테스트할지를-해결">스펙주도개발이 TDD의 &quot;무엇을 테스트할지&quot;를 해결</h3>
<p>TDD의 가장 어려운 질문은 &quot;어떤 테스트를 먼저 작성해야 하나?&quot;입니다. 스펙주도개발이 이 문제를 해결합니다.</p>
<p><code>spec.md</code>의 Acceptance Scenarios가 그대로 테스트 케이스가 됩니다:</p>
<pre><code class="language-text">spec.md의 시나리오                          →    테스트 케이스
─────────────────────────────────────────────────────────────
Given 파일 없이 요청,                        →    it(&#39;파일 없으면 400&#39;)
When POST /api/stt,
Then 400 반환

Given ElevenLabs 429 오류,                   →    it(&#39;429 → 크레딧 부족 메시지&#39;)
When 전사 요청,
Then 429 + 에러 메시지

Given 무음 파일,                             →    it(&#39;빈 텍스트 → 400&#39;)
When 전사 완료,
Then &quot;음성을 감지하지 못했습니다&quot;</code></pre>
<p>명세가 테스트 목록을 결정하고, 테스트 목록이 구현 범위를 결정합니다. <strong>에이전트가 &quot;무엇을 만들어야 하는지&quot; 추측할 필요가 없어집니다.</strong></p>
<h3 id="ai가-tdd를-더-빠르게-만든다">AI가 TDD를 더 빠르게 만든다</h3>
<p>전통적인 TDD의 가장 큰 불만은 &quot;테스트 작성이 너무 시간이 많이 걸린다&quot;는 것입니다. AI와 함께하면 이 단점이 사라집니다.</p>
<table>
<thead>
<tr>
<th>단계</th>
<th>전통적 TDD</th>
<th>AI + TDD</th>
</tr>
</thead>
<tbody><tr>
<td>테스트 작성</td>
<td>개발자가 직접 (느림)</td>
<td>에이전트가 spec.md 기반으로 생성 (빠름)</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>
<p>이 프로젝트에서 27개 태스크 중 <strong>테스트 작성(RED) 태스크가 절반을 차지</strong>했는데, 에이전트가 spec.md를 참고해 테스트를 자동 생성했기 때문에 실제 체감 부담은 크지 않았습니다.</p>
<hr>
<h2 id="speckits의-진짜-강점-constitution으로-아키텍처-원칙-강제">Speckits의 진짜 강점: Constitution으로 아키텍처 원칙 강제</h2>
<p><code>.specify/memory/constitution.md</code>에 프로젝트의 불변 원칙을 정의하면, <strong>에이전트가 모든 구현 단계에서 자동으로 이 원칙을 준수</strong>합니다.</p>
<p>제 프로젝트의 constitution.md 핵심 내용입니다:</p>
<pre><code class="language-markdown"># Project Constitution

## I. Feature-Sliced Design (FSD)

레이어 계층: app → features → entities → shared

- app/: 라우팅 엔트리만 담당
- features/: 행동(동사) 단위. ui/ + model/ + lib/
- entities/: 도메인(명사) 단위. api/ + dto/
  역방향 import 절대 금지.

## III. No Barrel Exports

# Bad

import { X } from &#39;@/features/some-feature&#39;

# Good

import { X } from &#39;@/features/some-feature/ui/SomeComponent&#39;

## V. Code Style

- 컴포넌트: function declaration (export default function Foo)
- var 절대 금지
- Boolean: is/has/should 접두사</code></pre>
<p>이 헌법이 없었다면, 6개 기능 브랜치를 구현하는 동안 에이전트가 매번 &quot;이 컴포넌트는 features에? entities에?&quot; 같은 질문을 반복했을 겁니다. Constitution 덕분에 <strong>6개 기능 전체에서 FSD 아키텍처가 일관되게 유지</strong>됐습니다.</p>
<hr>
<h2 id="결과-6개-기능의-최종-디렉토리-구조">결과: 6개 기능의 최종 디렉토리 구조</h2>
<pre><code class="language-text">src/
├── app/                     # Next.js App Router (라우팅만)
│   ├── login/page.tsx
│   ├── dashboard/page.tsx
│   ├── unauthorized/page.tsx
│   └── api/
│       ├── stt/route.ts     # ElevenLabs STT
│       ├── translate/route.ts  # Gemini 번역
│       ├── tts/route.ts     # ElevenLabs TTS
│       └── voices/route.ts
│
├── features/
│   ├── dubbing-create/      # AI 더빙 생성
│   │   ├── ui/              # DubbingForm, PipelineProgress, AudioPlayer
│   │   ├── model/           # useDubbingCreate hook
│   │   └── lib/             # validateFileInput (순수 함수)
│   └── auth-login/          # 인증
│       ├── ui/              # LoginPage, UnauthorizedPage
│       └── lib/             # 화이트리스트 확인
│
├── entities/dubbing/
│   ├── api/                 # transcribeFile, translateText, createDubbing
│   └── dto/                 # TypeScript 타입 정의
│
└── shared/config/env.ts     # 환경변수 (서버 전용)</code></pre>
<p>Constitution이 강제한 FSD 아키텍처 덕분에 코드 위치를 파악하는 데 드는 인지 비용이 거의 없었습니다.</p>
<hr>
<h2 id="speckits로-구현한-6개-기능-브랜치">Speckits로 구현한 6개 기능 브랜치</h2>
<table>
<thead>
<tr>
<th>기능</th>
<th>브랜치</th>
<th>산출물</th>
</tr>
</thead>
<tbody><tr>
<td>Next.js + FSD 기반 설계</td>
<td><code>feat/#1-nextjs-fsd-setup</code></td>
<td>spec.md → plan.md → tasks.md</td>
</tr>
<tr>
<td>AI 더빙 코어 (STT→번역→TTS)</td>
<td><code>feat/#3-ai-dubbing-core</code></td>
<td>spec.md → plan.md → tasks.md + API contracts</td>
</tr>
<tr>
<td>Google OAuth + 화이트리스트</td>
<td><code>feat/#5-auth-whitelist</code></td>
<td>spec.md → plan.md → tasks.md</td>
</tr>
<tr>
<td>UI/UX 폴리싱</td>
<td><code>feat/#8-ui-ux-polish</code></td>
<td>spec.md → tasks.md</td>
</tr>
<tr>
<td>연속 더빙 (새로고침 없이)</td>
<td><code>feat/#10-repeat-dubbing</code></td>
<td>spec.md → plan.md → tasks.md</td>
</tr>
<tr>
<td>Vercel 자동 배포 연동</td>
<td><code>feat/#12-vercel-auto-deploy</code></td>
<td>spec.md → plan.md → tasks.md</td>
</tr>
</tbody></table>
<p>모든 spec.md, plan.md, tasks.md 파일은 레포지터리의 <code>specs/feat/</code> 디렉토리에서 직접 볼 수 있습니다.</p>
<hr>
<h2 id="실전에서-배운-speckits-활용-노하우">실전에서 배운 Speckits 활용 노하우</h2>
<h3 id="1-명세-작성-시-엣지-케이스가-드러난다">1. 명세 작성 시 엣지 케이스가 드러난다</h3>
<p><code>/speckits:specify</code> 과정에서 에이전트의 자동 질문이 구현 전에 놓쳤던 엣지 케이스를 찾아줍니다.</p>
<p>AI 더빙 기능에서 발견한 케이스들:</p>
<ul>
<li>STT가 지원하지 않는 언어(<code>fr</code>, <code>de</code>)를 반환했을 때 번역 API에 어떻게 전달할 것인가?</li>
<li>원본과 타겟 언어가 동일하면 Gemini를 호출할 필요가 없다 (동일 언어 최적화)</li>
</ul>
<p>두 번째는 <code>wasSkipped: true</code>를 반환하는 최적화로 구현됐습니다. 구현 중에 발견했다면 훨씬 복잡한 리팩토링이 필요했을 겁니다.</p>
<h3 id="2-기능-하나에-하나의-브랜치와-specs-디렉토리">2. 기능 하나에 하나의 브랜치와 specs 디렉토리</h3>
<p>여러 기능을 동시에 요청하면 에이전트의 컨텍스트가 흐트러집니다. <code>feat/#N-feature-name</code> 브랜치와 <code>specs/feat/NNN-slug/</code> 디렉토리를 항상 1:1로 매핑하세요.</p>
<h3 id="3-요구사항이-바뀌면-specmd부터-업데이트">3. 요구사항이 바뀌면 spec.md부터 업데이트</h3>
<p>AI 더빙 코어에서 실제로 이 상황이 발생했습니다. &quot;텍스트 입력 → TTS&quot;에서 &quot;파일 업로드 → STT → 번역 → TTS&quot;로 변경됐을 때, spec.md의 <code>## Clarifications</code> 섹션에 변경 이유를 기록하고 에이전트에게 알렸습니다. 문서와 코드의 일관성이 유지됩니다.</p>
<pre><code class="language-markdown">## Clarifications

### Session 2026-03-10 (신규 스펙)

- Q: 요구사항 변경 내용은?
  → A: 기존 &quot;텍스트 입력 → TTS&quot;에서
  &quot;파일 업로드 → STT → 번역 → TTS&quot; 전체 파이프라인으로 변경</code></pre>
<h3 id="4-planmd의-아키텍처-결정은-나중에도-읽는다">4. plan.md의 아키텍처 결정은 나중에도 읽는다</h3>
<p>몇 주 후에 &quot;왜 이렇게 했지?&quot; 의문이 생길 때 plan.md를 보면 그 이유가 적혀 있습니다. 코드만 남기는 것보다 유지보수에 훨씬 유리합니다.</p>
<hr>
<h2 id="정리-스펙주도개발이-바꾼-것">정리: 스펙주도개발이 바꾼 것</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>이전 (바이브 코딩)</th>
<th>Speckits 이후</th>
</tr>
</thead>
<tbody><tr>
<td>엣지 케이스 발견 시점</td>
<td>구현 후</td>
<td>명세 단계에서 사전 발견</td>
</tr>
<tr>
<td>아키텍처 일관성</td>
<td>기능마다 달라짐</td>
<td>Constitution으로 강제</td>
</tr>
<tr>
<td>코드 리뷰 단위</td>
<td>수천 줄 한꺼번에</td>
<td>태스크 단위 소규모</td>
</tr>
<tr>
<td>변경 이유 추적</td>
<td>커밋 메시지 추측</td>
<td>spec.md Clarifications에 기록</td>
</tr>
<tr>
<td>TDD 적용</td>
<td>선택사항</td>
<td>tasks.md 구조에 의해 강제</td>
</tr>
</tbody></table>
<hr>
<h2 id="마치며">마치며</h2>
<p>코딩 에이전트의 진짜 가치는 &quot;코드를 대신 써주는 것&quot;이 아니라, <strong>구조화된 사고 과정을 함께 밟아주는 것</strong>이라고 느꼈습니다.</p>
<p>spec-kit / Speckits 덕분에 저는 아키텍처 결정, 엣지 케이스 발굴, 태스크 우선순위 조정에 집중하고, 반복적인 코드 작성은 에이전트에게 맡길 수 있었습니다.</p>
<p>레포지터리에는 이 글에서 소개한 <code>specs/feat/</code> 디렉토리 아래 실제 spec.md, plan.md, tasks.md 파일이 모두 공개돼 있습니다. 직접 살펴보고 싶다면 아래 링크로 오세요.</p>
<ul>
<li><strong>GitHub</strong>: <a href="https://github.com/jaeml06/Perso-AI-DevRel-Internship-Assignment">jaeml06/Perso-AI-DevRel-Internship-Assignment</a></li>
<li><strong>배포 서비스</strong>: <a href="https://perso-ai-devrel-internship-assignme.vercel.app">perso-ai-devrel-internship-assignme.vercel.app</a></li>
<li><strong>참고</strong>: <a href="https://github.com/github/spec-kit">GitHub spec-kit 공식 레포지터리</a> | <a href="https://github.blog/ai-and-ml/generative-ai/spec-driven-development-with-ai-get-started-with-a-new-open-source-toolkit/">스펙주도개발 소개 글 (GitHub Blog)</a></li>
</ul>
<p>⭐ <strong>도움이 됐다면 GitHub Star 부탁드립니다!</strong> 스펙주도개발 + Claude Code 경험을 나눠주시는 분들이 늘어날수록 커뮤니티가 풍부해집니다.</p>
<hr>
<p><em>이 포스트는 Claude Code + Speckits 워크플로우로 작성됐습니다.</em></p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[BFS(너비우선탐색)]]></title>
            <link>https://velog.io/@jaemin_ve/BFS%EB%84%88%EB%B9%84%EC%9A%B0%EC%84%A0%ED%83%90%EC%83%89</link>
            <guid>https://velog.io/@jaemin_ve/BFS%EB%84%88%EB%B9%84%EC%9A%B0%EC%84%A0%ED%83%90%EC%83%89</guid>
            <pubDate>Wed, 23 Jul 2025 04:08:44 GMT</pubDate>
            <description><![CDATA[<p><strong>BFS(너비 우선 탐색)</strong>은 그래프나 트리에서 루트 노드(또는 시작 노드)로부터 가까운 노드부터 우선적으로 탐색하는 방식입니다.</p>
<p><strong>큐(Queue)</strong> 자료구조를 기반으로 하며, <strong>방문한 노드는 다시 방문하지 않도록 처리</strong>해야 합니다.</p>
<ul>
<li>DFS가 “깊게 → 백트래킹”</li>
<li><strong>BFS는 “가까운 것부터 → 점점 멀리” 탐색</strong>.</li>
</ul>
<table>
<thead>
<tr>
<th><strong>유형</strong></th>
<th><strong>설명</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>최단 거리/최소 횟수</strong> 문제</td>
<td>미로 탐색, 전염, 전파, 이동 횟수, 최소 변환 등</td>
</tr>
<tr>
<td><strong>계층 구조 탐색</strong></td>
<td>트리 레벨 순회, 위상 정렬</td>
</tr>
<tr>
<td><strong>다익스트라 (특수 BFS)</strong></td>
<td>가중치 1일 때 최단 거리 계산</td>
</tr>
<tr>
<td><strong>시뮬레이션 문제</strong></td>
<td>바이러스 전파, 불 퍼짐, 토마토 익히기 등</td>
</tr>
</tbody></table>
<p><strong>JavaScript로 구현: 인접 리스트 기반</strong></p>
<pre><code class="language-jsx">const graph = [
  [],        // 0번 dummy
  [2, 3],    // 1 → 2, 3
  [4],
  [5, 6],
  [],
  [],
  [],
];

const visited = Array(graph.length).fill(false);

function bfs(start) {
  const queue = [start];
  visited[start] = true;

  while (queue.length &gt; 0) {
    const current = queue.shift();
    console.log(current);

    for (const next of graph[current]) {
      if (!visited[next]) {
        visited[next] = true;
        queue.push(next);
      }
    }
  }
}

bfs(1); // 시작 노드</code></pre>
<p>자바스크립트의 경우 큐를 지원하지 않기 때문에 큐를 직접 구현해야한다.</p>
<p>ex)</p>
<pre><code class="language-jsx">class Queue {
  constructor() {
    this.q = [];
    this.head = 0;
  }

  enqueue(value) {
    this.q.push(value);
  }

  dequeue() {
    if (this.isEmpty()) {
      throw new Error(&quot;Queue is empty&quot;);
    }
    return this.q[this.head++];
  }

  isEmpty() {
    return this.q.length === this.head;
  }

  size() {
    return this.q.length - this.head;
  }
}</code></pre>
<p><strong>2차원 배열(Grid 기반 BFS)</strong></p>
<pre><code class="language-jsx">const grid = [
  [1, 1, 0],
  [0, 1, 0],
  [1, 1, 1],
];
const visited = Array.from({ length: 3 }, () =&gt; Array(3).fill(false));

const dx = [-1, 1, 0, 0];
const dy = [0, 0, -1, 1];

function bfs(x, y) {
  const queue = [[x, y]];
  visited[y][x] = true;

  while (queue.length &gt; 0) {
    const [cx, cy] = queue.shift();

    for (let i = 0; i &lt; 4; i++) {
      const nx = cx + dx[i];
      const ny = cy + dy[i];

      if (
        nx &gt;= 0 &amp;&amp; nx &lt; 3 &amp;&amp;
        ny &gt;= 0 &amp;&amp; ny &lt; 3 &amp;&amp;
        grid[ny][nx] === 1 &amp;&amp;
        !visited[ny][nx]
      ) {
        visited[ny][nx] = true;
        queue.push([nx, ny]);
      }
    }
  }
}

bfs(0, 0);</code></pre>
<h2 id="주요특징">주요특징</h2>
<table>
<thead>
<tr>
<th><strong>항목</strong></th>
<th><strong>설명</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>탐색 방향</strong></td>
<td>너비 우선 (가까운 것부터)</td>
</tr>
<tr>
<td><strong>사용 자료구조</strong></td>
<td>큐 (Queue)</td>
</tr>
<tr>
<td><strong>방문 체크 방식</strong></td>
<td>visited 배열 사용</td>
</tr>
<tr>
<td><strong>시간 복잡도</strong></td>
<td>O(V + E)</td>
</tr>
<tr>
<td><strong>특징</strong></td>
<td>최단 거리 보장 (가중치 없을 때)</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[DFS(깊이 우선 탐색)]]></title>
            <link>https://velog.io/@jaemin_ve/DFS%EA%B9%8A%EC%9D%B4-%EC%9A%B0%EC%84%A0-%ED%83%90%EC%83%89</link>
            <guid>https://velog.io/@jaemin_ve/DFS%EA%B9%8A%EC%9D%B4-%EC%9A%B0%EC%84%A0-%ED%83%90%EC%83%89</guid>
            <pubDate>Wed, 16 Jul 2025 11:28:47 GMT</pubDate>
            <description><![CDATA[<p>DFS는 그래프의 한 정점에서 시작해, <strong>가능한 한 깊게</strong> 탐색하다가 더 이상 갈 곳이 없으면 <strong>다시 이전 정점으로 되돌아가며</strong> 탐색을 이어가는 방식입니다.</p>
<p><strong>스택 구조</strong> (재귀 함수 또는 직접 스택 사용)를 기반으로 동작합니다.</p>
<p>DFS의 경우, <strong>한 방향으로 가능한 깊이까지 탐색</strong>을 우선합니다. 즉, 하나의 가능한 경로를 재귀적으로 끝까지 따라가며 탐색하기 때문에 <strong>조건을 만족하는 해답이 그 경로 상에 있다면 매우 빠르게 도달</strong>할 수 있습니다.</p>
<p>보통 다음과 같은 문제에서 사용된다. 공통적으로 모든 경우를 탐색해야하는 경우 사용된다.</p>
<table>
<thead>
<tr>
<th><strong>1. 모든 경로 탐색</strong></th>
<th>시작 지점부터 도착 지점까지 갈 수 있는 모든 경로를 찾는 문제.</th>
</tr>
</thead>
<tbody><tr>
<td><strong>2. 백트래킹 문제</strong></td>
<td>조합, 순열, N-Queen 등 조건을 만족하는 경우의 수 탐색</td>
</tr>
<tr>
<td><strong>3. 연결 요소 세기</strong></td>
<td>그래프에서 연결된 그룹(connected component)의 개수 세기</td>
</tr>
<tr>
<td><strong>4. 미로 탐색</strong></td>
<td>갈 수 있는 경로를 따라 깊게 탐색하며 길이 존재하는지 확인</td>
</tr>
<tr>
<td><strong>5. 사이클 탐지</strong></td>
<td>DFS 도중 방문한 노드를 또 만나면 사이클 존재 가능성</td>
</tr>
<tr>
<td><strong>6. 섬의 개수 문제</strong></td>
<td>2차원 배열에서 인접한 땅(1)을 DFS로 모두 탐색 후 섬 개수 카운팅</td>
</tr>
</tbody></table>
<p><strong>JavaScript로 구현: 인접 리스트 기반</strong></p>
<pre><code class="language-jsx">const graph = [
  [],         // 0번 인덱스는 사용하지 않음
  [2, 3],     // 1번 노드와 연결된 노드들
  [4],
  [5, 6],
  [],
  [],
  []
];

const visited = Array(graph.length).fill(false);

function dfs(v) {
  visited[v] = true;
  console.log(v); // 방문 순서 출력

  for (const next of graph[v]) {
    if (!visited[next]) {
      dfs(next);
    }
  }
}

dfs(1); // 1번 노드부터 탐색 시작

//실행 결과
1
2
4
3
5
6</code></pre>
<p><strong>JavaScript로 구현: 인접 행렬 기반</strong></p>
<pre><code class="language-jsx">const graph = [
  // 0번 인덱스는 사용하지 않음
  [0, 0, 0, 0, 0, 0, 0], // dummy
  [0, 0, 1, 1, 0, 0, 0], // 1번 노드 → 2, 3
  [0, 0, 0, 0, 1, 0, 0], // 2번 노드 → 4
  [0, 0, 0, 0, 0, 1, 1], // 3번 노드 → 5, 6
  [0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0],
];

const visited = Array(graph.length).fill(false);

function dfs(v) {
  visited[v] = true;
  console.log(v); // 방문 순서 출력

  for (let i = 1; i &lt; graph.length; i++) {
    if (graph[v][i] === 1 &amp;&amp; !visited[i]) {
      dfs(i);
    }
  }
}

dfs(1);

// 실행 결과
1
2
4
3
5
6</code></pre>
<p><strong>Grid(격자) 기반 DFS</strong></p>
<pre><code class="language-jsx">const dx = [0, 1];
const dy = [1, 0];

function inRange(x, y) {
  return 0 &lt;= x &amp;&amp; x &lt; 5 &amp;&amp; 0 &lt;= y &amp;&amp; y &lt; 5;
}

function canGo(x, y) {
  if (!inRange(x, y)) return false;
  if (visited[x][y] || grid[x][y] === 0) return false;
  return true;
}

function dfs(x, y) {
  for (let i = 0; i &lt; dx.length; i++) {
    const newX = x + dx[i];
    const newY = y + dy[i];

    if (canGo(newX, newY)) {
      visited[newY][newX] = true;
      answer[newY][newX] = order++;
      dfs(newX, newY);
    }
  }
}

// 초기화 예시
const visited = Array.from({ length: 5 }, () =&gt; Array(5).fill(false));
const answer = Array.from({ length: 5 }, () =&gt; Array(5).fill(0));
let order = 1;
visited[0][0] = 1;
answer[0][0] = order++;
dfs(0, 0);</code></pre>
<h2 id="주요-특징">주요 특징</h2>
<table>
<thead>
<tr>
<th><strong>구분</strong></th>
<th><strong>DFS 설명</strong></th>
</tr>
</thead>
<tbody><tr>
<td>탐색 방향</td>
<td>깊이 우선 (한 방향으로 최대한 진행 후, 백트래킹)</td>
</tr>
<tr>
<td>자료구조</td>
<td>스택 or 재귀</td>
</tr>
<tr>
<td>구현 난이도</td>
<td>간단 (재귀로 쉽게 구현 가능)</td>
</tr>
<tr>
<td>시간 복잡도</td>
<td>O(V + E)</td>
</tr>
<tr>
<td>활용 예시</td>
<td>미로 탐색, 백트래킹 문제, 사이클 탐지 등</td>
</tr>
<tr>
<td>방문 기록 방식</td>
<td>보통 visited[] 배열 사용</td>
</tr>
</tbody></table>
<h2 id="예상-면접-질문">예상 면접 질문</h2>
<h3 id="dfs와-bfs의-차이점은"><strong>DFS와 BFS의 차이점은?</strong></h3>
<ul>
<li>DFS는 깊이 우선, BFS는 너비 우선 탐색입니다.</li>
<li>DFS는 스택(재귀) 기반, BFS는 큐 기반입니다.</li>
<li>DFS는 특정 조건 만족 여부를 빨리 찾는 데 유리하고, BFS는 최단 경로를 구할 때 유리합니다.</li>
</ul>
<h3 id="dfs는-언제-사용하나요"><strong>DFS는 언제 사용하나요?</strong></h3>
<ul>
<li>미로 탐색, 퍼즐, 조합 탐색, 백트래킹 문제 등 <strong>모든 경로를 탐색하거나 특정 조건을 만족하는 경로</strong>를 찾을 때 DFS가 유용합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[이벤트 버블링과 캡처링]]></title>
            <link>https://velog.io/@jaemin_ve/%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%B2%84%EB%B8%94%EB%A7%81%EA%B3%BC-%EC%BA%A1%EC%B2%98%EB%A7%81</link>
            <guid>https://velog.io/@jaemin_ve/%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%B2%84%EB%B8%94%EB%A7%81%EA%B3%BC-%EC%BA%A1%EC%B2%98%EB%A7%81</guid>
            <pubDate>Fri, 11 Jul 2025 03:14:14 GMT</pubDate>
            <description><![CDATA[<p>웹 브라우저에서 DOM 요소에 이벤트가 발생하면, 이 이벤트는 단순히 한 곳에서만 발생하지 않습니다. 실제로는 이벤트 전파(event propagation)라는 과정을 통해 여러 DOM 요소를 거쳐 흐르게 됩니다. 이 전파 흐름에는 버블링(bubbling)과 캡처링(capturing)이라는 두 가지 단계가 존재합니다.</p>
<h2 id="이벤트-전파란"><strong>이벤트 전파란?</strong></h2>
<p>이벤트 전파는 총 3단계로 진행됩니다.</p>
<ol>
<li><strong>캡처링 단계 (Capturing Phase)</strong><ul>
<li>최상위 요소(document)에서 이벤트가 <strong>타깃 요소로 내려가며 전파</strong>됨</li>
</ul>
</li>
<li><strong>타깃 단계 (Target Phase)</strong><ul>
<li><em>이벤트가 실제 발생한 요소(target)*</em>에서 핸들러가 실행됨</li>
</ul>
</li>
<li><strong>버블링 단계 (Bubbling Phase)</strong><ul>
<li>이벤트가 타깃 요소에서부터 <strong>다시 상위 요소로 전파</strong>
<img src="https://velog.velcdn.com/images/jaemin_ve/post/4cc55974-15d3-448d-8adf-5330f9d65beb/image.png" alt=""></li>
</ul>
</li>
</ol>
<p>ex)</p>
<p>[캡처링] document → body → div → [타깃] button → [버블링] div → body → document</p>
<h2 id="버블링bubbling"><strong>버블링(Bubbling)</strong></h2>
<p>브라우저는 기본적으로 <strong>버블링 방식</strong>으로 이벤트를 처리합니다.</p>
<p>즉, <strong>가장 안쪽 요소(타깃)</strong>에서 이벤트가 발생한 뒤, 그 이벤트가 <strong>상위 요소로 전파</strong>되며 각 요소에 등록된 이벤트 핸들러가 실행됩니다.</p>
<p>ex)</p>
<pre><code class="language-jsx">function BubblingExample() {
  const handleFormClick = () =&gt; alert(&quot;form&quot;);
  const handleDivClick = () =&gt; alert(&quot;div&quot;);
  const handlePClick = () =&gt; alert(&quot;p&quot;);

  return (
    &lt;form onClick={handleFormClick}&gt;
      FORM
      &lt;div onClick={handleDivClick}&gt;
        DIV
        &lt;p onClick={handlePClick}&gt;P (클릭해보세요)&lt;/p&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  );
}
//&lt;p&gt; 클릭 시: p → div → form 순으로 alert 발생
</code></pre>
<p>보통 이벤트 위임이나, 유저 행동을 추척하는 로깅등에서 이용됩니다.</p>
<h2 id="이벤트-위임"><strong>이벤트 위임</strong></h2>
<p>버블링 덕분에 이벤트 위임이 가능합니다.  여러 li요소가 있는 경우 각각에 리스너를 붙이는 대신, 부모 ul에만 리스너를 붙이는 방식으로 사용할 수 있습니다.</p>
<pre><code class="language-jsx">function EventDelegationExample() {
  const handleClick = (e) =&gt; {
    if (e.target.tagName === &quot;LI&quot;) {
      alert(`클릭한 항목: ${e.target.textContent}`);
    }
  };

  return (
    &lt;ul onClick={handleClick} style={{ cursor: &quot;pointer&quot; }}&gt;
      &lt;li&gt;사과&lt;/li&gt;
      &lt;li&gt;바나나&lt;/li&gt;
      &lt;li&gt;체리&lt;/li&gt;
    &lt;/ul&gt;
  );
}</code></pre>
<p>수많은 자식 요소 각각에 이벤트 리스너를 등록하면 <strong>메모리 사용량이 늘고</strong>, <strong>성능 저하</strong>가 발생할 수 있어 성능 최적화에 사용됩니다.</p>
<h2 id="캡처링capturing"><strong>캡처링(Capturing)</strong></h2>
<p>캡처링은 버블링과 반대입니다.</p>
<p><strong>이벤트가 최상위(document)에서 시작해 타깃 요소로 내려갑니다.</strong></p>
<p>기본적으로 캡처링 단계에서는 이벤트 핸들러가 실행되지 않습니다.</p>
<p>하지만 addEventListener의 세 번째 인자에 { capture: true }를 설정하면 캡처링 단계에서 핸들러가 실행됩니다. 리액트에서는 이벤트명 마지막에 <code>Capture</code>를 추가하면 됩니다.</p>
<pre><code class="language-jsx">function CapturingExample() {
  const handleClickCapture = (level) =&gt; () =&gt; alert(`캡처링: ${level}`);
  const handleClick = (level) =&gt; () =&gt; alert(`버블링: ${level}`);

  return (
    &lt;form
      onClickCapture={handleClickCapture(&quot;form&quot;)}
      onClick={handleClick(&quot;form&quot;)}
      style={styles}
    &gt;
      FORM
      &lt;div
        onClickCapture={handleClickCapture(&quot;div&quot;)}
        onClick={handleClick(&quot;div&quot;)}
      &gt;
        DIV
        &lt;p
          onClickCapture={handleClickCapture(&quot;p&quot;)}
          onClick={handleClick(&quot;p&quot;)}
        &gt;
          P (클릭해보세요)
        &lt;/p&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  );
}
//&lt;p&gt;를 클릭하면 캡처링 순서로 먼저 실행된 뒤 버블링 순서로 실행
//캡처링: form → div → p → 버블링: p → div → form</code></pre>
<p>보통 다음과 같은 이유로 이벤트 캡쳐링을 사용합니다.</p>
<ul>
<li><strong>보안 또는 로깅 목적</strong>으로 먼저 이벤트를 잡고 싶을 때</li>
<li><strong>자식 요소에서 stopPropagation이 발생하더라도 이벤트를 캐치하고 싶을 때</strong></li>
</ul>
<h2 id="eventtarget-vs-eventcurrenttarget"><strong>event.target vs event.currentTarget</strong></h2>
<ul>
<li><strong>event.target  :</strong> 실제 클릭한(이벤트가 발생한) 요소</li>
<li><strong>event.currentTarget :</strong> 현재 핸들러가 바인딩된 요소 (버블링 도중 이벤트를 잡은 요소)</li>
</ul>
<h2 id="이벤트-전파-제어"><strong>이벤트 전파 제어</strong></h2>
<h3 id="eventstoppropagation"><strong>event.stopPropagation()</strong></h3>
<p><strong>이벤트가 상위 요소로 버블링되지 않도록 막을 때, 사용됩니다.</strong>
ex) 버튼 클릭 시 부모 요소의 클릭 이벤트를 막고 싶을 때</p>
<h3 id="eventstopimmediatepropagation"><strong>event.stopImmediatePropagation()</strong></h3>
<p><strong>같은 요소에 등록된 다른 핸들러까지도 실행되지 않도록 차단합니다.</strong></p>
<p>단, 리액트 자체 이벤트 시스템에서는 지원하지 않고, DOM API를 직접 다루는 경우, 사용가능.</p>
<h2 id="예상-기술-면접">예상 기술 면접</h2>
<h3 id="이벤트-버블링이-무엇인가요"><strong>이벤트 버블링이 무엇인가요?</strong></h3>
<p>이벤트가 타깃 요소에서 발생한 뒤, 상위 요소들로 전파되며 이벤트 핸들러가 실행되는 흐름입니다.</p>
<h3 id="캡처링은-언제-사용하나요"><strong>캡처링은 언제 사용하나요?</strong></h3>
<p>보통은 사용되지 않지만, 부모가 먼저 이벤트를 처리해야 할 경우, 또는 로깅/보안/예외 처리 로직에서 사용됩니다.</p>
<h3 id="eventtarget과-currenttarget의-차이는"><strong>event.target과 currentTarget의 차이는?</strong></h3>
<p>event.target은 실제 클릭된 요소이고, currentTarget은 현재 이벤트 헨들러가 연결된 요소를 가르킵니다.</p>
<h3 id="이벤트-위임은-무엇인가요"><strong>이벤트 위임은 무엇인가요?</strong></h3>
<p>이벤트 버블링을 기반으로 동작하며 여러 자식 요소의 이벤트를 <strong>부모 요소 하나</strong>에서 처리하는 기법입니다.</p>
<blockquote>
<p>참고자료</p>
</blockquote>
<p><a href="https://ko.javascript.info/bubbling-and-capturing">https://ko.javascript.info/bubbling-and-capturing</a>
<a href="https://inpa.tistory.com/entry/JS-%F0%9F%93%9A-%EB%B2%84%EB%B8%94%EB%A7%81-%EC%BA%A1%EC%B3%90%EB%A7%81">https://inpa.tistory.com/entry/JS-📚-버블링-캡쳐링</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[www.google.com을 주소창에 입력하면 무슨 일이 일어날까?]]></title>
            <link>https://velog.io/@jaemin_ve/www.google.com%EC%9D%84%EC%A3%BC%EC%86%8C%EC%B0%BD%EC%97%90-%EC%9E%85%EB%A0%A5%ED%95%98%EB%A9%B4-%EB%AC%B4%EC%8A%A8-%EC%9D%BC%EC%9D%B4-%EC%9D%BC%EC%96%B4%EB%82%A0%EA%B9%8C</link>
            <guid>https://velog.io/@jaemin_ve/www.google.com%EC%9D%84%EC%A3%BC%EC%86%8C%EC%B0%BD%EC%97%90-%EC%9E%85%EB%A0%A5%ED%95%98%EB%A9%B4-%EB%AC%B4%EC%8A%A8-%EC%9D%BC%EC%9D%B4-%EC%9D%BC%EC%96%B4%EB%82%A0%EA%B9%8C</guid>
            <pubDate>Mon, 07 Jul 2025 07:51:07 GMT</pubDate>
            <description><![CDATA[<p><a href="http://www.google.com%EC%9D%84">www.google.com을</a> 주소창에 입력했을 때 발생하는 과정을 살펴보겠다.</p>
<hr>
<h2 id="1-dns-조회-domain-name-system-lookup">1. DNS 조회 (Domain Name System Lookup)</h2>
<p>사용자가 <a href="http://www.google.com%EC%9D%84">www.google.com을</a> 입력하면 브라우저는 먼저 이 <strong>도메인 이름을 IP 주소로 변환</strong>해야 합니다. 이 과정을 <strong>DNS 조회</strong>라고 부릅니다.</p>
<h3 id="과정-요약">과정 요약</h3>
<p>DNS 조회는 점진적으로 이루어진다. 브라우저는 먼저 가장 가까운 곳에서 DNS 정보를 찾기 시작하며, 찾지 못하면 점점 외부로 요청을 보내 확인한다.</p>
<ol>
<li><strong>브라우저 캐시 확인</strong><ul>
<li>브라우저는 최근에 요청한 도메인과 IP 정보를 <strong>일정 시간 캐싱</strong>합니다.</li>
<li>이 정보가 있으면 바로 사용하고, 없으면 다음 단계로 넘어갑니다.</li>
</ul>
</li>
<li><strong>운영체제(OS)의 DNS 캐시 확인</strong><ul>
<li>OS도 내부적으로 DNS 정보를 일정 기간 보관합니다.</li>
<li>브라우저에 없더라도 OS 수준에서 캐시된 IP가 있을 수 있습니다.</li>
<li>예: macOS는 dscacheutil -cachedump 명령어로 확인 가능</li>
</ul>
</li>
<li><strong>라우터의 DNS 캐시 확인</strong><ul>
<li>가정이나 사무실의 공유기(라우터)도 DNS 캐시를 유지할 수 있습니다.</li>
<li>이 단계에서 IP를 찾을 수 있다면 외부 네트워크로 나갈 필요가 없습니다.</li>
</ul>
</li>
<li><strong>ISP(인터넷 제공자)의 DNS 서버 요청</strong><ul>
<li>ISP는 자체 DNS 서버를 운영하며, 일반적으로 우리가 처음 접속하는 외부 DNS입니다.</li>
<li>ISP DNS도 캐시를 유지하고 있어 자주 요청되는 도메인은 빠르게 응답할 수 있습니다.</li>
</ul>
</li>
<li><strong>권한 있는 DNS 서버까지 쿼리 (재귀/반복)</strong><ul>
<li>ISP DNS 서버에서도 IP 정보를 찾지 못하면, ISP 서버는 서버의 IP주소를 찾기 위해 DNS query를 날린다.</li>
<li>이 과정에서 재귀적으로 루트 네임서버부터 실제 도메인을 관리하는 네임서버까지 필요한 IP 주소를 찾거나 찾을 수 없다는 오류 응답을 반환할 때까지 재귀적으로 진행된다.</li>
</ul>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/jaemin_ve/post/dd73acbb-bd3f-4b56-9bb6-900d2b4b2f66/image.png" alt=""></p>
<p>이 과정을 통해 <code>www.google.com</code>에 해당하는 IP 주소를 얻습니다.</p>
<hr>
<h2 id="2-tcp-연결-수립-3-way-handshake">2. TCP 연결 수립 (3-Way Handshake)</h2>
<p>IP 주소가 확인되면, 브라우저는 해당 서버와 <strong>TCP 연결</strong>을 수립합니다. TCP는 데이터를 <strong>신뢰성 있게 전송</strong>하기 위한 전송 계층 프로토콜입니다.</p>
<h3 id="3-way-handshake">3-Way Handshake</h3>
<ol>
<li><p>클라이언트가 <strong>SYN</strong> 패킷 전송</p>
<p> 브라우저가 서버와의 연결을 시작하고자 한다는 것을 알리는 신호.</p>
</li>
<li><p>서버가 <strong>SYN-ACK</strong> 패킷 응답 </p>
<p> 서버가 SYN 패킷을 받으면, 클라이언트에게 연결 요청을 수락한다는 의미의 SYN-ACK 패킷을 전송</p>
</li>
<li><p>클라이언트가 <strong>ACK</strong> 전송</p>
<p> 클라이언트는 서버에게 ACK 패킷을 보내 연결을 확정</p>
</li>
</ol>
<p>이 과정을 통해 양측은 연결을 설정하고 데이터를 교환할 준비를 마칩니다.</p>
<blockquote>
<p>만약 HTTPS라면 다음 단계에 SSL/TLS 핸드셰이크도 함께 수행됩니다.</p>
</blockquote>
<hr>
<h2 id="3-ssltls-핸드셰이크-https의-경우">3. SSL/TLS 핸드셰이크 (HTTPS의 경우)</h2>
<p>HTTPS는 HTTP 위에 <strong>TLS(Transport Layer Security)</strong> 암호화를 적용한 프로토콜입니다. 보안성을 위해 TLS 핸드셰이크를 통해 암호화 연결을 설정합니다.</p>
<h3 id="주요-작업">주요 작업</h3>
<ul>
<li>서버는 <strong>SSL 인증서</strong>를 클라이언트에 전송</li>
<li>클라이언트는 인증서를 검증하고, <strong>세션 키</strong>를 협상</li>
<li>이후 모든 HTTP 통신은 <strong>암호화된 채널</strong>에서 수행</li>
</ul>
<hr>
<h2 id="4-http-요청-전송">4. HTTP 요청 전송</h2>
<p>TCP 연결이 수립되면 브라우저는 HTTP/HTTPS 요청을 전송합니다.</p>
<h3 id="예시">예시</h3>
<pre><code>GET / HTTP/1.1
Host: www.google.com
User-Agent: Chrome/xx.x
</code></pre><p>이 요청은 웹 페이지를 요청하는 메시지이며, 필요한 쿠키, 캐시 제어, 인증 정보 등이 함께 전송될 수 있습니다.</p>
<hr>
<h2 id="5-서버의-응답">5. 서버의 응답</h2>
<p>웹 서버는 요청을 처리한 후 HTML, CSS, JavaScript, 이미지 등의 리소스를 HTTP 응답으로 브라우저에 전달합니다.</p>
<h3 id="응답-예시">응답 예시</h3>
<pre><code>HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
</code></pre><p>이후 브라우저는 이 HTML 문서를 해석하고 추가 리소스를 요청합니다 (예: JS, CSS, 폰트).</p>
<hr>
<h2 id="6-브라우저-렌더링-파이프라인">6. 브라우저 렌더링 파이프라인</h2>
<p>서버에서 받은 HTML과 리소스를 바탕으로 브라우저는 다음과 같은 과정을 거쳐 화면에 웹 페이지를 렌더링합니다.</p>
<h3 id="렌더링-단계">렌더링 단계</h3>
<ol>
<li><strong>HTML 파싱</strong> → <strong>DOM(Document Object Model)</strong> 생성</li>
<li><strong>CSS 파싱</strong> → <strong>CSSOM(CSS Object Model)</strong> 생성</li>
<li>DOM + CSSOM → <strong>Render Tree 구성</strong></li>
<li><strong>Layout</strong> 단계에서 각 요소의 위치 계산</li>
<li><strong>Paint</strong> 단계에서 픽셀로 변환</li>
<li><strong>Composite</strong> 단계에서 GPU 레이어 조합 및 화면 출력</li>
</ol>
<p>이 과정을 통해 사용자는 <code>www.google.com</code> 웹사이트를 시각적으로 확인할 수 있게 됩니다.</p>
<hr>
<h2 id="요약-정리">요약 정리</h2>
<table>
<thead>
<tr>
<th>단계</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>1. DNS 조회</td>
<td>도메인을 IP로 변환</td>
</tr>
<tr>
<td>2. TCP 연결</td>
<td>3-Way Handshake 수행</td>
</tr>
<tr>
<td>3. TLS 핸드셰이크</td>
<td>암호화 연결 수립 (HTTPS일 경우)</td>
</tr>
<tr>
<td>4. HTTP 요청</td>
<td>웹 페이지 리소스를 요청</td>
</tr>
<tr>
<td>5. 서버 응답</td>
<td>HTML, CSS, JS 등 리소스 반환</td>
</tr>
<tr>
<td>6. 렌더링 파이프라인</td>
<td>사용자 화면에 페이지 출력</td>
</tr>
</tbody></table>
<blockquote>
<p>참고 자료
<a href="https://yong-nyong.tistory.com/82">https://yong-nyong.tistory.com/82</a>
<a href="https://velog.io/@khy226/%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80%EC%97%90-url%EC%9D%84-%EC%9E%85%EB%A0%A5%ED%95%98%EB%A9%B4-%EC%96%B4%EB%96%A4%EC%9D%BC%EC%9D%B4-%EB%B2%8C%EC%96%B4%EC%A7%88%EA%B9%8C">https://velog.io/@khy226/브라우저에-url을-입력하면-어떤일이-벌어질까</a>
<a href="https://velog.io/@tnehd1998/%EC%A3%BC%EC%86%8C%EC%B0%BD%EC%97%90-www.google.com%EC%9D%84-%EC%9E%85%EB%A0%A5%ED%96%88%EC%9D%84-%EB%95%8C-%EC%9D%BC%EC%96%B4%EB%82%98%EB%8A%94-%EA%B3%BC%EC%A0%95">https://velog.io/@tnehd1998/주소창에-www.google.com을-입력했을-때-일어나는-과정</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[그래프란]]></title>
            <link>https://velog.io/@jaemin_ve/%EA%B7%B8%EB%9E%98%ED%94%84%EB%9E%80</link>
            <guid>https://velog.io/@jaemin_ve/%EA%B7%B8%EB%9E%98%ED%94%84%EB%9E%80</guid>
            <pubDate>Fri, 04 Jul 2025 03:50:19 GMT</pubDate>
            <description><![CDATA[<p><strong>정점(노드, Vertex)</strong>과 <strong>간선(Edge)</strong>으로 구성된 자료구조</p>
<p>트리보다 확장된 개념</p>
<ul>
<li>예: SNS 관계, 지하철 노선도 등</li>
</ul>
<hr>
<h3 id="정점vertex"><strong>정점(Vertex)</strong></h3>
<ul>
<li>각 노드를 의미</li>
<li>SNS → 유저, 지하철 → 역</li>
</ul>
<h3 id="간선edge"><strong>간선(Edge)</strong></h3>
<ul>
<li>정점과 정점을 잇는 선</li>
<li>방향이 있을 수도 있고 없을 수도 있음</li>
<li>간선에 <strong>가중치(Weight)</strong>가 있을 수도 있음 → 거리, 비용 등</li>
</ul>
<hr>
<h3 id="방향-그래프-vs-무방향-그래프"><strong>방향 그래프 vs 무방향 그래프</strong></h3>
<ul>
<li><strong>무방향 그래프</strong>: A ↔ B (양방향 이동 가능)</li>
<li><strong>방향 그래프</strong>: A → B (한 방향만 가능)</li>
<li><strong>차수(Degree)</strong>: 한 정점과 연결된 간선 수<ul>
<li><strong>무방향 그래프</strong>: 그냥 차수</li>
<li><strong>방향 그래프</strong>:<ul>
<li><strong>진입차수(In-degree)</strong>: 들어오는 간선 수</li>
<li><strong>진출차수(Out-degree)</strong>: 나가는 간선 수</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h3 id="사이클cycle"><strong>사이클(Cycle)</strong></h3>
<ul>
<li>출발한 정점으로 다시 돌아올 수 있는 경로가 있는 경우</li>
<li>순환이 있음</li>
</ul>
<hr>
<h3 id="연결-요소-connected-component"><strong>연결 요소 (Connected Component)</strong></h3>
<ul>
<li>간선들을 통해 모두 연결되어 있는 정점들의 집합</li>
<li>연결 그래프: 모든 정점이 서로 연결됨</li>
<li>비연결 그래프: 일부 정점들이 떨어져 있음</li>
</ul>
<h2 id="그래프-표현-방식">그래프 표현 방식</h2>
<p>그래프를 표현하는 방식은 <strong>인접 행렬 (Adjacency Matrix)</strong>와 <strong>인접 리스트 (Adjacency List)</strong> 2가지가 있다.</p>
<p><img src="https://velog.velcdn.com/images/jaemin_ve/post/075f7f62-9759-4fd2-8907-c6697f0e06fd/image.png" alt=""></p>
<h3 id="1-인접-행렬-adjacency-matrix"><strong>1. 인접 행렬 (Adjacency Matrix)</strong></h3>
<p>2차원 배열을 이용하여 정점 간의 연결 여부를 나타냄.</p>
<p>공간 복잡도는 <strong>O(V²)</strong>.</p>
<pre><code class="language-jsx">// 정점 수 V = 6 (1번부터 6번까지)
const V = 6;
const graph = Array.from(Array(V + 1), () =&gt; Array(V + 1).fill(0));

// 간선 추가 (1 → 5, 1 → 3 등)
graph[1][5] = 1;
graph[1][3] = 1;
graph[3][2] = 1;
graph[3][5] = 1;
graph[4][3] = 1;
graph[4][6] = 1;
graph[6][4] = 1;

// 예시: 1 → 5 연결 여부
console.log(graph[1][5]); // 1 (연결됨)
console.log(graph[1][2]); // 0 (연결 안됨)</code></pre>
<h3 id="2-인접-리스트-adjacency-list"><strong>2. 인접 리스트 (Adjacency List)</strong></h3>
<p>각 정점이 연결된 정점들의 리스트를 갖는 방식.</p>
<p>공간 복잡도는 <strong>O(V + E)</strong>.</p>
<pre><code class="language-jsx">const V = 6;
const graph = Array.from(Array(V + 1), () =&gt; []);

// 간선 추가
graph[1].push(5, 3);
graph[2] = [];
graph[3].push(2, 5);
graph[4].push(3, 6);
graph[5] = [];
graph[6].push(4);

// 예시: 정점 3에서 연결된 노드 출력
console.log(graph[3]); // [2, 5]</code></pre>
<p><strong>정리 비교표</strong></p>
<table>
<thead>
<tr>
<th><strong>항목</strong></th>
<th><strong>인접 행렬</strong></th>
<th><strong>인접 리스트</strong></th>
</tr>
</thead>
<tbody><tr>
<td>표현 방식</td>
<td>2차원 배열</td>
<td>배열의 배열 (리스트)</td>
</tr>
<tr>
<td>공간 복잡도</td>
<td>O(V²)</td>
<td>O(V + E)</td>
</tr>
<tr>
<td>간선 확인</td>
<td>O(1)</td>
<td>O(정점의 차수)</td>
</tr>
<tr>
<td>연결된 정점 순회</td>
<td>O(V)</td>
<td>O(정점의 차수)</td>
</tr>
<tr>
<td>장점</td>
<td>구현이 간단, 연결 여부 빠르게 확인</td>
<td>메모리 절약, 많은 간선에 유리</td>
</tr>
<tr>
<td>단점</td>
<td>메모리 낭비 가능</td>
<td>연결 여부 확인이 느림</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[undefined, null, NaN - 자바스크립트]]></title>
            <link>https://velog.io/@jaemin_ve/undefined-null-NaN-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8</link>
            <guid>https://velog.io/@jaemin_ve/undefined-null-NaN-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8</guid>
            <pubDate>Mon, 30 Jun 2025 07:03:43 GMT</pubDate>
            <description><![CDATA[<p>자바스크립트를 개발을 하다보면 <strong>값이 없거나 올바르지 않을 경우,</strong> undefined, null, NaN를 볼 수 있다.</p>
<p>이것들에 의미와 용도, 내부 처리 방식에서 중요한 차이점이 있습니다.</p>
<h2 id="undefined">undefined</h2>
<p><strong>undefined</strong>는 <strong>변수는 선언되었지만 값이 할당되지 않았을 때</strong> 자바스크립트 엔진이 자동으로 부여하는 값입니다.</p>
<pre><code class="language-jsx">let a;
console.log(a); // undefined</code></pre>
<p>또한, 다음의 경우에도 <strong>undefined</strong>가 발생합니다:</p>
<ul>
<li>함수에서 return 문이 없을 때</li>
<li>객체에서 존재하지 않는 속성에 접근할 때</li>
</ul>
<pre><code>function test() {}
console.log(test()); // undefined

const obj = {};
console.log(obj.name); // undefined</code></pre><blockquote>
<p>메모리 측면에서는 undefined는 가비지 컬렉션과 직접적인 관련이 없습니다. 단지 값이 정의되지 않았음을 표현할 뿐입니다.</p>
</blockquote>
<hr>
<h2 id="null">null</h2>
<p><strong>null</strong>은 <strong>개발자가 명시적으로 변수에 &quot;값이 없다&quot;는 것을 표현하기 위해 할당하는 값</strong>입니다.</p>
<pre><code>let user = null; // 현재 사용자 정보가 없음을 명확히 표현</code></pre><p>특히 메모리 해제와 관련이 있습니다. 예를 들어 대용량 객체를 더 이상 사용하지 않을 때, 해당 참조를 <code>null</code>로 설정하면 가비지 컬렉션 대상이 될 수 있습니다:</p>
<pre><code>let data = { /* 큰 데이터 */ };
data = null; // 더 이상 참조하지 않으므로 메모리에서 해제 가능</code></pre><hr>
<h2 id="undefined와-null-비교">undefined와 null 비교</h2>
<pre><code class="language-jsx">console.log(null == undefined) //true
console.log(null === undefined) //false</code></pre>
<hr>
<h2 id="nan-not-a-number">NaN (Not-a-Number)</h2>
<p><strong>NaN</strong>은 <strong>숫자 타입이지만 수학적으로 의미 없는 계산 결과를 나타냅니다</strong>.</p>
<pre><code>const result = 0 / 0;
console.log(result); // NaN

console.log(Number(&#39;abc&#39;)); // NaN</code></pre><p>특징:</p>
<ul>
<li><code>typeof NaN</code>은 <code>&#39;number&#39;</code></li>
<li>자기 자신과 같지 않은 유일한 값: <code>NaN === NaN</code>은 <code>false</code></li>
<li><code>isNaN(value)</code> 또는 <code>Number.isNaN(value)</code>로 확인해야 함</li>
</ul>
<pre><code>console.log(NaN === NaN); // false
console.log(Number.isNaN(NaN)); // true</code></pre><hr>
<h2 id="비교">비교</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>의미</th>
<th>typeof 결과</th>
<th>개발자가 직접 설정</th>
<th>가비지 컬렉션 관련</th>
</tr>
</thead>
<tbody><tr>
<td>undefined</td>
<td>값이 할당되지 않음</td>
<td>&quot;undefined&quot;</td>
<td>아니요</td>
<td>아니요</td>
</tr>
<tr>
<td>null</td>
<td>값이 없음을 의도적으로 설정</td>
<td>&quot;object&quot;</td>
<td>예</td>
<td>예</td>
</tr>
<tr>
<td>NaN</td>
<td>숫자가 아님</td>
<td>&quot;number&quot;</td>
<td>아니요</td>
<td>아니요</td>
</tr>
</tbody></table>
<h2 id="예상-면접-질문">예상 면접 질문</h2>
<h3 id="1-undefined와-null의-차이는-무엇인가요">1. undefined와 null의 차이는 무엇인가요?</h3>
<p>undefined는 값이 할당되지 않은 상태로, 자바스크립트 엔진이 자동으로 부여합니다. 반면 null은 값이 없음을 개발자가 의도적으로 표현하기 위해 사용하는 값입니다.</p>
<h3 id="2-null을-사용하는-이유는-무엇인가요">2 null을 사용하는 이유는 무엇인가요?</h3>
<p>null은 어떤 값이 의도적으로 &quot;비어있다&quot;는 것을 명확하게 표현할 수 있으며, 객체의 참조를 끊고 메모리 해제를 유도하는 데 사용할 수 있습니다.</p>
<h3 id="3-nan은-왜-typeof가-number인가요">3. NaN은 왜 typeof가 number인가요?</h3>
<p>NaN은 수학 연산 결과로 발생하며, JavaScript에서는 숫자형 연산의 실패를 나타내기 위해 NaN을 &quot;number&quot; 타입으로 정의했습니다.</p>
<h3 id="4-nan--nan이-false인-이유는">4. NaN === NaN이 false인 이유는?</h3>
<p>NaN은 계산 불능이라는 개념을 담고 있기 때문에 어떤 NaN 값도 자신과 같다고 판단하지 않습니다. 이는 IEEE 754 부동소수점 표준인 ‘NaN은 어떤 수와도 같지 않으며, <strong>자기 자신과도 같지 않다’</strong>를 따릅니다. 즉, NaN !== NaN은 자바스크립트 고유의 이상한 동작이 아니라, <strong>IEEE 754 표준에 따라 정상적인 동작</strong>입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Controlled Component와 Uncontrolled Component]]></title>
            <link>https://velog.io/@jaemin_ve/Controlled-Component%EC%99%80-Uncontrolled-Component</link>
            <guid>https://velog.io/@jaemin_ve/Controlled-Component%EC%99%80-Uncontrolled-Component</guid>
            <pubDate>Fri, 27 Jun 2025 06:07:43 GMT</pubDate>
            <description><![CDATA[<p>React에서는 <strong>input</strong>, <strong>textarea</strong>, <strong>select</strong> 같은 폼 요소를 사용할 때 두 가지 방식 중 하나로 데이터를 다룰 수 있습니다.</p>
<p>바로 <strong>Controlled Component(제어 컴포넌트)</strong> 와 <strong>Uncontrolled Component(비제어 컴포넌트)</strong> 입니다.</p>
<h2 id="controlled-component란"><strong>Controlled Component란?</strong></h2>
<p><strong>입력값의 상태(state)를 React가 직접 관리하는 컴포넌트를 의미합니다.</strong></p>
<p>입력값은 컴포넌트의 상태에 저장되고, 사용자의 입력은 이벤트 핸들러를 통해 상태를 업데이트합니다.</p>
<p>ex)</p>
<pre><code class="language-tsx">function ControlledInput() {
  const [value, setValue] = React.useState(&#39;&#39;);

  return (
    &lt;input
      type=&quot;text&quot;
      value={value}
      onChange={(e) =&gt; setValue(e.target.value)}
    /&gt;
  );
}</code></pre>
<ul>
<li><strong>완전한 React 기반 데이터 흐름</strong><ul>
<li>입력값을 상태(state)로 저장하여 언제든지 읽고, 수정하고, 검증할 수 있습니다.</li>
</ul>
</li>
<li><strong>실시간 유효성 검사/자동완성/조건부 렌더링 등에 적합</strong><ul>
<li>예: 이메일 입력이 완료되면 자동으로 확인 메시지 출력 등</li>
</ul>
</li>
<li><strong>장점</strong>: 상태 관리가 명확하고 테스트 용이</li>
<li><strong>단점</strong>: 입력에 대해 상태 갱신 → <strong>성능 부하 발생 가능</strong> (대량 필드일 경우 최적화 필요)</li>
</ul>
<h2 id="uncontrolled-component란"><strong>Uncontrolled Component란?</strong></h2>
<p><strong>DOM 자체에서 값을 직접 관리하는 컴포넌트를 의미합니다.</strong></p>
<p>입력값을 React state로 제어하지 않고, <strong>ref를 통해 DOM에 직접 접근</strong>하여 값을 읽거나 설정합니다.</p>
<p>ex)</p>
<pre><code class="language-tsx">function UncontrolledInput() {
  const inputRef = React.useRef(null);

  const handleClick = () =&gt; {
    alert(inputRef.current.value);
  };

  return (
    &lt;&gt;
      &lt;input type=&quot;text&quot; ref={inputRef} /&gt;
      &lt;button onClick={handleClick}&gt;입력값 보기&lt;/button&gt;
    &lt;/&gt;
  );
}</code></pre>
<ul>
<li><strong>React는 구조만 잡고 값은 DOM에게 위임</strong><ul>
<li>HTML <form> 태그처럼 브라우저가 기본 동작을 처리</li>
</ul>
</li>
<li><strong>간단한 폼, 외부 위젯, 파일 업로드</strong> 등에 적합</li>
<li><strong>ref를 통해 값에 접근하거나 초기값 설정만 하고 이후는 방치</strong></li>
<li><strong>장점</strong>: 성능 부담 적고 코드 간결</li>
<li><strong>단점</strong>: 유효성 검사가 어려움, React 상태와 비동기 동작이 어긋날 수 있음</li>
</ul>
<h2 id="특징-비교-표">특징 비교 표</h2>
<table>
<thead>
<tr>
<th><strong>항목</strong></th>
<th><strong>Controlled Component</strong></th>
<th><strong>Uncontrolled Component</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>값의 저장 위치</strong></td>
<td>React의 state로 저장</td>
<td>DOM 요소의 내부 값 (input.value)</td>
</tr>
<tr>
<td><strong>상태 제어 주체</strong></td>
<td>React가 직접 제어</td>
<td>브라우저(DOM)가 제어</td>
</tr>
<tr>
<td><strong>초기값 설정</strong></td>
<td>useState로 초기값 설정</td>
<td>defaultValue, defaultChecked 사용</td>
</tr>
<tr>
<td><strong>값 변경 방법</strong></td>
<td>onChange로 상태 업데이트</td>
<td>DOM을 통해 직접 변경되며 React는 관여하지 않음</td>
</tr>
<tr>
<td><strong>값 접근 방법</strong></td>
<td>상태 변수 (value)로 바로 접근 가능</td>
<td>ref.current.value로 DOM에 접근해야 함</td>
</tr>
<tr>
<td><strong>입력값 동기화</strong></td>
<td>항상 React 상태와 UI가 동기화</td>
<td>React와 입력값이 동기화되지 않음 (비일관성 가능성 존재)</td>
</tr>
<tr>
<td><strong>데이터 흐름 추적</strong></td>
<td>명확하고 예측 가능함 (단방향)</td>
<td>외부 변화가 React에 의해 추적되지 않음</td>
</tr>
<tr>
<td><strong>디버깅</strong></td>
<td>상태를 통해 언제든지 추적 가능</td>
<td>ref 접근 없이 값 추적 어려움</td>
</tr>
<tr>
<td><strong>코드 양</strong></td>
<td>상대적으로 많아질 수 있음</td>
<td>간단한 로직은 짧게 끝남</td>
</tr>
<tr>
<td><strong>성능</strong></td>
<td>입력마다 re-render 발생 (최적화 필요 시 useMemo 등 필요)</td>
<td>입력은 DOM이 처리 → 더 가볍지만 추적 불가</td>
</tr>
</tbody></table>
<h2 id="언제-controlled를-쓰고-언제-uncontrolled를-쓸까"><strong>언제 Controlled를 쓰고, 언제 Uncontrolled를 쓸까?</strong></h2>
<p>Controlled Component가 적합한 경우</p>
<ul>
<li>입력값을 즉시 검증해야 할 때 (e.g., 이메일 형식 검사)</li>
<li>여러 입력 요소를 조합해 상태를 하나로 관리할 때</li>
<li>입력값을 기반으로 동적으로 UI를 변경할 때</li>
</ul>
<p>Uncontrolled Component가 적합한 경우</p>
<ul>
<li>값이 굳이 React의 상태로 필요 없는 간단한 입력일 때</li>
<li>외부 라이브러리나 포커스 제어 등 DOM 직접 접근이 필요할 때</li>
<li>성능상 오버헤드를 줄이고 싶을 때 (예: 파일 업로드 등)</li>
</ul>
<h2 id="결론">결론</h2>
<p>Controlled Component는 데이터 흐름 관리와 유효성 검사가 중요한 경우에 유용하며, Uncontrolled Component는 단순한 폼이나 성능 최적화가 필요한 경우에 유용합니다. 상황에 따라 적절한 방식을 선택하여 사용되나, 개인적으로 성능이 정말 중요한 기능이 아니라면 state로 입력값을 관리하여 상태의 흐름을 제어하는 것이 좋을 것 같다.</p>
<h2 id="예상-면접-질문">예상 면접 질문</h2>
<h3 id="controlled-component와-uncontrolled-component의-차이점은-무엇인가요"><strong>Controlled Component와 Uncontrolled Component의 차이점은 무엇인가요?</strong></h3>
<p>Controlled Component는 입력값을 React의 state로 직접 제어하는 방식이고,
Uncontrolled Component는 DOM 자체가 입력값을 관리하며 React는 ref를 통해 접근하는 방식입니다.</p>
<h3 id="controlled-component와-uncontrolled-component-중-어느-것이-더-좋은가요"><strong>Controlled Component와 Uncontrolled Component 중 어느 것이 더 좋은가요?</strong></h3>
<p><strong>상황에 따라 다릅니다.</strong></p>
<p>React의 철학인 상태 기반 UI 관리에 부합하고, <strong>실시간 유효성 검사, 상태 동기화, 동적 폼 처리</strong> 등을 할 경우에는 <strong>Controlled Component가 더 적합합니다.</strong></p>
<p>반면, <strong>파일 업로드나 외부 라이브러리 연동</strong>, 또는 <strong>입력값 추적이 불필요한 간단한 폼</strong>에서는</p>
<p><strong>Uncontrolled Component가 코드도 간결하고 성능 측면에서도 유리합니다.</strong></p>
<blockquote>
<p>참고</p>
</blockquote>
<p><a href="https://velog.io/@white0_0/React-Uncontrolled-Component-Controlled-Component">https://velog.io/@white0_0/React-Uncontrolled-Component-Controlled-Component</a>
<a href="https://velog.io/@nanafromjeju/React-Controlled-Component%EC%99%80-Uncontrolled-Component-%EC%B0%A8%EC%9D%B4%EC%A0%90">https://velog.io/@nanafromjeju/React-Controlled-Component와-Uncontrolled-Component-차이점</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바스크립트 호이스팅]]></title>
            <link>https://velog.io/@jaemin_ve/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%98%B8%EC%9D%B4%EC%8A%A4%ED%8C%85</link>
            <guid>https://velog.io/@jaemin_ve/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%98%B8%EC%9D%B4%EC%8A%A4%ED%8C%85</guid>
            <pubDate>Thu, 26 Jun 2025 05:17:55 GMT</pubDate>
            <description><![CDATA[<p>자바스크립트에서 <strong>변수, 함수 선언이 해당 스코프의 최상단으로 끌어올려지는 동작</strong>을 말합니다.</p>
<p><strong>코드가 실행되기 전, 선언 부분이 먼저 처리되어 실행 컨텍스트의 초기화 단계에서 메모리</strong>에 등록되는 것이다.</p>
<h2 id="예제-1-변수-호이스팅"><strong>예제 1: 변수 호이스팅</strong></h2>
<pre><code class="language-tsx">console.log(a); // undefined
var a = 10;
console.log(a); // 10</code></pre>
<p>위의 코드는 다음과 같이 동작한다.</p>
<pre><code class="language-tsx">var a;          // 선언이 먼저 끌어올려짐
console.log(a); // undefined (초기화는 안 됨)
a = 10;
console.log(a);</code></pre>
<p> <code>var</code>로 선언된 변수는 선언과 초기화는 끌어올려지지만 값 할당은 끌어올려지지 않기 때문에, 값 할당이 이뤄지기 전까지는 <code>undefined</code>로 평가됩니다.</p>
<p><strong>let과 const는 왜 다르게 동작할까?</strong></p>
<pre><code class="language-tsx">console.log(b); // ❌ ReferenceError
let b = 5;</code></pre>
<p>let, const도 <strong>호이스팅은 되지만</strong>, var와 달리 <strong>“TDZ (Temporal Dead Zone, 일시적 사각지대)”</strong> 라는 개념 때문에 <strong>초기화 전에는 접근이 불가능</strong>합니다.</p>
<p>TDZ는 <strong>변수가 선언되었지만 초기화되기 전까지의 구간</strong>을 말합니다. TDZ는 코드에서 변수가 선언된 시점부터 초기화될 때까지의 구간에서 변수를 사용하지 못하게 막아주는 역할을 합니다.</p>
<p>이런 기능이 필요한 이유는 <strong>안정성과 디버깅 용이성 향상</strong>에 있다. <strong>초기화 순서를 엄격히 지킬 수 있어</strong></p>
<pre><code class="language-tsx">console.log(a); // undefined
var a = 10;</code></pre>
<p>와 같은 버그 가능성을 미연에 방지할 수 있다.</p>
<h2 id="예제-2-함수-선언식의-호이스팅"><strong>예제 2: 함수 선언식의 호이스팅</strong></h2>
<pre><code class="language-tsx">greet(); // ✅ 정상 실행
function greet() {
  console.log(&quot;Hello!&quot;);
}</code></pre>
<p>function 키워드로 선언된 함수는 <strong>전체 함수 정의가 통째로 호이스팅</strong>됩니다.</p>
<p>따라서 <strong>호출이 함수보다 앞에 있어도 문제 없이 실행됩니다.</strong></p>
<h3 id="함수-표현식의-경우는-다르다">함수 표현식의 경우는 다르다!</h3>
<pre><code class="language-tsx">sayHello(); // ❌ TypeError: sayHello is not a function

var sayHello = function() {
  console.log(&quot;Hi&quot;);
}</code></pre>
<p>sayHello는 변수로서 undefined가 먼저 할당되기 때문에, <strong>호이스팅 후 undefined()를 실행하려다 에러가 발생</strong>합니다.</p>
<h2 id="자바스크립트는-어떻게-호이스팅을-할까"><strong>자바스크립트는 어떻게 호이스팅을 할까?</strong></h2>
<p>자바스크립트 엔진은 코드를 아래의 두 단계로 나눠 처리합니다:</p>
<ol>
<li><strong>컴파일 단계 (Compile Phase)</strong></li>
<li><strong>실행 단계 (Execution Phase)</strong></li>
</ol>
<p>이 두 단계 중 <strong>“컴파일 단계”에서 선언이 먼저 처리되기 때문에</strong>, 호이스팅이라는 현상이 발생합니다.</p>
<h3 id="컴파일-단계">컴파일 단계</h3>
<p>코드가 실행되기 전, 자바스크립트 엔진은 먼저 코드를 스캔하여 <strong>변수 선언(var)과 함수 선언(function)을 찾아 미리 메모리에 등록</strong>합니다.</p>
<p>이 단계에서는 <strong>할당은 하지 않고</strong>, 단순히 <strong>“이 변수가 존재한다”는 사실만 기록합니다.</strong></p>
<p>선언은 이미 메모리에 존재하므로 오류는 발생하지 않지만, 아직 값은 없어서 <strong>undefined</strong>가 출력됩니다.</p>
<h3 id="실행-단계"><strong>실행 단계</strong></h3>
<p>컴파일이 끝나면 이제 <strong>실행 단계</strong>로 넘어가며,</p>
<p>앞서 선언된 변수와 함수들이 <strong>실제로 할당되고 실행</strong>됩니다.</p>
<h2 id="그럼-이런-문제-많은-호이스팅-왜-도입되었나">그럼 이런 문제 많은 호이스팅 왜 도입되었나?</h2>
<p>처음에 자바스크립트는 웹 브라우저에서 작은 스크립트를 실행하기 위해 설계됐습니다.</p>
<p>이때 변수나 함수를 선언한 위치에 상관없이 자유롭게 호출할 수 있어야 유연한 개발이 가능했다.</p>
<p>하지만 <strong>es6</strong>이후, 이런 유연함이 예측하기 어려운 버그를 발생시키기 때문에 <strong>let, const</strong>를 도입하게 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리액트란?]]></title>
            <link>https://velog.io/@jaemin_ve/%EB%A6%AC%EC%95%A1%ED%8A%B8%EB%9E%80-n60lzuay</link>
            <guid>https://velog.io/@jaemin_ve/%EB%A6%AC%EC%95%A1%ED%8A%B8%EB%9E%80-n60lzuay</guid>
            <pubDate>Tue, 24 Jun 2025 08:13:07 GMT</pubDate>
            <description><![CDATA[<p>프론트엔드 개발을 시작하면서 가장 많이 접하게 되는 라이브러리 중 하나가 <strong>React</strong>다.</p>
<p>“React가 무엇이고, 왜 쓰는 걸까?”</p>
<h3 id="react란"><strong>React란?</strong></h3>
<p>React <strong>JavaScript 라이브러리</strong> Meta에서 개발했고, 지금은 전 세계 다양한 웹 서비스에서 사용하고 있다.</p>
<p>React는 사용자 인터페이스(UI)를 구현하기 위한 라이브러리다.</p>
<ul>
<li><strong>선언형 (Declarative)</strong>: UI를 상태에 따라 <strong>무엇을 그려야 하는지 선언</strong>.</li>
<li><strong>컴포넌트 기반 (Component-Based)</strong>: UI를 <strong>재사용 가능한 컴포넌트 단위</strong>로 나눈다.</li>
<li><strong>Virtual DOM</strong>을 사용하여 성능을 최적화한다.</li>
</ul>
<h3 id="왜-react를-사용하는가"><strong>왜 React를 사용하는가?</strong></h3>
<ul>
<li>UI를 작은 단위로 나누어 관리하여 유지보수가 쉽다.</li>
<li>Virtual DOM을 통해 최소한의 실제 DOM 조작한다.</li>
<li>상태가 바뀌면 자동으로 UI가 반영되어 복잡한 로직을 단순화.</li>
</ul>
<h2 id="react의-핵심-요소"><strong>React의 핵심 요소</strong></h2>
<h3 id="1-jsx"><strong>1. JSX</strong></h3>
<ul>
<li>JavaScript 안에서 <strong>XML을 추가한 확장된 문법.</strong></li>
<li>createElement()를 대체하는 문법적 설탕</li>
</ul>
<pre><code class="language-tsx">const element = &lt;h1&gt;Hello React&lt;/h1&gt;;</code></pre>
<p>JSX는 Babel과 같은 컴파일러를 통해 JavaScript 코드로 변환됩니다.</p>
<pre><code class="language-tsx">React.createElement(&quot;h1&quot;, null, &quot;Hello React&quot;);</code></pre>
<h3 id="2-컴포넌트-component"><strong>2. 컴포넌트 (Component)</strong></h3>
<ul>
<li>UI의 <strong>재사용 가능한 단위</strong></li>
<li>함수형 컴포넌트(Function Component)가 주류</li>
</ul>
<pre><code class="language-tsx">function Welcome(props) {
  return &lt;h1&gt;Welcome, {props.name}&lt;/h1&gt;;
}</code></pre>
<h3 id="3-props-and-state"><strong>3. Props and State</strong></h3>
<p>React는 UI 상태를 state와 props를 통해 관리하며, 중요한 원칙 중 하나는 <strong>단방향 데이터 흐름</strong>이다.</p>
<p><strong>항상 부모에서 자식으로만 흐르기 때문에,</strong> </p>
<ul>
<li>데이터의 흐름이 명확하여 디버깅이 쉬움</li>
<li>유지보수성 향상</li>
<li>외부에 의존하지 않고 props만으로 작동(재사용 용이)</li>
</ul>
<table>
<thead>
<tr>
<th><strong>항목</strong></th>
<th><strong>Props</strong></th>
<th><strong>State</strong></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>UI 상태 관리</td>
</tr>
</tbody></table>
<h3 id="4-virtual-dom"><strong>4. Virtual DOM</strong></h3>
<p>메모리 내에 가상으로 존재하는 DOM을 의미한다.</p>
<ul>
<li><p>매번 변경 사항을 직접 DOM에 반영하는 것은 느리다.</p>
</li>
<li><p>React는 DOM을 직접 조작하지 않고 <strong>Virtual DOM</strong>이라는 <strong>메모리 상의 트리</strong>를 사용한다.</p>
</li>
<li><p>메모리에 있는 <strong>Virtual DOM을 변경하고 이전 Virtual DOM과 비교하여</strong>  변경사항만 최소한으로 실제 DOM에 반영하여 성능을 최적화한다.</p>
<h3 id="동작-흐름"><strong>동작 흐름</strong></h3>
<ol>
<li>상태 변화 발생</li>
<li>새로운 Virtual DOM 생성</li>
<li>이전 Virtual DOM과 비교(diff)</li>
<li>실제 DOM에 최소한의 변경만 적용</li>
</ol>
</li>
</ul>
<p>이런 비교 과정을 <strong>Reconciliation(재조정)</strong>이라고 하고 비교하는 알고리즘을 <strong>Diffing</strong>이라 한다.</p>
<blockquote>
<p>참고자료</p>
</blockquote>
<p><a href="https://velog.io/@jini_eun/React-React.js%EB%9E%80-%EA%B0%84%EB%8B%A8-%EC%A0%95%EB%A6%AC">https://velog.io/@jini_eun/React-React.js란-간단-정리</a></p>
<blockquote>
</blockquote>
<p><a href="https://cha-coding.tistory.com/entry/Reactjs-Reactjs-%EB%9E%80">https://cha-coding.tistory.com/entry/Reactjs-Reactjs-란</a></p>
<blockquote>
</blockquote>
<p><a href="https://velog.io/@jojeon4515/React-React%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80">https://velog.io/@jojeon4515/React-React란-무엇인가</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[이진 탐색]]></title>
            <link>https://velog.io/@jaemin_ve/%EC%9D%B4%EC%A7%84-%ED%83%90%EC%83%89</link>
            <guid>https://velog.io/@jaemin_ve/%EC%9D%B4%EC%A7%84-%ED%83%90%EC%83%89</guid>
            <pubDate>Tue, 24 Jun 2025 03:47:06 GMT</pubDate>
            <description><![CDATA[<p><strong>이진 탐색(Binary Search)</strong>은 <strong>정렬된 배열에서 원하는 값을 빠르게 찾는 알고리즘</strong>입니다. 시간 복잡도는 <strong>O(log n)</strong>으로 매우 효율적입니다.</p>
<hr>
<h2 id="1-이진-탐색-개념"><strong>1. 이진 탐색 개념</strong></h2>
<ul>
<li>배열이 <strong>정렬되어 있어야만</strong> 사용할 수 있음</li>
<li><strong>중간 값을 기준으로 절반씩 버리면서</strong> 탐색</li>
<li>목표 값이 중간보다 작으면 왼쪽 범위로, 크면 오른쪽 범위로 이동</li>
</ul>
<pre><code class="language-tsx">function binarySearch(arr, target) {
  let left = 0;
  let right = arr.length - 1;

  while (left &lt;= right) {
    const mid = Math.floor((left + right) / 2);

    if (arr[mid] === target) return mid;
    if (arr[mid] &lt; target) left = mid + 1;
    else right = mid - 1;
  }

  return -1; // 찾지 못했을 경우
}</code></pre>
<h2 id="2-응용">2. 응용</h2>
<p><strong>배열에 같은 값이 여러 개 있을 때</strong>, 그중에서도 <strong>특정 위치(가장 왼쪽 또는 가장 오른쪽)</strong> 값을 찾고 싶을 때 사용하는 이진 탐색 응용입니다.</p>
<p><strong>가장 왼쪽 인덱스 찾기</strong></p>
<pre><code class="language-tsx">function lowerBound(arr, target) {
  let left = 0;
  let right = arr.length - 1;
  let result = -1;

  while (left &lt;= right) {
    let mid = Math.floor((left + right) / 2);
    if (arr[mid] === target) {
      result = mid;
      right = mid - 1; // 왼쪽으로 더 찾아보기
    } else if (arr[mid] &lt; target) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }
  return result;
}</code></pre>
<p><strong>가장 오른쪽 인덱스 찾기</strong></p>
<pre><code class="language-tsx">function upperBound(arr, target) {
  let left = 0;
  let right = arr.length - 1;
  let result = -1;

  while (left &lt;= right) {
    let mid = Math.floor((left + right) / 2);
    if (arr[mid] === target) {
      result = mid;
      left = mid + 1; // 오른쪽으로 더 찾아보기
    } else if (arr[mid] &lt; target) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }
  return result;
}</code></pre>
<p><strong>Lower Bound :  target이상의 같이 최초로 나오는 위치.</strong></p>
<pre><code class="language-tsx">/**
 * lowerBound: 정렬된 배열에서 target 이상의 값이
 * 처음 나타나는 인덱스를 반환한다.
 * (C++ std::lower_bound와 동일한 동작)
 *
 * @param {number[]} arr   오름차순으로 정렬된 배열
 * @param {number}   target 찾고자 하는 기준 값
 * @returns {number}  조건을 만족하는 최소 인덱스
 *                    └ 없으면 arr.length (삽입 위치)
 */
function lowerBound(arr, target) {
  let left = 0;
  let right = arr.length - 1;
  let minIdx = arr.length;          // 기본값: 배열 뒤 (없을 때 삽입 위치)

  while (left &lt;= right) {
    const mid = Math.floor((left + right) / 2);

    if (arr[mid] &gt;= target) {       // target 이상이면 후보
      minIdx = Math.min(minIdx, mid);
      right = mid - 1;              // 더 왼쪽에 있을지 탐색
    } else {
      left = mid + 1;               // target보다 작으므로 오른쪽으로
    }
  }

  return minIdx;
}

/* 사용 예시 */
const nums = [2, 4, 4, 6, 8, 10];
console.log(lowerBound(nums, 5)); // 3  (값 6이 있는 위치)
console.log(lowerBound(nums, 4)); // 1  (첫 번째 4)
console.log(lowerBound(nums, 11)); // 6 (배열 길이 → 삽입 위치)</code></pre>
<p>Upper Bound : target을 초과하는 값이 최초로 나오는 위치</p>
<pre><code class="language-tsx">/**
 * upperBound: 정렬된 배열에서 target 초과 값이
 * 처음 나타나는 인덱스를 반환한다.
 * (C++ std::upper_bound와 동일한 동작)
 *
 * @param {number[]} arr    오름차순 정렬된 배열
 * @param {number}   target 초과 기준 값
 * @returns {number} 조건을 만족하는 최소 인덱스 (없으면 arr.length)
 */
function upperBound(arr, target) {
  let left = 0;
  let right = arr.length - 1;
  let minIdx = arr.length;

  while (left &lt;= right) {
    const mid = Math.floor((left + right) / 2);

    if (arr[mid] &gt; target) {
      minIdx = Math.min(minIdx, mid); // 후보 저장
      right = mid - 1;                // 더 왼쪽도 탐색
    } else {
      left = mid + 1;                 // target 이하 → 오른쪽 탐색
    }
  }

  return minIdx;
}</code></pre>
<p>target보다 같거나 작은 숫자들의 위치중 가장 큰 위치</p>
<pre><code class="language-tsx">function customBound(arr, target){
  let left = 0;
  let right = arr.length;
  let mid_index = -1

  while(left &lt;= right){
    const mid = Math.floor((left + right) / 2)

    if(arr[mid] &lt;= target){
      left = mid+1;
      mid_index = Math.max(mid_index, mid)
    }else{
      right = mid - 1
    }
  }
  return mid_index
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[클로저]]></title>
            <link>https://velog.io/@jaemin_ve/%ED%81%B4%EB%A1%9C%EC%A0%80</link>
            <guid>https://velog.io/@jaemin_ve/%ED%81%B4%EB%A1%9C%EC%A0%80</guid>
            <pubDate>Mon, 23 Jun 2025 06:37:30 GMT</pubDate>
            <description><![CDATA[<p><strong>클로저</strong>는 함수가 선언될 때의 스코프를 기억하여, 함수가 생성된 이후에도 그 스코프에 접근할 수 있는 기능을 말한다.</p>
<p>클로저는 자바스크립트의 <strong>함수가 일급 객체라는 특성</strong>과 <strong>렉시컬 스코프</strong>의 조합으로 만들어 진다.</p>
<h2 id="예시-코드">예시 코드</h2>
<pre><code class="language-tsx">function makeCounter() {
  let count = 0; // 🔐 프라이빗 변수
  return function () {
    return ++count;
  };
}

const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2</code></pre>
<ul>
<li><code>makeCounter()</code> 실행 → 내부 변수 <code>count</code>가 생성</li>
<li>내부에서 <strong>익명 함수</strong>를 반환 ⇒ 반환된 함수는 <strong><code>count</code>가 있는 렉시컬 환경을 기억</strong></li>
<li>이후 <code>counter()</code> 호출 시마다 <code>count</code>를 증가시키며 상태 유지</li>
</ul>
<h2 id="클로저가-필요한-이유"><strong>클로저가 필요한 이유</strong></h2>
<ol>
<li> <strong>데이터 은닉 (Encapsulation)</strong></li>
</ol>
<pre><code class="language-tsx">function createCounter() {
  let count = 0;
  return {
    increment() { count++; return count; },
    decrement() { count--; return count; },
  }
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2</code></pre>
<p>이 예시에서는 외부에서 <code>count</code>에 직접 접근하거나 변경할 수 없다. <code>createCounter</code> 내부에서만 접근 가능한 <code>count</code>는 클로저 덕분에 유지된다.</p>
<p>이로 인해 중요한 상태를 외부에서 직접 수정하지 못하도록 보호할 수 있다.</p>
<ol>
<li> <strong>상태 유지</strong> </li>
</ol>
<p>클로저를 이용하면 함수 호출 이후에도 특정 상태를 계속 유지할 수 있다.</p>
<p>이는 UI 컴포넌트, 애니메이션, 이벤트 핸들러 등에 자주 사용된다.</p>
<pre><code class="language-tsx">function makeGreeting(name) {
  return function () {
    console.log(`Hi, ${name}`);
  }
}

const greetKim = makeGreeting(&#39;김철수&#39;);
greetKim(); // Hi, 김철수</code></pre>
<p>함수가 실행된 이후에도 <code>name</code>이라는 상태를 잃지 않고 유지합니다.</p>
<ol>
<li><strong>모듈화를 구현하는데 사용</strong></li>
</ol>
<p>모듈화는 특정 기능을 캡슐화하고, 외부에 공개하고자 하는 부분만 선택적으로 노출하여 코드의 응집력을 높이고, 유지보수성을 향상시킬 수 있다. 클로저를 활용하면 필요한 함수와 데이터만 외부로 노출함으로써 모듈 패턴을 쉽게 구현할 수 있다.</p>
<h2 id="react에서-클로저-사용-예">React에서 클로저 사용 예</h2>
<p><strong>useState, useEffect, useCallback 내부</strong></p>
<p>React의 훅들은 클로저를 적극적으로 사용한다.</p>
<p><strong>useCallback 예시:</strong></p>
<pre><code class="language-tsx">const MyComponent = () =&gt; {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() =&gt; {
    console.log(`현재 count: ${count}`);
  }, [count]);

  return &lt;button onClick={handleClick}&gt;클릭&lt;/button&gt;;
}</code></pre>
<p>이 <code>handleClick</code> 함수는 렌더링 시점의 <code>count</code> 값을 클로저로 캡처합니다. 즉, 해당 시점의 스냅샷을 기억한다.</p>
<p><strong>주의점</strong></p>
<p>클로저로 인해 오래된 값이 기억되는 <strong>Stale Closure 문제</strong>가 발생할 수 있다. 이를 해결하기 위해 <code>useRef</code>, <code>useEffect</code>의 dependency array를 관리한다.</p>
<h2 id="예상-면접-질문">예상 면접 질문</h2>
<p><strong>1. 클로저란 무엇인가요?</strong></p>
<p>클로저는 함수가 선언될 당시의 렉시컬 스코프를 기억하여, 함수가 그 스코프 밖에서 호출되더라도 내부 변수에 접근할 수 있도록 하는 자바스크립트의 기능이다.</p>
<p><strong>2. 클로저는 메모리에 어떤 영향을 주나요?</strong></p>
<p>클로저는 함수가 참조하는 외부 변수들을 메모리에 유지시키기 때문에, 해당 변수가 더 이상 필요하지 않아도 참조가 남아 있다면 <strong>가비지 컬렉션(GC)</strong> 대상이 되지 않아 메모리 누수를 유발할 수 있습니다. 따라서 이벤트 핸들러, 타이머 등에서는 참조를 해제해주는 것이 중요하다.</p>
<p><strong>3. 클로저를 활용한 데이터 은닉 또는 상태 유지 방법은?</strong></p>
<p>클로저를 사용하면 외부에서 직접 접근할 수 없는 프라이빗 변수를 생성할 수 있다. 예를 들어, 클로저 내부에 <code>let count = 0</code>과 같은 상태를 유지시키고, 이 변수에 접근하는 인터페이스만 반환함으로써 상태를 은닉할 수 있다.</p>
<p><strong>4. React에서 클로저가 문제를 일으키는 경우는? → stale closure</strong></p>
<p>함수형 컴포넌트에서는 렌더링 시점의 상태나 props를 클로저로 캡처하기 때문에, 나중에 호출될 때 <strong>이전 값(old snapshot)</strong> 을 참조하게 되는 문제, 즉 <code>stale closure</code>가 발생할 수 있다. 이를 방지하기 위해 <code>useEffect</code>의 의존성 배열이나 함수형 업데이트(<code>prev =&gt; ...</code>)를 사용한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[1급 객체]]></title>
            <link>https://velog.io/@jaemin_ve/1%EA%B8%89-%EA%B0%9D%EC%B2%B4</link>
            <guid>https://velog.io/@jaemin_ve/1%EA%B8%89-%EA%B0%9D%EC%B2%B4</guid>
            <pubDate>Sun, 22 Jun 2025 05:50:24 GMT</pubDate>
            <description><![CDATA[<p>자바스크립트를 제대로 이해하기 위해서는 &#39;1급 객체(First-Class Object)&#39;라는 개념을 알아야 한다.</p>
<h2 id="1급-객체의-정의">1급 객체의 정의</h2>
<p>프로그래밍 언어에서 어떤 요소가 다음 세 가지 조건을 만족할 때, 이를 &#39;1급 객체&#39;라고 한다.</p>
<ul>
<li><strong>변수나 데이터 구조(배열, 객체 등)에 할당 가능하다.</strong></li>
<li><strong>함수의 파라미터로 전달할 수 있다.</strong></li>
<li><strong>함수의 반환값으로 사용될 수 있다.</strong></li>
</ul>
<p>자바스크립트에서 함수는 이 모든 조건을 충족하므로 1급 객체로 간주된다.</p>
<h3 id="1-변수나-데이터-구조에-할당-가능">1. 변수나 데이터 구조에 할당 가능</h3>
<p>자바스크립트에서는 함수를 변수에 할당할 수 있다.</p>
<pre><code class="language-tsx">const greet = function(name) {
  return `Hello, ${name}`;
};

console.log(greet(&quot;Alice&quot;)); // Hello, Alice</code></pre>
<p>또한 배열이나 객체에도 함수를 저장할 수 있다.</p>
<pre><code class="language-tsx">const arr = [
  function(a, b) { return a + b; },
  function(a, b) { return a - b; }
];

console.log(arr[0](5, 3)); // 8
console.log(arr[1](5, 3)); // 2</code></pre>
<h3 id="2-함수의-파라미터로-전달-가능">2. 함수의 파라미터로 전달 가능</h3>
<p>자바스크립트 함수는 다른 함수에 인자로 전달될 수 있다.</p>
<pre><code class="language-tsx">function greet(name, formatter) {
  return formatter(name);
}

function formalGreeting(name) {
  return `안녕하세요, ${name} 님`;
}

function casualGreeting(name) {
  return `안녕, ${name}!`;
}

console.log(greet(&quot;Alice&quot;, formalGreeting)); // 안녕하세요, Alice 님
console.log(greet(&quot;Bob&quot;, casualGreeting)); // 안녕, Bob!</code></pre>
<h3 id="3-함수의-반환값으로-사용-가능">3. 함수의 반환값으로 사용 가능</h3>
<p>자바스크립트에서는 함수가 다른 함수를 반환하는 형태로도 사용할 수 있다.</p>
<pre><code class="language-tsx">function createMultiplier(x) {
  return function(y) {
    return x * y;
  };
}

const multiplyByTwo = createMultiplier(2);
console.log(multiplyByTwo(5)); // 10</code></pre>
<h2 id="1급-객체로서의-함수가-중요한-이유">1급 객체로서의 함수가 중요한 이유</h2>
<p>자바스크립트가 함수를 1급 객체로 취급하기 때문에 얻는 주요 이점은 다음과 같다.</p>
<ul>
<li><strong>함수형 프로그래밍</strong>: 함수를 인자와 리턴값으로 자유롭게 활용하여 복잡한 로직을 단순화할 수 있다.</li>
<li><strong>콜백 함수</strong>: 비동기 프로그래밍이나 이벤트 핸들러와 같은 콜백 함수 구현이 간편해진다.</li>
<li><strong>코드의 재사용성과 유연성 증가</strong>: 함수가 독립적인 객체로 다루어지기 때문에 유연한 코드 작성이 가능하다.</li>
</ul>
<p><strong>etc)</strong> </p>
<p><strong>고차 함수(Higher-Order Function)</strong></p>
<p>고차 함수란 다음 중 하나 이상을 만족하는 함수:</p>
<ul>
<li>하나 이상의 <strong>함수를 인자로 전달받는 함수</strong></li>
<li><strong>함수를 반환하는 함수</strong></li>
</ul>
<p><strong>콜백 함수(Callback Function)</strong></p>
<p>콜백 함수는 <strong>다른 함수에 인자로 전달되어 나중에 호출되는 함수</strong></p>
<h2 id="예상-면접-질문">예상 면접 질문</h2>
<p><strong>자바스크립트에서 함수가 1급 객체라는 건 무슨 의미인가요?</strong></p>
<p> 변수에 할당, 인자로 전달, 리턴값으로 사용이 가능하다는 의미입니다.</p>
<p><strong>고차 함수와 콜백 함수의 차이는 무엇인가요?</strong></p>
<p>고차 함수는 함수를 인자로 받거나 반환하는 함수이고, 콜백 함수는 고차 함수에 인자로 전달되어 나중에 호출되는 함수입니다.</p>
<p><strong>실제 사용 예시는 어떤 게 있나요?</strong></p>
<p> Array.prototype.map, filter, reduce, setTimeout, addEventListener 등은 모두 고차 함수이며, 이들에 전달되는 함수는 콜백 함수입니다.</p>
<blockquote>
<p>출처</p>
</blockquote>
<p><a href="https://velog.io/@reveloper-1311/%EC%9D%BC%EA%B8%89-%EA%B0%9D%EC%B2%B4First-Class-Object%EB%9E%80">https://velog.io/@reveloper-1311/일급-객체First-Class-Object란</a></p>
<p><a href="https://inpa.tistory.com/entry/CS-%F0%9F%91%A8%E2%80%8D%F0%9F%92%BB-%EC%9D%BC%EA%B8%89-%EA%B0%9D%EC%B2%B4first-class-object#1._%EB%B3%80%EC%88%98%EB%82%98_%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%90_%EB%8B%B4%EC%9D%84_%EC%88%98_%EC%9E%88%EC%96%B4%EC%95%BC_%ED%95%9C%EB%8B%A4.-1">https://inpa.tistory.com/entry/CS-👨‍💻-일급-객체first-class-object#1._변수나_데이터에_담을<em>수</em>있어야_한다.-1</a></p>
<p><a href="https://developer.mozilla.org/ko/docs/Glossary/First-class_Function">https://developer.mozilla.org/ko/docs/Glossary/First-class_Function</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스코프(Scope)란?]]></title>
            <link>https://velog.io/@jaemin_ve/%EC%8A%A4%EC%BD%94%ED%94%84Scope%EB%9E%80</link>
            <guid>https://velog.io/@jaemin_ve/%EC%8A%A4%EC%BD%94%ED%94%84Scope%EB%9E%80</guid>
            <pubDate>Thu, 19 Jun 2025 10:28:38 GMT</pubDate>
            <description><![CDATA[<p><strong>스코프</strong>란 <em>“변수와 함수에 접근할 수 있는 범위”</em> 를 의미한다.</p>
<p>자바스크립트에서 스코프는 크게 <strong>전역 스코프(Global Scope)</strong>, <strong>함수 스코프(Function Scope)</strong>, <strong>블록 스코프(Block Scope)</strong>로 구분된다.</p>
<h2 id="글로벌-스코프">글로벌 스코프</h2>
<p>전역 스코프에 선언된 변수는 <strong>어디서든 접근 가능</strong>하다.</p>
<pre><code class="language-tsx">var globalVar = &quot;전역 변수&quot;;

function checkScope() {
    console.log(globalVar); // &quot;전역 변수&quot; 출력
}

checkScope();
console.log(globalVar); // &quot;전역 변수&quot; 출력</code></pre>
<h2 id="지역-스코프">지역 스코프</h2>
<p>특정 함수에 해당하는 <strong>스코프로 해당 함수 자신과 하위 함수</strong>에서만 접근이 가능하다.</p>
<p>자바스크립트에서 지역 스코프는 <strong>함수 스코프(Function Scope)</strong> 또는 <strong>블록 스코프(Block Scope)</strong>를 통칭한다.</p>
<h2 id="함수-스코프">함수 스코프</h2>
<p><strong>함수 내부에서 선언된 변수는 그 함수 내에서만 유효</strong>하며, 외부에서는 접근할 수 없다.</p>
<pre><code class="language-tsx">function greet() {
  const message = &#39;Hello!&#39;;
  console.log(message); // Hello!
}

greet();
console.log(message); // ❌ ReferenceError</code></pre>
<blockquote>
<p>var 키워트도 함수 스코프를 따른다.</p>
</blockquote>
<pre><code class="language-tsx">function test() {
  if (true) {
    var x = 5;
  }
  console.log(x); // 5 (var는 함수 스코프)
}</code></pre>
<h2 id="블록-스코프-block-scope"><strong>블록 스코프 (Block Scope)</strong></h2>
<p>let, const 키워드로 선언된 변수는 <strong>블록({}) 내부에서만 접근 가능</strong>하다.</p>
<pre><code class="language-tsx">if (true) {
  const message = &#39;Block scope!&#39;;
  let count = 3;
  console.log(message); // Block scope!
}

console.log(message); // ❌ ReferenceError
console.log(count);   // ❌ ReferenceError</code></pre>
<h2 id="스코프-체인scope-chain"><strong>스코프 체인(Scope Chain)</strong></h2>
<p>자바스크립트는 <strong>안쪽 스코프에서 바깥 스코프로 거슬러 올라가며 변수 참조를 탐색</strong>합니다.</p>
<pre><code class="language-tsx">const a = 1;

function outer() {
  const b = 2;

  function inner() {
    const c = 3;
    console.log(a, b, c); // 1 2 3
  }

  inner();
}

outer();</code></pre>
<h2 id="렉시컬-스코프lexical-scope"><strong>렉시컬 스코프(Lexical Scope)</strong></h2>
<p>자바스크립트는 <strong>렉시컬 스코프(Lexical Scope)</strong> 를 따릅니다. 즉, <strong>함수가 어디서 호출되었는지가 아니라, 어디서 선언되었는지</strong>에 따라 스코프가 결정된다.</p>
<pre><code class="language-tsx">const value = &#39;global&#39;;

function print() {
  console.log(value);
}

function run() {
  const value = &#39;local&#39;;
  print(); // global
}

run();</code></pre>
<h2 id="기술-면접-예상-질문">기술 면접 예상 질문</h2>
<ol>
<li><p><strong>자바스크립트에서 스코프란 무엇인가요?</strong></p>
<p> 스코프란 특정 변수나 함수에 접근할 수 있는 <strong>범위(scope)</strong>를 말합니다.</p>
<p> 자바스크립트에서는 전역 스코프, 함수 스코프, 블록 스코프가 있으며, 변수나 함수가 선언된 위치에 따라 접근 가능한 범위가 달라진다.</p>
</li>
</ol>
<ol start="2">
<li><p><strong>함수 스코프와 블록 스코프의 차이를 설명해주세요.</strong></p>
<p> 함수 스코프는 함수 전체를 범위로 가지며, var로 선언된 변수는 함수 내 어디서든 접근할 수 있습니다.</p>
<p> 반면, 블록 스코프는 중괄호 {} 로 구분된 코드 블록을 범위로 하며, let, const로 선언된 변수는 해당 블록 내부에서만 접근 가능하다.</p>
</li>
</ol>
<ol start="3">
<li><p><strong>var, let, const의 스코프 차이를 설명해보세요.</strong></p>
<p> var는 함수 스코프를 따르고, let과 const는 블록 스코프를 따른다.</p>
<p> 따라서 var로 선언한 변수는 블록 외부에서도 접근이 가능하지만, let과 const는 블록 내부에서만 유효하다.</p>
<p> 또한 const는 let과 다르게 재할당이 불가능하다. 그래서 선언과 동시에 초기화가 필요하다.</p>
</li>
</ol>
<ol start="4">
<li><p><strong>렉시컬 스코프란 무엇인가요?</strong></p>
<p> 자바스크립트는 <strong>렉시컬 스코프</strong>를 따른다.</p>
<p> 이는 함수가 어디서 <strong>호출되었는지가 아닌, 선언된 위치</strong>를 기준으로  스코프가 결정된다.</p>
<p> 따라서 함수 내부에서 외부 변수를 참조할 때는 호출 위치가 아닌 정의된 위치의 스코프 체인을 따라 탐색하게 된다..</p>
</li>
</ol>
<ol start="5">
<li><p><strong>스코프 체인이란 무엇인가요?</strong>
자바스크립트에서 변수를 참조할 때, 현재 스코프에 없으면 <strong>바깥 스코프로 거슬러 올라가며 찾는 구조</strong>를 스코프 체인이라고 한다.</p>
<p> 이는 중첩된 함수에서 외부 함수의 변수에 접근할 수 있는 이유이기도 합니다.</p>
</li>
</ol>
<ol start="6">
<li><strong>다음 코드에서 출력 결과를 설명해주세요.</strong></li>
</ol>
<pre><code>```tsx
const x = &#39;global&#39;;

function outer() {
  const x = &#39;outer&#39;;

  function inner() {
    console.log(x);
  }

  inner();
}

outer();
```

출력 결과는 &#39;outer&#39;.

이는 자바스크립트의 렉시컬 스코프에 따라 inner 함수는 outer 함수 내부에서 정의되었기 때문에, x를 찾을 때 outer의 스코프를 먼저 확인하게 됩니다.</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[자바스크립트에서 동기와 비동기]]></title>
            <link>https://velog.io/@jaemin_ve/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%97%90%EC%84%9C-%EB%8F%99%EA%B8%B0%EC%99%80-%EB%B9%84%EB%8F%99%EA%B8%B0</link>
            <guid>https://velog.io/@jaemin_ve/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%97%90%EC%84%9C-%EB%8F%99%EA%B8%B0%EC%99%80-%EB%B9%84%EB%8F%99%EA%B8%B0</guid>
            <pubDate>Tue, 17 Jun 2025 04:00:18 GMT</pubDate>
            <description><![CDATA[<h2 id="1-동기와-비동기">1. 동기와 비동기</h2>
<h3 id="동기synchronous">동기(synchronous)</h3>
<ul>
<li>태스크가 <strong>순차적으로(직렬적으로)</strong> 실행됨. 하나의 작업이 끝나야 다음 작업 수행 가능.</li>
<li>태스크가 끝날 때가지 대기하기 때문에 비효울적</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jaemin_ve/post/9f3bfa67-97c7-4bd7-933f-fcb02b7b075c/image.png" alt=""></p>
<pre><code class="language-tsx">console.log(&#39;A&#39;);
console.log(&#39;B&#39;);

//실행결과
A
B</code></pre>
<h3 id="비동기">비동기</h3>
<ul>
<li>태스크가 병렬적으로 실행됨.</li>
<li>어떤 작업을 요청한 후, 완료될 때까지 <strong>기다리지 않고 다음 작업을 먼저 실행</strong>함. 결과는 나중에 받음.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jaemin_ve/post/d057f3f6-1c7b-4a40-b51f-a6fe471634db/image.png" alt=""></p>
<pre><code class="language-tsx">console.log(&#39;A&#39;);
setTimeout(() =&gt; {
  console.log(&#39;B&#39;);
}, 1000);
console.log(&#39;C&#39;);

//
A
C
(1초 후) B</code></pre>
<h2 id="2-비동기-처리의-도구들"><strong>2. 비동기 처리의 도구들</strong></h2>
<p>setTimeout, XMLHttpRequest, fetch, EventListener, WebSocket 등은 대표적인 <strong>비동기 함수</strong></p>
<p>과거 콜백함수를 이용하여 비동기 작업을 처리. </p>
<p>콜백 지옥으로 인해 <strong>가독성 저하</strong> 및 <strong>에러 처리 어려움</strong> 등으로 유지보수가 힘들다.</p>
<pre><code class="language-tsx">login(user, (userInfo) =&gt; {
  getProfile(userInfo.id, (profile) =&gt; {
    getPosts(profile.id, (posts) =&gt; {
      console.log(posts);
    });
  });
});</code></pre>
<p>이를 개선하기 위해 <strong>Promise → async/await</strong> 등장</p>
<h2 id="3-promise란">3. Promise란?</h2>
<ul>
<li>ES6에서는 비동기 처리를 위해 새롭게 <strong>프로미스(Promise)</strong>를 도입.</li>
<li><strong>프로미스</strong>는 미래에 결과를 반환할 비동기 작업의 상태와 결과를 표현하는 객체</li>
</ul>
<pre><code class="language-jsx">const promise = new Promise((resolve, reject) =&gt; {
  // 비동기 작업
  setTimeout(() =&gt; {
    resolve(&#39;성공!&#39;);
  }, 1000);
});

promise
  .then(result =&gt; console.log(result))   // → 성공!
  .catch(error =&gt; console.error(error)) 
  .finally(() =&gt; {
    console.log(&#39;항상 실행됨&#39;);
  });</code></pre>
<p>Promise는 비동기 처리가 성공 여부에 따라 다양한 상태(state)를 가진다.</p>
<ul>
<li><code>pending</code>: 비동기 처리가 아직 수행되지 않은 상태</li>
<li><code>fulfilled</code>: 비동기 처리가 수행된 상태 (성공)</li>
<li><code>rejected</code>: 비동기 처리가 수행된 상태 (실패)</li>
<li><code>settled</code>: 비동기 처리가 수행된 상태 (성공 또는 실패)</li>
</ul>
<h3 id="promise-호출-과정">Promise 호출 과정</h3>
<ol>
<li><strong>Promise 객체 생성</strong></li>
</ol>
<pre><code>```tsx
const promise = new Promise((resolve, reject) =&gt; {
  // 비동기 작업 수행
});
```

이 시점에서 **promise 객체는 pending 상태**입니다.

resolve(value)를 호출하면 **fulfilled 상태로 전이**

reject(reason)을 호출하면 **rejected 상태로 전이**</code></pre><ol start="2">
<li><p><strong>비동기 작업 수행 (예: setTimeout, fetch 등)</strong></p>
<pre><code class="language-tsx"> const promise = new Promise((resolve, reject) =&gt; {
   setTimeout(() =&gt; {
     resolve(&#39;작업 성공&#39;);
   }, 1000);
 });
</code></pre>
<p> resolve()나 reject()는 <strong>작업이 끝났을 때 “결과”를 전달하는 역할</strong></p>
<p> 여기선 1초 뒤 resolve()가 실행되므로 <strong>1초 후에 fulfilled 상태가 됨</strong></p>
</li>
<li><p><strong>.then()으로 성공 처리 연결</strong></p>
<pre><code class="language-tsx"> promise.then(result =&gt; {
   console.log(result);  // → &quot;작업 성공&quot;
 });</code></pre>
<p> promise가 fulfilled 상태로 바뀌면, 등록된 .then() 콜백이 호출됨. <strong>then 메소드는</strong> Promise를 반환.</p>
<p> result는 resolve()에서 전달한 값</p>
</li>
<li><p><strong>.catch()으로 에러 처리 연결</strong></p>
<pre><code class="language-tsx"> promise
   .then(result =&gt; {
     console.log(result);
   })
   .catch(error =&gt; {
     console.error(&#39;에러 발생:&#39;, error);
   });</code></pre>
<p> reject()가 호출되었거나 .then() 내부에서 에러가 나면 .catch()가 실행됨. catch 메소드는 Promise를 반환.</p>
<p> error는 reject()에서 전달한 값</p>
</li>
<li><p><strong>.finally으로 성공여부 상관없이 항상실행</strong></p>
<pre><code class="language-tsx"> .catch(error =&gt; {
     console.error(&#39;에러 발생:&#39;, error);
   })
 .finally(() =&gt; {
     console.log(&#39;항상 실행됨&#39;);
   });</code></pre>
<p> 결과에 상관없이 항상 실행됨</p>
<p> finally()는 <strong>값을 전달하지 않음</strong> (다음 then()으로는 이전 then()의 결과가 넘어감)</p>
</li>
</ol>
<p>프로미스는 후속 처리 메소드인 then, catch, finally <strong>메소드를 체이닝(chainning)</strong>하여 여러 개의 프로미스를 연결하여 사용할 수 있다. 이런 방식을 이용해 콜백 헬을 해결한다.</p>
<h3 id="promise-중간-디버그-흐름">Promise 중간 디버그 흐름</h3>
<pre><code class="language-tsx">const promise = new Promise((resolve, reject) =&gt; {
  console.log(&#39;🟡 Promise 시작&#39;);        // 즉시 실행됨
  setTimeout(() =&gt; {
    console.log(&#39;🟢 비동기 완료 → resolve 호출&#39;);
    resolve(&#39;완료&#39;);
  }, 1000);
});

console.log(&#39;🟠 .then 등록 전&#39;);

promise.then(res =&gt; {
  console.log(&#39;🔵 then 실행:&#39;, res);
});

console.log(&#39;🔴 .then 등록 완료&#39;);

//출력
🟡 Promise 시작
🟠 .then 등록 전
🔴 .then 등록 완료
🟢 비동기 완료 → resolve 호출
🔵 then 실행: 완료</code></pre>
<h2 id="4-async--await">4. Async / Await</h2>
<h3 id="async-await-이해">async await 이해</h3>
<p>async/await는 Promise 기반 비동기 코드를 <strong>동기식처럼 읽기 쉽게 작성</strong>할 수 있게 해줍니다.</p>
<pre><code class="language-jsx">async function 함수명() {
  await 비동기_처리_메서드_명();
}</code></pre>
<p><strong>async 함수란?</strong></p>
<ul>
<li><strong>async 함수는 항상 프라미스를 반환</strong>한다. 일반 값을 반환하면 자동으로 Promise.resolve(value)로 감싸진다.</li>
<li>아래 예시의 함수를 호출하면 result가 1인 이행 프라미스가 반환된다.</li>
</ul>
<pre><code class="language-jsx">async function example() {
  return 1;
}
example().then(console.log); // 1</code></pre>
<p><strong>await란?</strong></p>
<ul>
<li><strong>async 함수 내부에서만 사용 가능하다</strong></li>
<li>await promise 형태로 사용하며, Promise가 처리될 때까지 기다렸다가 결과를 반환</li>
<li>프라미스가 처리되길 기다리는 동안엔 엔진이 다른 작업이 수행 가능하다.</li>
</ul>
<pre><code class="language-jsx">async function delay() {
  await new Promise(resolve =&gt; setTimeout(resolve, 1000));
  console.log(&#39;1초 후 실행&#39;);
}</code></pre>
<p><strong>async await를 이용한 성공 에러처리</strong></p>
<p>await를 통해 then을 사용하지 않고 결과 값을 얻을 수 있고, catch대시 try…catch구문을 이용해 에러처리가 가능하다.</p>
<pre><code class="language-jsx">async function f() {

  try {
    let response = await fetch(&#39;http://유효하지-않은-주소&#39;);
  } catch(err) {
    alert(err); // TypeError: failed to fetch
  }
}

f();</code></pre>
<p>에러가 발생하면 제어 흐름이 catch 블록으로 넘어간다. 또한, 여러 줄의 코드를 try로 감쌀 수 있다.</p>
<h3 id="await-time-으로-5초-대기"><strong>await time() 으로 5초 대기</strong></h3>
<pre><code class="language-tsx">console.log(&#39;a&#39;);
await time();
console.log(&#39;b&#39;);</code></pre>
<p>이렇게 하려면 time()이 <strong>Promise를 반환해야 await이 효과가 있음</strong>.</p>
<pre><code class="language-tsx">function time() {
  return new Promise(resolve =&gt; setTimeout(resolve, 5000));
}

async function run() {
  console.log(&#39;a&#39;);
  await time();   // 5초 기다림
  console.log(&#39;b&#39;);
}
run();

//출력
a
(5초 후) 
b</code></pre>
<blockquote>
<p>출처</p>
</blockquote>
<ul>
<li><a href="https://velog.io/@khy226/%EB%8F%99%EA%B8%B0-%EB%B9%84%EB%8F%99%EA%B8%B0%EB%9E%80-Promise-asyncawait-%EA%B0%9C%EB%85%90">https://velog.io/@khy226/동기-비동기란-Promise-asyncawait-개념</a></li>
<li><a href="https://joshua1988.github.io/web-development/javascript/promise-for-beginners/">https://joshua1988.github.io/web-development/javascript/promise-for-beginners/</a></li>
<li><a href="https://velog.io/@dbwjd5864/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%9D%98-%ED%94%84%EB%A1%9C%EB%AF%B8%EC%8A%A4-Promise%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C%EC%9A%94">https://velog.io/@dbwjd5864/자바스크립트의-프로미스-Promise는-무엇일까요</a></li>
<li><a href="https://poiemaweb.com/es6-promise">https://poiemaweb.com/es6-promise</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트에서 FCM을 이용한 알림 구현하기]]></title>
            <link>https://velog.io/@jaemin_ve/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%90%EC%84%9C-FCM%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%95%8C%EB%A6%BC-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jaemin_ve/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%90%EC%84%9C-FCM%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%95%8C%EB%A6%BC-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 21 Nov 2024 16:00:45 GMT</pubDate>
            <description><![CDATA[<h2 id="도입-배경">도입 배경</h2>
<p>모우다 서비스는 모임, 채팅, 게시판등 커뮤니티 성격이 강한 서비스를 제공하고 있다. 이런 상황에서 내부 QA 진행 중, 모임 수정, 취소, 채팅 상황에서 사용자가 즉각적으로 알아야 하는 정보를 파악하기 쉽지 않다는 문제가 있었다. 또한 모임 생성 정보를 서비스에 접속해야 알 수 있다는 것이 모임을 독려하는데 어려움이 있다고 판단하였다. </p>
<p>이러한 문제를 해결하기 위해 알림 기능을 추가하려고 한다. </p>
<h1 id="웹--알림의-기본-개요">웹  알림의 기본 개요</h1>
<h2 id="11-웹-알림이란">1.1 웹 알림이란</h2>
<p>웹 알림은 사용자가 브라우저를 통해 특정 웹 사이트나 웹 애플리케이션으로부터 받는 알림 메시지를 말한다. 이러한 알림은 사용자가 웹 페이지에 머물러 있지 않더라도 중요하거나 관심있는 정보를 실시간으로 전달하는 것을 가능하게 한다. 이러한 기능을 통해 서비스 제공자는 이용자에게 지속적으로 서비스 가치를 전달할 수 있으며, 실시간으로 정보를 전달함으로써 사용자 경험을 개선할 수 있다.</p>
<p>브라우저에서의 알림 기능은 특히 크로스 플랫폼 환경에서 중요한 역할을 한다. 즉, 모바일 앱에서만 가능했던 실시간 푸시 알림 기능을 웹에서도 제공함으로써 모바일과 데스크탑 모두에서 일관된 사용자 경험을 제공할 수 있다. 이러한 이유로 웹 알림은 사용자 참여도를 높이고 웹 애플리케이션의 가치를 증대하는 핵심 요소로 자리잡고 있다.</p>
<p><img src="https://velog.velcdn.com/images/jaemin_ve/post/5c354629-4d3b-4231-81e8-6c63bebc891c/image.png" alt=""></p>
<p>이러한 알림 기능은 다양한 웹 사이트에서 활용되고 있다. 예를 들어 커뮤니티 기능을 제공하는 서비스 ‘슬랙’과 ‘인스타그램’이 기능을 활용한다. 대화에 읽지 않는 활동이 있을 경우 이를 표시하여 사용자가 서비스를 지속적으로 접근하고 이용을 촉진하도록 만들 수 있다.</p>
<h2 id="12-웹-알림-기본-요구사항">1.2 웹 알림 기본 요구사항</h2>
<h3 id="푸시-알림을-구현하기-위해">푸시 알림을 구현하기 위해</h3>
<p>웹에서 앱과 동일한 push알림을 구현하기 위해 <a href="https://developer.mozilla.org/ko/docs/Web/API/Notifications_API">Notification API</a>, <a href="https://developer.mozilla.org/ko/docs/Web/API/Push_API">Push API</a>을 사용한다.</p>
<h3 id="notification-api"><a href="https://developer.mozilla.org/ko/docs/Web/API/Notifications_API">Notification API</a></h3>
<p>브라우저가 제공하는 시스템 알림을 표시할 수 있도록 제어할 수 있게하는 API이다. 이러한 알림은 최상단 브라우징 컨텍스트 뷰포트의 바깥에 위치하고 있어 사용자가 탭을 변경하거나 다른 앱으로 이동했을 때에도 표시할 수 있다. </p>
<p>알림은 기본적으로 두 단계를 거쳐 완성된다. </p>
<p>첫째, 사용자가 시스템 알림 표시에 대한 권한을 허용해야 한다. <a href="https://developer.mozilla.org/en-US/docs/Web/API/Notification/requestPermission_static">Notification.requestPermission()</a> 메서드를 호출하여 사용자가 서비스로부터의 알림을 허용하지, 차단할지, 현재 시점에 선택하지 않을 지 선택할 수 있다. 선택된 이후에는, 사용자가 브라우저의 설정을 변경하지 않는 한 앱이나 브라우저가 초기화되기 전까지 유지된다. </p>
<p>둘째, Notification 생성자를 사용해 알림을 생성한다. 필수값 title인자와 텍스트 방향, 바디 내용, 표시할 아이콘, 재생할 알림 사운드등 옵션을 지정하는 옵션 객체를 선택적으로 사용할 수 있다. </p>
<h3 id="push-api"><a href="https://developer.mozilla.org/ko/docs/Web/API/Push_API">Push API</a></h3>
<p>웹이 활성화 되어 있는지 여부와 상관없이 푸시 메시지를 수신할 수 있도록해주는 기능을 제공하는 API이다.  Push API를 사용하기 위해서는 후술할 S<strong>evice Worker</strong>가 활성화 되어 있어야 한다.</p>
<h3 id="13-서비스-워커를-이용한-푸시-알림">1.3 서비스 워커를 이용한 푸시 알림</h3>
<p><a href="https://developer.mozilla.org/ko/docs/Web/API/Service_Worker_API">Service Worker</a>는 페이지의 메인 javascript와 독립된 스레드에서 실행되며, 브라우저와 네트워크 사이에 존재하므로 브라우저 탭을 닫더라도 네트워크와 통신이 가능하다. 이를 통해 백그라운드에서 푸시 메시지를 수신할 수 있는 환경을 제공한다</p>
<p><img src="https://velog.velcdn.com/images/jaemin_ve/post/45f182a8-a0fa-4161-b5fa-d69010169b1a/image.png" alt=""></p>
<p>서비스 워커는 웹 애플리케이션과 관련 없이 독립적인 라이프사이클을 가집니다.</p>
<p><img src="https://velog.velcdn.com/images/jaemin_ve/post/51239b28-0e8a-479c-bcc7-f3b434cc1e38/image.png" alt=""></p>
<h3 id="설치-중-installing">설치 중 (Installing)</h3>
<p>서비스 워커를 등록하면, 자바스크립트가 다운로드 된 후, 파싱되고 나면, <strong>서비스 워커</strong>는 설치 중 상태에 들어가게 된다.</p>
<p>설치가 성공적으로 이루어지면, 설치됨 상태가 되고, 설치 중 오류가 발생하면 서비스 워커는 중복 상태가 된다. 이 경우 페이지를 새로 고침하여 서비스 워커를 다시 등록해야 한다</p>
<h3 id="설치됨대기중-installedwaiting">설치됨/대기중 (Installed/waiting)</h3>
<p>서비스 워커가 성공적으로 설치되면, 설치됨 상태로 넘어가게 되고, 현재 활성화 되어있는 다른 서비스 워커가 앱을 제어하고 있지 않으면, 바로 활성화 중 상태로 전환된다.</p>
<p>앱을 제어하고 있는 경우에는 대기 중 상태가 유지 된다.</p>
<h3 id="활성화-중-activating">활성화 중 (Activating)</h3>
<p>서비스 워커가 활성화되어 <strong>앱을 제어하기 전</strong>, activate 이벤트가 발생한다.</p>
<h3 id="활성화-됨-activated">활성화 됨 (Activated)</h3>
<p>서비스 워커가 활성화 되면 페이지를 제어하고, fetch 이벤트와 같은 동작 이벤트를 받을 준비가 된다.</p>
<p>서비스 워커는 페이지 로딩이 시작하기 전에만 페이지 제어 권한을 가져올 수 있다. 즉, 서비스 워커가 활성화 되기 전에 로딩이 시작된 페이지는 서비스 워커가 제어할 수 없다.</p>
<h3 id="중복-redunant">중복 (Redunant)</h3>
<p>서비스 워커가 등록중, 설치 중 실패하거나 새로운 버전으로 교체되면 중복 상태가 된다.</p>
<p>이 상태의 서비스 워커는 앱에 아무런 영향을 미치지 못한다.</p>
<p>서비스 워커의 이런 특성으로 인해 사용에 몇가지 주의사항이 존재한다.</p>
<ul>
<li>서비스 워커는 웹 애플리케이션과 다른 독립적인 라이프 사이클을 가지고 있기 때문에 페이지의 DOM에 접근할 수 없다.</li>
<li>보안 상의 이유로 HTTPS에서만 동작한다. 네트워크 요청을 수정할 수 있기 때문에 중간자 공격에 취약하기 때문이다. 단 localhost는 예외이다.</li>
</ul>
<p>이러한 서비스 워커의 특징 덕분에 웹 애플리케이션이 종료돼도 서비스 워커가 동작하여 알림 메시지를 수신할 수 있다.</p>
<h2 id="fcm을-이용한-웹-푸시-알림-구현">FCM을 이용한 웹 푸시 알림 구현</h2>
<h3 id="21-웹-알림의-흐름web-push-protocol">2.1 웹 알림의 흐름(Web Push Protocol)</h3>
<p>push 알림을 수신하는 브라우저, 발송하는 서버 사이에 다음같은 상호작용으로 동작한다.</p>
<p>클라이언트는 푸시 서비스로 구독 요청을 보내고 구독에 성공한 경우, 브라우저를 식별할 수 있는 정보를 포함한 구독 정보를 브라우저에게 제공한다. 이 구독 정보를 서버에 저장해 두었다가 푸시 메시지 보내야할 때, 구독 정보와 메시지를 푸시 서비스로 보내고 푸시 서비스 구독 정보를 바탕으로 클라이언트에서 푸시 메시지 제공한다.</p>
<p>푸시 메시지를 보낼 때 보안을 위해 VAPID(Voluntary Application Server Identification) 인증 방식을 사용하여 메시지를 안전하게 전송합니다. 서버에서 푸시 서비스에게 푸시 알림 요청을 보낼 때, 일련의 정보가 담긴 JWT를 VAPID비공개 키로 암호화한다. 푸시 서비스는 VAPID 공개키를 사용하여 서버의 푸시 알림 요청에 대한 유효성을 검증한다.</p>
<p><img src="https://velog.velcdn.com/images/jaemin_ve/post/f48eb155-053e-4862-8bcf-df2f982ffc8b/image.png" alt=""></p>
<h3 id="23-push-서비스로-fcm을-이용하자">2.3 push 서비스로 FCM을 이용하자</h3>
<p>Web Push Protocol을 쉽게 구현하기 위해 FCM을 이용할 수 있다. FCM을 이용하면 무료로 Push 서비스를 구현할 수 있고 공식 문서와 인테넷에 관련 자료를 얻기 쉽다고 판단하여 결정하게 되었다.</p>
<p>FCM을 사용하가 위해서는 몇가지 설정이 필요로 하다. </p>
<ol>
<li><strong>SDK 추가</strong></li>
</ol>
<p><a href="https://firebase.google.com/?hl=ko">firebase</a>로 이동하여 console로 이동 후, 프로젝트를 생성해야 한다. firebase에서 제공하는 가이드 라인에 따라 프로젝트를 생성 후, 프로젝트 설정을 확인하면 SDK를 설정하는 방법을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/jaemin_ve/post/30a1c483-7ac7-478b-9139-892933f9cc87/image.png" alt=""></p>
<p>위의 코드를 프로젝트의 src폴더에 넣어 설정할 수 있다.</p>
<ol>
<li><strong>Notification 권한을 받은 후, VAPID키 발급</strong></li>
</ol>
<p>사용자에게 알림 권한을 받고, 브라우저에 해당하는 고유의 토큰을 발급 받아야한다. 토큰을 받기 위해서는 먼저 VAPID키를 발급 받아야한다. 처음에 생성한 프로젝트에서 ‘클라우드 메시징’에서 웹 푸시 인증서에서  키쌍을 생성할 수 있다.</p>
<p>다음으로 알림 권한을 요청하는 함수를 작성한 코드다.</p>
<pre><code class="language-jsx"> import { getMessaging, getToken } from &#39;firebase/messaging&#39;;

import { app } from &#39;./initFirebase&#39;;
import checkCanUseFirebase from &#39;@_utils/checkCanUseFirebase&#39;;

const messaging = checkCanUseFirebase() ? getMessaging(app) : null;

export function requestPermission(mutationFn: (currentToken: string) =&gt; void) {
  console.log(&#39;권한 요청 중...&#39;);
  Notification.requestPermission().then((permission) =&gt; {
    if (permission === &#39;granted&#39;) {
      console.log(&#39;알림 권한이 허용됨&#39;);
      //@ts-expect-error 파이어베이스가 사용되면 messaging이 존재
      getToken(messaging, {
        vapidKey: process.env.VAPID_KEY,
      })
        .then((currentToken) =&gt; {
          if (currentToken) {
            console.log(currentToken);
            mutationFn(currentToken);
          } else {
            // Show permission request UI
            console.log(
              &#39;No registration token available. Request permission to generate one.&#39;,
            );
            // ...
          }
        })
        .catch((err) =&gt; {
          console.log(&#39;An error occurred while retrieving token. &#39;, err);
          // ...
        });
      // FCM 메세지 처리
    } else {
      console.log(&#39;알림 권한 허용 안됨&#39;);
    }
  });
}</code></pre>
<p>다음 코드는 알림을 요청하고 토큰을 발급받는 코드예시이다.</p>
<p>Notification.requestPermission()함수를 사용해 사용자가 알림을 허용한 경우,앞에서 발급받은 VAPID와 초기화된 Firebase 앱 인스턴스로 생성된 messaging 객체를  getToken함수를 인자로 넘겨주어 고유한 토큰을 발급받을 수 있다. 이 발급받은 토큰을 서버에 넘겨주어 저장한다.</p>
<ol>
<li><strong>메시지 수신 설정</strong></li>
</ol>
<p>알림 메시지에는 두가지 형식이 존재한다. 웹 어플리케이션이 동작하는 상황에서 수신할 수 있는 foreground 메시지와 service worker가 백그라운드에서 동작할 때 수신할 수 있는 background 메시지가 있다.</p>
<p>먼저 forground 메시지를 설정하는 코드를 설명한다.</p>
<pre><code class="language-jsx">const messaging = getMessaging(app);

  onMessage(messaging, (payload) =&gt; {
    console.log(&#39;포그라운드 알림 도착: &#39;, payload);

    const notificationTitle = payload.notification?.title || &#39;알림&#39;;
    const notificationOptions = {
      body: payload.notification?.body || &#39;&#39;,
      icon: payload.notification?.icon,
      data: { link: payload.fcmOptions?.link || &#39;/&#39; },
    };

    if (Notification.permission === &#39;granted&#39;) {
      try {
        const notification = new Notification(
          notificationTitle,
          notificationOptions,
        );

        notification.onclick = function (event) {
          event.preventDefault();
          window.open(notificationOptions.data.link, &#39;_blank&#39;);
        };
      } catch (error) {
        console.error(&#39;알림 생성 중 오류 발생:&#39;, error);
      }
    } else {
      console.warn(&#39;알림 권한이 허용되지 않았습니다.&#39;);
    }
  });</code></pre>
<p>messaging 객체를 생성하고 onMessage 함수를 이용하여 FCM에서 웹 페이지가 열려 있는 동안 push 메시지를 수신할 때 호출된다. 클라이언트는 FCM SDK를 통해 메시지를 수신하고, 메시지가 도착하면 미리 등록된 onMessage() 핸들러가 자동으로 호출된다. </p>
<p>알림이 허용되어 있으면, payload객체를 통해 new Notification으로 알림 객체를 생성한다. 추가적으로 알림을 클릭 시, 해당 페이지로 이동하는 이벤트를 추가했다.</p>
<p>다음으로 background 메시지를 설정하는 방식이다.</p>
<p>background에서 메시지를 사용하기 위해서는 서비스 워커 설정이 필요로 하다. 이때 서비스 워커는 public 폴더에 위치해야 한다. 서비스 워커는 해당 사이트 루트 경로에 있어야 정상적으로 동작할 수 있기 때문이다. </p>
<p>그렇기 때문에 정적인 파일 위치하는 public폴더에 위치시켜야 한다. 웹 애플리케이션은 서비스워커를 firebase SDK를 서비스 워커에 별도로 설치를 해야한다. 이를 위해 브라우저 클라이언트에서 사용한 것과 동일한 firebase 설정 객체를 이용해서 firebase 앱을 초기화 해야한다.
다음은 sevice worker에서 firebase 앱을 설정하는 예시이다.</p>
<pre><code class="language-jsx">// firebaseConfig.js
const firebaseConfig = {
  apiKey: &quot;YOUR_API_KEY&quot;,
  authDomain: &quot;YOUR_PROJECT_ID.firebaseapp.com&quot;,
  projectId: &quot;YOUR_PROJECT_ID&quot;,
  storageBucket: &quot;YOUR_PROJECT_ID.appspot.com&quot;,
  messagingSenderId: &quot;YOUR_MESSAGING_SENDER_ID&quot;,
  appId: &quot;YOUR_APP_ID&quot;,
  measurementId: &quot;YOUR_MEASUREMENT_ID&quot;
};

self.firebaseConfig = firebaseConfig;</code></pre>
<pre><code class="language-jsx">//firebase-messaging-sw.js
importScripts(
  &quot;https://www.gstatic.com/firebasejs/10.8.0/firebase-app-compat.js&quot;
);
importScripts(
  &quot;https://www.gstatic.com/firebasejs/10.8.0/firebase-messaging-compat.js&quot;
);

importScripts(&#39;/firebaseConfig.js&#39;);

self.addEventListener(&quot;install&quot;, function () {
  self.skipWaiting();
});

self.addEventListener(&quot;activate&quot;, function () {
  console.log(&quot;fcm service worker가 실행되었습니다.&quot;);
});

firebase.initializeApp(firebaseConfig);</code></pre>
<p>public폴더에서는 모듈이 동작하지 않기 때문에 importScripts를 사용한다.</p>
<p>여기서 firebase-messaging-sw.js에 다음과 같은 코드를 추가하여 커스텀된 알림을 사용할 수 있다.</p>
<pre><code class="language-jsx">messaging.onBackgroundMessage((payload) =&gt; {
    const notificationTitle = payload.title;
    const notificationOptions = {
        body: payload.body
        // icon: payload.icon
    };
    self.registration.showNotification(notificationTitle, notificationOptions);
});</code></pre>
<p>하지만 background 메시지는 기본적으로 firebase SDK에서 알림 메시지를 처리하고 있다. 그래서 다음과 같이 커스텀 알림 메시지를 사용할 경우 알림 메시지가 두번 오는 결과가 발생한다. 그래서 커스텀 알림을 사용할 필요가 없는 경우, 굳이 위의 코드는 필요하지 않다.</p>
<p>또한 알림을 클릭 했을 때, 이벤트도 설정할 수 있다.
예시로 알림을 클릭 했을 시, 해당 페이지로 이동하는 로직을 추가했다.</p>
<pre><code class="language-jsx">self.addEventListener(&#39;notificationclick&#39;, function(event) {
  console.log(&#39;[firebase-messaging-sw.js] 알림이 클릭되었습니다.&#39;);

  // 알림 데이터를 가져오기
  const link = event.notification.data.FCM_MSG.notification.click_action;

  event.notification.close(); // 알림 닫기

  // 사용자가 알림을 클릭했을 때 해당 링크로 이동
  if (link) {
    event.waitUntil(
      clients.matchAll({ type: &#39;window&#39;, includeUncontrolled: true }).then(windowClients =&gt; {
        // 이미 열린 창이 있는지 확인
        for (let i = 0; i &lt; windowClients.length; i++) {
          const client = windowClients[i];
          if (client.url === link &amp;&amp; &#39;focus&#39; in client) {
            return client.focus();
          }
        }
        // 새 창을 열거나 이미 있는 창으로 이동
        if (clients.openWindow) {
          return clients.openWindow(link);
        }
      })
    );
  }
});</code></pre>
<p>background 알림 메시지를 받기 위한 설정을 완료하였다.</p>
<p>마지막으로 작성한 서비스 워커 코드를 브라우저에 등록할 필요가 있다.</p>
<pre><code class="language-jsx">if (&#39;serviceWorker&#39; in navigator) {
  navigator.serviceWorker
    .register(`/firebase-messaging-sw.js`)
    .then((registration) =&gt; {
      console.log(&#39;Service Worker registered with scope:&#39;, registration.scope);
      initializeForegroundMessageHandling();
    })
    .catch((error) =&gt; {
      console.log(&#39;Service Worker registration failed:&#39;, error);
    });
} else {
  // 서비스 워커가 지원되지 않는 경우에도 포그라운드 메시지 처리를 초기화
  initializeForegroundMessageHandling();
}</code></pre>
<p>처음에 if (&#39;serviceWorker&#39; in navigator)는 브라우저가 <strong>Service Worker</strong> 기능을 지원하는지 화인하고 /firebase-messaging-sw.js라는 파일을 Service Worker로 등록합니다. </p>
<ol>
<li><strong>지원하지 않는 브라우저 예외처리하기</strong></li>
</ol>
<p>모바일 환경에서 safri나 카카오톡 공유를 통한 링크 통해 서비스에 접속하여 모임 목록 페이지에 진입한 경우, 흰색화면이 출력되는 경우가 있다. 이때 개발자 도구를 확인하면 다음과 같은 에러가 발생한 것을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/jaemin_ve/post/215d9b2a-4759-4067-9d5c-1e3bb86afcbb/image.png" alt=""></p>
<p>이는 해당 환경의 브라우저에서는 Notification을 지원하지 않아 에러가 발생하여 javascript 코드가 중단되어 서비스를 이용하지 못하는 문제이다. 특정 브라우저나 환경에서 Notification을 지원하지 않기 때문에 알림 기능을 지원하지 않을 수 있다. 하지만 이 때문에 어떤한 설명도 없이 전체 서비스를 이용하지 못하는 것은 사용자 관점에서 큰 문제다. </p>
<p>Notification을 지원하는 브라우저인지 확인해서 지원하지 않는 브라우저라면 해당 코드를 호출하지 않는 방식으로 문제를 해결할 수 있다.</p>
<pre><code class="language-jsx">import { isSupported } from &#39;firebase/messaging&#39;;
export default async function checkCanUseFirebase() {
  if (location.hostname === &#39;localhost&#39;) return true;
  if (location.protocol !== &#39;https:&#39;) return false;
  const messagingSupported = await isSupported();
  if (!messagingSupported) {
    console.error(&quot;This browser doesn&#39;t support Firebase Messaging.&quot;);
    return false;
  }
  return true;
}</code></pre>
<p>해당 함수는 알림 서비스를 이용할 수 있는 브라우저인지 확인하는 함수로 Firebase Messaging 지원 여부를 확인하는 isSupported를 사용하여 검사할 수 있다. </p>
<p>해당 함수를 initializeFirebaseApp함수 내부에 호출하여 false인 경우는 initializeFirebaseApp를 바로 return하는 방식으로 구현하면  모바일에서 safri나 카카오 브라우저에서 알림을 제외한 서비스를 정상적으로 이용할 수 있다.</p>
<pre><code class="language-jsx">export const initializeFirebaseApp = async () =&gt; {
  const canUseFirebase = await checkCanUseFirebase();
  if (canUseFirebase) {
    return initializeApp(firebaseConfig);
  } else {
    console.warn(&#39;Firebase는 이 환경에서 지원되지 않습니다.&#39;);
    return undefined;
  }
};</code></pre>
<h3 id="fcm을-이용하면서-발생한-유의사항">FCM을 이용하면서 발생한 유의사항</h3>
<ul>
<li>public 폴더에서 .env를 사용할 수 없다. 그렇기 때문에 해당 SDK 키가 github에 노출될 위험이 존재한다. firebaseConfig.js로 별도로 분리하고 배포 과정에서 동적으로 파일을 생성하고 빌드하도록 구성하고 있다.</li>
<li>Safari나 Firefox같은 경우 푸시 알림 악용을 방지하기 위해 알림 허용 요청을 발생시키기 위해서는 사용자 제스처가 촉발되어야 알림 권한 허용창을 제시할 수 있습니다.</li>
<li>크롬과 Firefox에서는 사이트가 보안 콘텍스트(즉, HTTPS)가 아니면 알림을 아예 요청할 수 없으며 크로스 오리진 <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe">iframe</a>으로부터의 알림 권한은 요청할 수 없다.</li>
<li>23년 3월, 애플이 이번 iOS 16.4 버전부터 웹을 통한 푸시 알림을 허용하면서 아이폰 사용자들도 사파리나 크롬 등을 통해 웹푸시를 수신할 수 있게 되었다. 하지만 애플에서 웹 푸시의 기능을 제한하기 위해 ios와 ipad os에서 웹 알림을 사용하기 위해서는 PWA로 웹앱을 설치해야 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[우아한테크코스 레벨 4 생활기 - 최종 데모데이까지]]></title>
            <link>https://velog.io/@jaemin_ve/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%EB%A0%88%EB%B2%A8-4-%EC%83%9D%ED%99%9C%EA%B8%B0-%EC%B5%9C%EC%A2%85-%EB%8D%B0%EB%AA%A8%EB%8D%B0%EC%9D%B4%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@jaemin_ve/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%EB%A0%88%EB%B2%A8-4-%EC%83%9D%ED%99%9C%EA%B8%B0-%EC%B5%9C%EC%A2%85-%EB%8D%B0%EB%AA%A8%EB%8D%B0%EC%9D%B4%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Tue, 29 Oct 2024 08:42:12 GMT</pubDate>
            <description><![CDATA[<p>2024년 우테코에서 대장정이 레벨 4를 끝으로 거의 마무리가 되었다. 공식적은 교육은 레벨 4가 끝이다. </p>
<p>레벨 4에서는 프로젝트와 동시에 레벨 1, 2처럼 미션도 진행했기 때문에 레벨 3에서보다 바쁘게 보냈던 것 같다.</p>
<p>지금까지 데모데이 별로 회고를 작성했는데 할게 많다보니 생각보다 쉽지 않았던 것 같다. 그래도 원서까지 지원했던 크루에 비해서는 여유가 있었지만...</p>
<h2 id="5차-데모데이까지">5차 데모데이까지</h2>
<p> 첫 한 주동안에는 프론트앤드 성능 개선을 위한 개인 미션을 진행했다. 또한 레벨 4에서 모우다 팀을 앞으로 어떤 방향으로 나아갈지를 주로 이야기 했다. 레벨 3와는 다르게 레벨 4에서는 미션도 있고 본격적으로 기업 원서 접수를 하는 크루도 있었다. 레벨 4에서는 프로젝트 참여도가 이전과는 같지 않을 것이라고 생각했다. 또한 레벨 3에서 받은 피드백을 바탕으로 어떤 방향으로 서비스를 발전시킬지 서로가 합의를 하는 시간이 필요했다. 서로 레벨 4에서는 어떤 계획을 가지고 있는지, 모우다 서비스를 어떻게 생각하는지 공유했다. 우리 팀은 전체적으로 아직은 프로젝트에 집중하고 싶다는 의견이 많이 나왔다. 그래서 우리 팀의 우선 순위를 프로젝트, 미션, 원서 순으로 두고 진행하고자 했다. </p>
<p>프로젝트 진행에 앞서 레벨 3 런칭데이에서 받았던 피드백을 살펴보았다. </p>
<blockquote>
<h3 id="개선이-필요한-것">개선이 필요한 것</h3>
<ul>
<li>처음 들어갔을 때 안내 페이지가 있으면 좋겠음.<ul>
<li>상돌 의견: <a href="https://haengdong.pro/">행동대장 팀 페이지</a>를 들어가봤는데, 이정도 안내 페이지는 만들면 좋을 것 같음.</li>
</ul>
</li>
<li>치코 소개팅 빨리 시켜주세요.</li>
<li>방장 이외에도 채팅방의 + 버튼에서 할 일이 있었으면 좋겠다.<ul>
<li>상돌 의견: 투표 기능 등.. 있으면 좋긴 하겠다.</li>
</ul>
</li>
<li>답글 기능 UX 개선<ul>
<li>상돌 의견: 브리 / 크론이 시연했을 때 댓글 답글에 대해서 언급한 적이 있음. 답글이면 바로 아래에 작성할 수 있게 해주면 좋겠다.</li>
</ul>
</li>
<li>약속 정했을 때 달력이 보였으면 좋겠어요.<ul>
<li>테바 의견: 치코 해줘!</li>
</ul>
</li>
<li>프로필 사진 / 프로필 기능 확대</li>
<li>알림이 없을 때 문구 표시해주세요.<ul>
<li>상돌 의견: 알림센터에서 아무것도 없을 때 “알림이 없어요” 와 같은 문구가 있으면 될 듯!</li>
</ul>
</li>
<li>채팅기능에서 사진이 있으면 좋겠어요. - 호기 여친 -</li>
<li>채팅방에서 입력창이 너무 밑에 있어요.</li>
<li>해주세요 기능에서 글자수를 모르겠어요.</li>
<li>카테고리, 검색 기능이 필요해요</li>
<li>중요: 모임을 만들 때 이전 날짜를 지정하면 그 창은 넘어가지고 마지막에 생성 버튼을 눌렀을 때 안됨<ul>
<li><strong>날짜 지정 페이지에서 이전 날짜이면 넘어가지 못하게 해야함!</strong></li>
</ul>
</li>
<li>모임의 최소 인원이 정해지면 좋겠어요</li>
<li>참여를 안했는데 참여 취소가 왜 나오나욤?</li>
<li>인원만 선택하는 등 빠르게 만들기 기능이 있으면 좋겠어요.</li>
<li>유저 닉네임을 변경하고 싶어요.</li>
<li>방장의 강퇴 기능이 있으면 좋겠어요</li>
<li>방장이 아니어도 다락방에 누가 있는지 볼 수 있으면 좋겠어요.</li>
<li>모임장만 초대코드를 공유하는 것이 별로. 쫓아내는 기능도 필요해요</li>
<li><strong>중요: 모임을 생성하고 뒤로가기 버튼을 누르면 만들기 페이지로 이동함</strong></li>
</ul>
</blockquote>
<p>런칭 데이에 참여한 크루들 덕분에 다양한 피드백을 받을 수 있었다. 하지만 여기에 있는 모든 피드백을 수용할 수도, 할 필요도 없다고 생각했다. 당장 필요한 개선 사항부터 적용하기 위해 각 피드백을 3개의 우선순위를 두어 분류했다. </p>
<p>모우다에서 가장 개선이 시급하다고 생각하는 우선사항들은 다음과 같다.</p>
<blockquote>
<p>안내 페이지 (온보딩,랜딩) → 디자인 / 어떻게?</p>
<p>글자 수 제한 등 유효성 검사 </p>
<p>모임 생성 후 뒤로가기 시 만들기 페이지로 이동하는 문제</p>
<p>초대 코드로 들어간후 뒤로가기 누르면 에러</p>
<p>카톡 인앱 브라우저에서 사용 못함</p>
<p>새로고침</p>
</blockquote>
<p>당장의 사용성에 저해가 되는 부분들을 중점적으로 선정하고 담당자를 정했다. 나는 글자 수 제한 등 유효성 검사와 새로고침을 구현했다. </p>
<p>그렇게 한 주가 마무리 되고 나는 예비군으로 떠났다. 아쉽게도 휴학생인 나는 가차없이 동원예비군을 진행했다.</p>
<p>훈련을 갔다온 후, 바로 추석연휴가 시작되었다. 레벨 4 시작한지 얼마 되지도 않았는데 훈련도 있고 쉬는 날도 있었고 레벨 4 초반에는 한 것도 별로 없는데 벌써 달의 절반이 지나갔다.   </p>
<p>추석 연휴가 끝이나고 마음의 정리가 제대로 되지 않음을 느꼈다. 레벨 4가 시작되고 계속 프로젝트에 집중하지 못했다.</p>
<p>쉬는 날이 많아서 집중하지 못하는 것이라고 생각했다. </p>
<p>스스로 목표를 정하지 못해서 생긴 문제였다. 다른 크루들은 이제 원서도 쓰고 취업 준비하는 것을 보고 불안해하며 마음을 잡지 못했던 것 같다. 말은 프로젝트에 집중한다고 했지만 실제로 준비하는 크루들을 보니 흔들렸던 것 같다. </p>
<p> 그래서 정확히 레벨4에서는 프로젝트에 집중하고 남은 시간에 미션을 진행하자고 정했다. 이력서의 경우는 레벨 5부터 작성하고 마음 먹었다. 생각을 정리하니 나의 것에 집중할 수 있었다. </p>
<p>팀적으로 추석이 끝나고 모우다 서비스를 어떻게 진행해야 할지 추가적으로 회의를 거쳤다. 커뮤니티 사이트는 지속적인 사용자가 있어야 서비스가 유지되었다. 존재한다고 의미있는 서비스가 아니였기 때문에 많은 고민을 했다. 이대로 우리 서비스 도메인을 유지할지, 아니면 다시 회의를 통해 새로운 도메인을 찾을지 회의를 했다. </p>
<p>먼저 현재 모우다 서비스의 문제점을 찾는 것부터 시작했다. 현재 모우다 서비스는 초기에 기획했던 &#39;슬랙에 비해 모임을 만들기 편한 환경&#39;을 만족했는지 평가했다. 슬랙이 업무용 메신저이므로 보통은 모임글을 올리기 어렵다고 생각했고 이에 분리된 환경만 갖추면 이용자가 있을 것이라고 생각했다. 하지만 기존에 익숙했던 환경을 버리고 새로운 서비스를 이용하는 것이 사용자에게는 부담이었고 이를 감안하면서까지 우리 서비스를 사용할 이유가 부족했다는 판단이 나왔다. 기존의 도메인을 유지한채 어떻게 하면 유입자를 만들 수 있을지 고민하는 방향으로 의견이 모여졌다. </p>
<p>일단 5차 데모데이가 앞으로 다가왔기 때문에 개인 미션에서 수행했던 성능 개선을 프로젝트에 적용하는 시간을 가졌다. </p>
<p>데모데이에서는 코치분들에게 지금까지 피드백으로 받았던 변경사항을 설명했다.</p>
<p>FE에서 칭찬 받았던 부분인 모바일 퍼스트팀이기 때문에 시연을 모바일로 진행했던 점, 요구사항이었던 운영체제와 브라우저 지원 범위를 구체적으로 확인한 흔적과 그에 대한 대응의 흔적을 잘 확인할 수 있다고 하셨다. 또한 성능 개선도 기존 대비 몇 %가 개선되었는지 가시적으로 확인할 수 있었다고 해서 다행히였다. </p>
<p>그렇게 데모데이를 마치고 앞으로 남은 한달간 달릴 준비를 했다. </p>
<h2 id="최종-데모데이까지">최종 데모데이까지</h2>
<p>우리가 목표로 삼을 몇가지 포인트를 정했다.</p>
<p>첫 번째, 사용자 유입을 도울 미끼 기능을 넣기. 이를 위해 룰렛 기능을 넣기로 했다. 크루들끼지 커피쏘기같은 내기를 할 때가 있다. 이때 가위바위보로 진행하는데 이를 룰렛 형태의 서비스로 제공하면 미니 모임의 형태로 제공하고 직접적으로 사용자에게 자극을 줄 수 있을 것로 예상되었다.</p>
<p>두 번째, 모임을 만들기 이상이 없게하기. 현재 모우다에서 모임을 만들 수 있지만 실제 모임을 매끄럽게 만들기에는 아직 부족한 부분이 있었다. 이를 보안하기 위해 채팅 기능을 강화하고자 했다. 현재 채팅은 채팅을 할 수 있다 수준이었다. 그래서 채팅을   유용하게 이용하기위해 현재 날짜가 표시되게 만들고 채팅방에서 참여자와 모임 상태를 확인할 수 있도록 개선하고자했다.</p>
<p>세 번째, 실사용자인 것을 어필하기. 닉네임에 생성에 제한이 없는 현재로는 아직 장난식으로 닉네임을 지어 사용자 식별이 어렵다는 문제가 있었다. 모임에 참여하고 생성함에 있어 누가 있는지도 중요한 요소이기 때문에 적어도 실명이 공개되어 있다는 제약을 걸고 싶었다. 또한 개인 식별을 위한 프로필 설정 기능 필요했다. 이를 위해 기존 카카오 로그인은 실명을 받을 수 없는 문제가 있었다. 이를 개선하기 위해 애플, 구글로그인으로 변경하기로 결정했다.</p>
<p>내가 맡은 역할은 마이페이지를 고도화한 프로필 기능과 oauth로그인 기능이었다. </p>
<p>기존 카카오 로그인은 oauth 2.0으로 구현하였는데, 구글과 애플 로그인 또한 이와는 다르지 않는 것 같아서 어렵지 않았다. </p>
<p>그랬어야 했다...</p>
<p>구글 로그인의 경우 oauth 2.0으로 구현할 수 있었고 그렇게 구현을 했는데 프론트에는 문제없이 code값을 받아 서버에 보냈고 서버에서 구글간의 통신을 통해 access_token을 발급 받을 수 있어야했지만 구글은 계속 invalid_Grant 에러를 주었다. 에러 메세지라도 명확했더라면 쉽게 찾을 지도 모르겠지만 2일을 허비했음에도 끝내 왜 서버에서 구글에게 code를 검증할 때 에러가 발생하는지 찾지 못했다. </p>
<p>다른 방법이 필요했다. 구글에서는 oauth 2.0을 직접 구현하는 방식이 아닌 구글 id token값을 이용하는 방식으로 수정했다. 이 방법은 간단하게 구글 로그인을 구현할 수 있지만 구글 로그인을 내가 렌더링하는 것이 아닌 구글에서 지정한 형태로 렌더링되는 문제가 있었다. 이 때문에 구글 로그인 버튼 로그인 크기가 정해져 있어 다른 로그인 UI를 해당 형태와 맞춰야 했다.</p>
<p>다음 애플 로그인이 문제였다. 애플 로그인인 카카오 로그인과 같인 oauth 2.0으로 구현이 가능했다. 실제로 정상적으로 로그인이 되었다. 하지만 사용자 정보를 가져오는데 문제가 발생했다. 애플에서 사용자 정보를 가져올 경우, 로그인에 대한 응답을 클라이언트가 아닌 서버로 응답을 보내는 문제가 있었다. 이를 해결하는 것은 결과적으로 어렵지 않았지만 처음에는 당황하여 바로 해결하지 못했다. 또한 이 사용자 정보를 최초 한번만 전송했다. 이 최초 한 번이 무슨 의미인지 처음에는 잘 몰랐다. 구현하면서 사용자 정보를 가져오지 못하는 문제가 발생해서 확인해보니 우리 서비스에 접속한 최초 한번의 한해서 정보를 받아올 수 있었다. 그러니 계속 테스트를 위해 애플 로그인 해왔던 우리 계정에서는 사용자 정보를 가져올 수 없었다. 이는 나중에 회원탈퇴를 구현할 때에도 어려움이 되었다. </p>
<p>외부 서비스를 이용하는 것이 구현하는데 더 쉬울 것으로 예상되었으나, 서비스에 따라 정책때문에 구현이 어려움이 있는 경우도 있었다. 특히 프로젝트를 진행하면서 애플에서는 지원하지 않는 css, 함수들 때문에 어려움이 있었는데 끝까지 나를 당황시키는 기업이 아닐 수 없다.</p>
<p>프로필 기능을 구현함에 있어서도 애플은 자기만의 길을 고수했다. 프로필 사진을 업로드할 수 있는 기능을 만들었지만 특정 jpeg에서는 정상동작하지 않는 문제가 있었다. ios로 촬영한 사진에서 이런 문제가 발생했는데 찾아보니 jpeg여도 내부적으로  애플 고유의 인코딩 방식인 heic를 사용하고 있었다. 이 때문에 브라우저 호환성에 문제가 있었다. 이것을 클라이언트에서 일반적은 jpeg로 추가 변환하는 과정을 필요로 했다. </p>
<p>이런 자잘한 문제 때문에 개발을 진행하는데 어려움도 있었지만 문제가 발생했을 때, 문제를 파악하고 해결하는 능력은 길러진 것 같았다. </p>
<p>기능을 구현하고나니 어느새 한달이 지난 최종 데모데이를 앞에 두고 있었다. </p>
<h2 id="최종데모데이와-그-이후">최종데모데이와 그 이후</h2>
<p>최종데모데이에서는 각 캠퍼스의 모든 팀이 모두 모여 부스를 운영하여 서비스를 소개하는 날이다. 우리 모우다 팀은 오전에 부스 운영을 하게 되었다. 오전 출원중에 신규 이용자가 로그인이 되지 않는 충격적인 사실을 듣고 발을 헏디뎌 넘어지고 말았다.. </p>
<p>빨리 가서 확인해보니 서버에서 최초 사용자에 대한 분기 처리가 부족하여 문제가 발생했다. 우리들이 QA를 진행했을 때 모두 기존 사용자였기 때문에 문제가 없었는데 막상 새로운 계정으로 접속하고자 하니 문제가 발생했다. QA는 아무리 진행해도 매번 새로운 에러가 발생하는 것 같다. 왜 기업이 알파 서비스, 베타 서비스를 진행하는 새삼 깨닫게 되었다. </p>
<p>다행히 데모데이에서는 큰 문제없이 서비스를 소개할 수 있었다. </p>
<p>데모데이가 종료된 후, 푹 쉬었다. 지금까지 계속 개발을 진행하다보니 조금 지쳤던 것 같다. 일단 주말동안에는 푹 쉬었고 레벨 4의 버퍼 기간이 되었다. </p>
<p>레벨 4 버퍼에서는 앞으로 모우다 프로젝트의 진행에 대한 결정을 진행할 예정이다. </p>
<p>나의 첫 프로젝트가 어느정도 막을 내렸다. 이후에 서비스 개발을 계속 진행할 의향은 있지만 크루들의 의견도 들어보아야하니 계속 진행하게 될지는 모르겠다.</p>
<p>첫 프로젝트였던 만큼 적응하기 힘들었던 부분도 있었지만, 그래도 이런 크루, 팀원들과 같이 프로젝트를 진행할 수 있었던 것이 영광이었다. 우테코에서 정해준 가이드라인과 크루들 덕분에 어떻게 하나의 서비스가 출시되는지 경헝할 수 있었다. </p>
<p>유저시나리오에서부터 웹 접근성 개선까지 4달간의 여정이 끝이나고 배운 것도 많았지만 배워야할 것도 많이 있다는 것을 느낄 수 있었다. 마지막에는 그간 배웠던 내용을 다시 정리하는 시간을 가지며 내 것으로 만들 계획이다. </p>
<p>아직 부족하지만 새롭게 배우는 것을 즐기는 개발자가 되도록 노력하자! </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[우테코 레벨 3 유연성 강화 글쓰기]]></title>
            <link>https://velog.io/@jaemin_ve/%EC%9A%B0%ED%85%8C%EC%BD%94-%EB%A0%88%EB%B2%A8-3-%EC%9C%A0%EC%97%B0%EC%84%B1-%EA%B0%95%ED%99%94-%EA%B8%80%EC%93%B0%EA%B8%B0</link>
            <guid>https://velog.io/@jaemin_ve/%EC%9A%B0%ED%85%8C%EC%BD%94-%EB%A0%88%EB%B2%A8-3-%EC%9C%A0%EC%97%B0%EC%84%B1-%EA%B0%95%ED%99%94-%EA%B8%80%EC%93%B0%EA%B8%B0</guid>
            <pubDate>Sun, 01 Sep 2024 12:18:24 GMT</pubDate>
            <description><![CDATA[<h2 id="솔플형-인간">솔플형 인간</h2>
<p>협업은 언제부터인가 나에게 있어 거추장스러운 옷이다. 내가 경험했던 협업은 같이 목표를 향해 나아가는 것이 아니라 누가 더 많은 짐을 지는 지에 대한 싸움이었다. 내 스스로 돌아볼 때 리더의 역할과는 어울리지 않는다고 생각하지만 결국 아쉬운 사람이 총대를 맡아야 했다.</p>
<p>그렇게 맨 총대는 보통 삐그덕거렸다. 협업 인원들을 독려하고 설득하여 일을 헤쳐 나가는 것은 이상일 뿐 현실은 연락이 제대로 되는 것만 해도 다행이었다. 이런 경험이 누적되어 내 마음속에서 협업은 업무를 진행하는 방해 요소가 되어 있었다.</p>
<p>&#39;또 대충해 오면 내가 다시 해야겠지&#39;</p>
<p>&#39;또 잠수타지 않겠지?&#39;</p>
<p>차라리 처음부터 혼자 하는 것이 마음 편하다고 느꼈고, 나는 솔플형 인간이 되어 있었다.</p>
<h2 id="프로젝트-협업의-꽃">프로젝트, 협업의 꽃?</h2>
<p>우아한테크코스 레벨 3에서는 프로젝트를 진행한다. 이전까지의 프로젝트 경험이 없었던 나는 새로운 것을 시작하는 설렘보다 두려움이 앞섰다. 내가 경험이 없었기 때문에 다른 팀원들에게 도움이 되지 않을까 봐 걱정도 되었다.</p>
<p>하지만 그건 지금 생각해 보면 표면적인 이유였다. 이런 내가 지금까지와 같은 경험을 하게 될까 두려웠다. 사람 사는 곳 같다고 하지만 아무리 그래도 다 열정 있는 사람이라고 뽑은 곳인데 같지는 않을 것이라고 애써 생각하며 레벨 3의 시작을 맞이했다.</p>
<p>레벨 3가 시작하고 프론트엔드에게 주어진 첫 요구사항은 프로젝트 세팅이었다. 지금까지는 미션에서는 이미 세팅이 된 상태에서 미션을 진행했다면 이번에는 기초 공사부터 진행해야 했다. 당연히 미션 이외에는 CRA를 사용했기 때문에 웹팩을 기반으로 한 세팅 경험이 없었다. 회의를 통해 프론트 팀원끼리 주말에 공부하고 같이 적용해 보자는 결론이 나왔다.</p>
<p>주말 동안 세팅을 위한 공부를 하면서 불안했다. 같이 하는 것이 아니라 온전히 나의 과업이라는 생각을 지울 수 없었고 심리적으로 압박을 느끼며 공부했다. 자세하게 공부하는 것은 좋다.</p>
<p>하지만 이런 방식으로 계속 프로젝트를 진행할 수 는 없다. 팀원들과의 소통 갈등 그리고 결국 혼자서 거대한 프로젝트를 감당할 수 없었다는 것을 이성으로 알고 있었다.</p>
<h2 id="협업에-필요로-하는-것--대화-">협업에 필요로 하는 것 -대화-</h2>
<p>그럼 어떻게 하면 이런 문제에서 벗어날 수 있을까?</p>
<p>결국 팀원 간의 대화가 답이라는 결론이 나왔다. 지금까지 협업에서는 업무적인 측면을 제외하고 팀원들 간의 제대로 된 대화를 나누기 힘들었다. 하지만 우테코에서는 평일에 항상 같이 있고 아침에는 데일리 미팅, 매주 월요일에는 유연성 강화를 위한 소통의 시간이 할애되어 있었다. 이러한 시간을 적극적으로 활용하고자 <strong>내 생각, 감정 공유하기</strong>을 목표로 세웠다.</p>
<p>데일리 미팅을 통해 내 감정을 공유하는 일을 계속하자 했다. 또한 오늘 무엇이 목표인지 공유하면서 내가 현재 맡은 역할에 집중하고자 했다.</p>
<p>처음에는 팀원들과의 어색한 관계 때문에 솔직한 감정을 공유하는 것이 쉽지 않았다. 그래서 팀 분위기를 먼저 만드는 것이 중요했다. 이 부분은 같은 크루의 도움을 많이 받았다. 팀원들이 소프트 스킬에 관심이 많았고, 무겁고 진지한 분위보다 편한 분위기를 만드는 크루들이었다.
그런 분위기 속에서 크루들과의 대화를 통해 단순히 프로젝트에만 집중하는 것이 아니라 내가 속한 팀에 대해 알아갈 수 있었다.</p>
<p>지금까지 협업에 필요로 하는 것은 일에 집중하는 분위기라고 생각했다. 그래야 서로가 일을 할 수 있다고 생각했다.</p>
<p>그건 단순히 업무적인 측면만을 고려한 편협한 생각이었다. 우리가 하고자 하는 것은 협업이다.</p>
<p>협업이 제대로 이루어지기 위해서는 서로 요구를 파악하고 이것을 쉽게 이야기할 수 있는 분위가 중요하다.</p>
<p>각자 크루들이 자기 일을 하고 싶게 만드는 분위기</p>
<p>그런 분위기를 만들기 위해서는 어떻게 해야 할까?</p>
<p>나부터 솔직하게 내 감정을 공유하고 내 생각을 이야기하는 것을 목표로 세웠다. 회의를 진행하다 나의 생각이 있으면 드러내려고 노력했고, 의견이 나왔다면 상대방 의견에 경청하는 자세로 있었다.</p>
<p>이러한 자세들이 팀을 결속시키고 협업을 가능하게 하는 것 같다.</p>
<p>우리 팀의 문화로 데일리 바잉이 있다. 이러한 데일리 바잉은 퇴원 30분 전 팀이 모여 오늘 무엇을 했는지 공유하는 시간이었다. 이 시간 덕분에 오로지 내가 해야 할 일에 집중할 수 있었다. 다른 팀원 간에 서로 잘해 나가고 있다는 신뢰를 얻을 수 있는 시간이고, 어려운 부분이 있다면 공유하여 해결책을 생각해 볼 수 있는 시간이다.</p>
<p>대화는 단순히 친목을 넘어서 팀을 결속시키고 팀원 간의 목표에 한 발짝 다가갈 수 있는 힘이였다.</p>
<p>우테코에서 그간 소프트 스킬의 중요성을 이야기 했지만 내게 와닿는 것은 없었다. 이번 프로젝트를 진행하면서 왜 대화가 중요한지 그리고 원활한 대화를 위해 소프트 스킬을 신경 써야 하는 이유를 찾을 수 있었다.</p>
<p>혼자 힘이었다면 아마 계속 모르고 있었을 것이다. 계속 소프트 스킬을 강조하고 이러한 시간을 만들어준 크루들에게 감사를 느낀다.</p>
<h2 id="대화를-넘어">대화를 넘어</h2>
<p>어느덧 레벨 3도 거의 끝을 달리고 있다. 이제는 미션을 진행하면서 팀을 믿고 나아갈 수 있다. 더 이상 조급해하지 않고</p>
<p>프로젝트를 즐길 수 있다. 그래서 다음 목표는 어떻게 하면 효과적으로 협업을 진행할 수 있을지 생각하고 있다. 현재 겪었던 어려움은 다른 사람이 구현한 코드를 PR을 통해 부분적으로 알아도 전체적인 맥락을 파악하기 쉽지 않았었다. 이를 어떻게 하면 해결하여 개발 과정에서 발행하는 리소스를 줄이고 이전에 작업했던 사람과 어떻게 싱크를 맞출 수 있는지 고민했다. 답은 코치분들도 지금까지 강조해 왔던 &#39;문서화&#39;이다.</p>
<p>내가 구현한 기능이 무엇이고, 어떻게 동작하는지, 왜 이렇게 구현했는지 문서화를 해놓는다면 협업에 도움이 될 것으로 생각한다.</p>
<p>직접 경험해보아야 알 수 있는 것이 있다. 이번 레벨 3의 프로젝트를 통해 협업을 위해 무엇이 필요한지 실제 피부로 경험했다. 답은 이미 분명 들어왔던 것이었고 알고 있는 것들이었다. 그것을 경험을 통해 확신을 얻을 수 있었다. 레벨 4에서도 팀원들과 대화하며 협업 과정을 즐기며 나아갈 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[우테코 레벨 2 유연성 강화 글쓰기]]></title>
            <link>https://velog.io/@jaemin_ve/%EC%9A%B0%ED%85%8C%EC%BD%94-%EB%A0%88%EB%B2%A8-2-%EC%9C%A0%EC%97%B0%EC%84%B1-%EA%B0%95%ED%99%94-%EA%B8%80%EC%93%B0%EA%B8%B0</link>
            <guid>https://velog.io/@jaemin_ve/%EC%9A%B0%ED%85%8C%EC%BD%94-%EB%A0%88%EB%B2%A8-2-%EC%9C%A0%EC%97%B0%EC%84%B1-%EA%B0%95%ED%99%94-%EA%B8%80%EC%93%B0%EA%B8%B0</guid>
            <pubDate>Sun, 01 Sep 2024 12:15:54 GMT</pubDate>
            <description><![CDATA[<h1 id="달리기전에-생각해보셨나요">달리기전에 생각해보셨나요?</h1>
<h3 id="chapter2의-시작">chapter2의 시작</h3>
<hr>
<p>우테코에서의 새로운 시작. 레벨2</p>
<p>레벨1에서의 적응 기간이 끝나고 우테코에 완전히 익숙해졌다. 이제 우테코에 나오는 것도 익숙해지고 크루들과도 친해져 즐겁게 우테코 생활을 보냈다. 레벨1 유.강.스의 목표였던 &#39;나를 믿고 도전하자&#39;는 의미 있는 성과를 거둔 것 같아 새로운 목표를 생각하고 있었다. 레벨 2에서는 무엇을 이루고 싶은가 하고 고민하였다. 나에 대한 기록을 남기는 것에 대한 중요성을 깨달아 미션마다 회고 작성하기를 선택할까하는 생각이 들었다.</p>
<p>다만 이 목표가 정말 나의 유연성을 강화하기 위한 목표인지 의문이 들었다. 유연성 강화를 위한 목표라기보다는 과정일 뿐이었다. 그래서 목표를 추상적으로 생각하여 나 자신을 되돌아보기로 수정하였다. 이번 레벨 동안에는 자신을 되돌아보고, 이를 이루기 위해 회고 작성을하기로 마음먹었다.</p>
<p>그렇게 레벨 2의 시작을 알리는 미션 1 페이먼츠가 시작을 알렸다.</p>
<h3 id="어-이제-출발인데요">어? 이제 출발인데요?</h3>
<hr>
<p>레벨2에 올라와서 걱정이 많았다. 일단 가장 큰 문제는 내가 리액트를 다루어 본 경험이 다른 크루에 비해 상대적으로 적었다는 것이다. 방학 동안 급하게 공식 문서를 읽어보는 시간을 가졌지만 역시 실제 프로젝트에서 익숙하게 사용하지는 못했다. step 1는 페어의 도움으로 미션을 진행할 수 있었지만 step 2는 온전히 나의 능력으로 구현해야 했다. 그렇게 미션에 치여 1주간을 보냈지만, 다음 미션 시작일 넘어서까지 리뷰어 &#39;지그&#39;와 어떻게 하면 코드를 발전시킬 수 있는지 의견을 주고받으며 코드를 개선해 나갔다.</p>
<p>리뷰어의 열정 덕분에 끝까지 미션을 완료했고 스스로도 충실감을 느낄 수 있었다. 그리고 지쳤다. 제대로 쉬지도 못한 상태에서 미션 1과 미션 2를 동시에 진행하는 과정에서 제대로 체력 관리를 하지 못했다. 계속 이런 식으로 미션을 진행할 수 없다는 판단이 들었다.</p>
<p>현재 나에게 필요한 것이 바뀌는 순간이었다. 현재 나에게 필요한 것은 이 트랙을 완주할 페이스 관리였다고, 유연성 강화 목표를 &#39;초조하지 말자, 나의 페이스에 맞추자&#39;로 변경하였다.</p>
<h3 id="내-속도는-xxxkm야-">내 속도는 xxxkm야 ~</h3>
<hr>
<p>이러한 목표를 설정하고 처음으로 유.강.스 시간을 가졌다. 자신의 페이스가 무엇인지 확인해 보는 것이 중요하다는 피드백을 받았다. 그래서 미션에 바로 적용하고자 했다. 미션을 진행할 때 하루는 할 수 있는 것, 하고 싶은 것을 전부 할 수 있도록 계속 시간을 쏟았고 다른 날은 미션 요구사항에 집중하며 비교를 해보았다.</p>
<p>전자는 시간상으로 여유가 없었고 압박감을 느끼며 미션을 구현했다. 단순히 하루의 완성도를 보았을 때는 전자가 높았지만, 그다음 날까지 영향이 갈 정도로 정신적으로 힘들었다. 후자의 경우 들이는 시간 자체에는 큰 차이가 없었지만, 미션 자체에 집중할 수 있었고 더 즐겁게 임할 수 있었다.</p>
<p>결론을 정했다. step 1에서는 미션 요구사항에 집중하고 step 2에서는 미션 요구사항에 집중하되 내가 추가로 적용하고 싶은 1 ~ 2가지를 선택해서 구현하자는 계획을 세웠다.</p>
<h3 id="브레이크">브레이크</h3>
<hr>
<p>지금까지 페어 미션을 진행했을 때 제대로 휴식 시간도 없이 몰아치며 미션을 수행했었다. 미션 3에서는 나와 스타일이 다른 페어를 만나게 되었다. 페어의 의견으로 미션 중간에 잠깐씩 휴식 시간을 가지게 되었다. 그때에는 솔직히 걱정되었다.</p>
<p>&#39;구현해야 할 게 많은데...&#39;</p>
<p>그러다가 막히는 부분이 생겼다. 미리 생각해 두었던 퇴원 시간이 가까워졌지만, 미련이 남아 계속 붙잡고 있었다.</p>
<p>그런 나에게 페어가 이럴 때는 한숨 돌리고 보면 잘 해결될 거라고 조언해 주었다.</p>
<p>그렇게 페어의 의견을 받아들이고 집에 가는 동안 머리를 비울 수 있었다. 집에서 차분히 어디가 잘못되었는지 생각해 보았다.</p>
<p>답이 보였다. 조급하게 생각했는지 시도했던 부분에서 놓쳤던 코드가 있어서 발생한 문제였다. 또한 다음날은 공휴일로 비대면으로 페어 미션을 진행했었다. 그렇게 잠깐의 휴식 시간을 가지며 미션을 진행했고 처음에 목표했던 구현 사항을 달성하는 데 성공했다. 만약 나 혼자 구현하는 것이었다면, 추가로 내일할 부분까지 남은 시간만큼 구현 시도를 했을 것이다.</p>
<p>페어는 다시 나에게 말했다. 오늘 할 것은 다 했으니 내일 다시 진행하자고.</p>
<p>페어의 의견을 받아들이고 어느 정도 휴식 시간을 가지고 다음 날 미션을 완료했다. 미션을 되돌아보는 시간을 가졌다. 지금까지 이렇게 마음에 여유를 가지고 미션을 진행한 적이 없었다. 항상 미션을 진행이 끝나면 회복하는 데 며칠이 걸렸는데 이번에는 하루 쉬고 나니 회복되었다.</p>
<p>시간에 쫒겨 여유 없이 달리다 보니 정신적 여유가 없었다. 이번 미션에 무엇이 중요한지, 무엇을 얻어갈지 생각하지 않고 미션을 진행했다. 미션 진행하는데 힘들다 보니 이런 것에 생각할 여유도 없었다. 이번 페어와 미션을 진행하면서 내가 미션을 어떻게 진행해야 하는가 생각해 보게 되었다.</p>
<h3 id="단거리-아니-장거리">단거리? 아니 장거리</h3>
<hr>
<p>여유 있게 한다는 것은 놀면서 한다는 게 아니다. 내가 무엇을 하는 건지, 왜 하는 건지 의미를 찾는 과정이다. 이번 미션이 끝이 아니다. 앞으로도 배워야 할 것이 많다. 미션을 해결하며 나도 모르게 다른 크루와 비교하며 어느샌가 스스로에 대한 기준이 높아진 것 같다. 남과의 비교가 나에게 독으로 작용한 것 같다. 지금 내가 할 수 있는 것을 생각하자. 오늘 하루 하는 것이 개발자로서 나의 끝이 아니다.</p>
<p>지금 나는 마라톤을 뛰고 있다.</p>
<p>내가 발전할 수 있는 부분을 생각하며,</p>
<p>내가 가지고 갈 수 있는 것을 생각하며,</p>
<p>더 멀리 바라보자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[우테코 레벨 1 유연성 강화 글쓰기]]></title>
            <link>https://velog.io/@jaemin_ve/%EC%9A%B0%ED%85%8C%EC%BD%94-%EB%A0%88%EB%B2%A8-1-%EC%9C%A0%EC%97%B0%EC%84%B1-%EA%B0%95%ED%99%94-%EA%B8%80%EC%93%B0%EA%B8%B0</link>
            <guid>https://velog.io/@jaemin_ve/%EC%9A%B0%ED%85%8C%EC%BD%94-%EB%A0%88%EB%B2%A8-1-%EC%9C%A0%EC%97%B0%EC%84%B1-%EA%B0%95%ED%99%94-%EA%B8%80%EC%93%B0%EA%B8%B0</guid>
            <pubDate>Sun, 01 Sep 2024 12:14:51 GMT</pubDate>
            <description><![CDATA[<h3 id="한-발짝을-위한-깨달음">한 발짝을 위한 깨달음</h3>
<hr>
<p> 대학교에서 3년간 컴퓨터공학을 공부하면서 학교 수업 이외의 활동에는 참여하지 않았다. 그때는 당장 필요 없는 활동으로 보였고 내가 참여하기에는 전부 힘들어 보이는 활동이었다. 그렇게 &#39;난 아직 부족하다. 더 실력을 쌓고 지원하자&#39; 또는 &#39;내가 잘할 수 있을까?&#39;라는 의문만 가진 채 결정 바로 앞에서 서성이다 돌아갔다.
벽을 넘기 위해 고개를 위로 올려다보지도 않고 그저 내 눈높이에 벽 뒤가 보이지 않는 것 같으니, 회피하고 도망친 것이다.</p>
<p>우테코를 지원하게 된 이유도 어찌 보면 나의 의지로 지원한 게 아니었다. 활동하는 동아리에서 나와 같은 학년들이 대부분은 지원하다 보니 그 분위기에 편승하여 지원해 보자는 생각을 가지게 되었다. 그렇게 자소서 앞에 앉았지만, 그 공백들은 쉽게 채워지지 않았다. 
그렇게 글을 쓰며 깨달았다. 
&#39;쓸 게 아무것도 없네?&#39;
어렴풋이 느끼고 있던 것보다 자신에 대해 어필할 무엇인가가 부족했다. 타인에게 평가받는 두려움, 자신에 대한 낮은 자신감이 만든 결과였다. 
자신이 부족함을 느낌에도 그것을 채우려고 실행하지 않고 계속 머릿속에만 되뇌었다.</p>
<p>물론 지금도 이게 단지 살아가는 데 문제는 없다고 생각한다.
하지만 자신이 더 높은 곳을 바라고 있다면, 문제가 되었다.
욕심이 들었다.
더 많은 것을 경험하고 배워보고 싶었다.
그리고 목표를 세웠다.</p>
<p><strong>&#39;지금 할 수 있는 것은 하자&#39;</strong></p>
<p><strong>&#39;그리고 배우자&#39;</strong></p>
<p>그렇게 꾸역꾸역 채워 넣은 자소서를 제출하고 프리코스를 열심히 제출했다.
그리고 학교 수업에 교수님의 질문에 대답하기 시작했다. 물론 가산점을 준다는 명목도 있었지만, 적어도 내가 알고 있는 답은 그냥 넘기는 것이 아니라 수업 시간에 대답하게 되었다. 
덕분에 운 좋게 이를 좋게 봐주는 사람이 있어 졸업작품을 같이 하자는 제안도 생겼다. 결국 우테코에 합격하여 함께하지 못했다.
다만 이런 기회들이 나의 첫걸음이라 생각한다.  </p>
<h3 id="우테코에서의-개발자">우테코에서의 개발자</h3>
<hr>
<p>우테코가 단순히 개발 공부만을 위하는 곳이라고 생각했다. 
하지만 들어 와보니 개발 공부는 물론이고, 소프트 스킬의 발전도 중요하게 생각하는 것 같다. 그리고 그것을 유도하기 위해 여러 활동도 준비되어 있었다. 
처음에는 그냥 레크리에이션 같은 것인 줄 알았다. 이 당시에 &#39;이런 걸 왜 하는 거지&#39;라고도 생각했다. </p>
<p>하지만 우테코는 성장하는 개발자를 키우는 곳이다. 
그리고 개발자에게 필요한 역량은 단순히 코드를 잘 작성하는 것에만 있는 것이 아니다.
수업을 들으며 어떤 마음가짐으로 이번 우테코 6기를 보내야 할지 갈피를 잡을 수 있었다.
우테코 전까지 난 성과 증명 마인드 셋을 가지고 있었다. 실패를 마주하였을 때 스트레스를 많이 받아 결국 회피했다. 
그것을 자소서를 작성하면서 느꼈고 우테코에 들어와 정확히 무엇이 원인이었는지 알았다.
성장하기 위해서는 바뀌어야 했다. 
 미션을 진행하면서 느낀 &#39;이게 맞는 건가?&#39;보다 리뷰어, 크루, 페어 의견을 교류하며 &#39;이것을 통해 무엇을 배울 수 있는가?&#39;에 초점을 맞추고자 마음먹었다.</p>
<p>하지만 갑자기 마음만 먹는다고 사람은 바뀌지 않는다.</p>
<h3 id="테코톡을-찍어-볼까">테코톡을 찍어 볼까?</h3>
<hr>
<p>소프트 스킬의 개발을 위해 매주 월요일에는 유연성 강화 스터디를 진행한다. 
&#39;상상 속의 최악의 상황은 오지 않는다. 나를 믿으며 도전하자&#39;
유연성을 강화하기 위해 내가 세운 목표다.
이전까지의 마음가짐은 고치고자 마음먹었지만 실천한 경험이 적었던 참이었다. 
매주 스터디를 하며 목표에 가깝게 행동했는지 회고를 하면 큰 도움이 되리라 생각했다. 
그쯤에 레벨 1에서 테코톡을 진행할 크루를 모집하고 있었다. 처음에는 또 머뭇거렸다.</p>
<p>&#39;발표를 잘 못하는데...&#39;</p>
<p>&#39;나중에 생활에 익숙해지고 해도 늦지 않을까?&#39;</p>
<p>&#39;이 주제로 해도 내가 잘 설명할 수 있을까?&#39;</p>
<p>그렇게 어떻게든 피할 곳을 찾으려 머릿속으로 생각했지만 피할 수만은 없었다. 
그래서 더 깊이 생각하기 전에, 내가 세운 목표에 달성할 수 있기 위해, 신청 버튼을 눌렀다. 
지금 잘하지 못하더라도 이번 기회에 준비하면서 발전하자고 생각하며 스스로 자신감을 불어 넣었다.
정신없이 미션을 진행하다 보니 어느새 내가 테코톡을 진행해야 하는 시간이 되었다. 처음에는 바쁜 일정에 순간 &#39;괜히 했나&#39;라고 생각했다. </p>
<p>하지만 앞으로 이보다 바쁜 일정은 많다. 이런 상황에서도 내가 할 수 있는 역량을 다하면 된다. 원래 발표를 잘하지 못했는데 지금 당장 잘할 수는 없다. 전보다 지금 나아질 수 있는 것에 집중하자고 마음먹고 준비를 했다. 
또한 유연성 강화 스터디의 피드백을 읽어보며 나의 선택에 대한 지지를 통해 확신을 가질 수 있었다.</p>
<p>덕분에 걱정으로 시간을 허비하는 것이 아니라 테코톡 준비 자체에 집중할 수 있었다.
이번엔 피하지 않고 정면에서 바라봤다. 그 덕에 발전할 부분을 찾을 수 있을 것 같다. </p>
<h3 id="다음을-위한-준비">다음을 위한 준비</h3>
<hr>
<p>벌써 레벨 1이 끝나간다. 아직 부족한 게 이렇게 많은데 끝나간다니 아쉽기도 하다. 하지만 기대가 되기도 한다.
 레벨 2에서는 얼마나 새로운 것을 배우게 될지...</p>
<p>레벨 1에서는 처음 생각한 유연성 강화 목표치 그 이상을 달성할 수 있었다. 매주 스터디를 통해 회고하며 보낸 시간이 헛되지 않았다. 지금의 목표에서 끝나는 것이 아니라 다른 목표도 추가하고 싶은 욕심이 생겼다. 물론 과유불급(過猶不及)이라는 말도 있다. 그래서 급하게 정하는 것이 아니라 레벨 1이 끝나고 정리하며 새로운 목표에 대해 차분히 생각해 볼 생각이다. </p>
<p>조급해지지 말자.</p>
<p>할 수 있는 것을 하자.</p>
<p><strong>지금까지 세운 목표가 흔들리지 않도록, 앞으로 세울 목표가 내가 바라는 방향을 가리키도록...</strong></p>
]]></description>
        </item>
    </channel>
</rss>