<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>soleil_lucy_75.log</title>
        <link>https://velog.io/</link>
        <description>여행과 책을 좋아하는 개발자입니다.</description>
        <lastBuildDate>Sun, 28 Jun 2026 14:32:18 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>soleil_lucy_75.log</title>
            <url>https://velog.velcdn.com/images/soleil_lucy_75/profile/959f229d-b645-4e8b-bace-3963d85a872d/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. soleil_lucy_75.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/soleil_lucy_75" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[『하네스 엔지니어링 with 클로드 코드』 하네스 엔지니어링 이해하기]]></title>
            <link>https://velog.io/@soleil_lucy_75/harness-engineering-with-claude-code-review</link>
            <guid>https://velog.io/@soleil_lucy_75/harness-engineering-with-claude-code-review</guid>
            <pubDate>Sun, 28 Jun 2026 14:32:18 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>&quot;한빛미디어 서평단 &lt;나는리뷰어다&gt; 활동을 위해서 책을 협찬받아 작성된 서평입니다.&quot;</p>
</blockquote>
<h1 id="책을-읽게-된-계기">책을 읽게 된 계기</h1>
<p>하네스 엔지니어링은 2026년 상반기에 나온 용어로 알고 있습니다. 컨텍스트 엔지니어링을 넘어, 하네스 엔지니어링을 통해 AI가 더 잘 돌아가게끔 만드는 방법이 화제가 되면서 너도나도 적용해보던 것 같습니다. 그런데 저는 정작 이 단어를 들어보기만 했지, 제대로 공부해본 적이 없었습니다.</p>
<p>언젠가부터 마음에 걸렸습니다. 에이전트를 만들고 워크플로우를 설계해서 AI를 쓴다는데, 대체 그걸 어떻게 한다는 건지 궁금했습니다. 무엇보다 제가 진행하는 개인 프로젝트에 적용해서 AI를 제대로 한번 써보고 싶었습니다. 그래서 &quot;하네스 엔지니어링이라는 게 어떤 건지부터 공부해보자&quot;는 마음으로 이 책을 집어 들었습니다.</p>
<h1 id="인사이트">인사이트</h1>
<h2 id="아-이게-하네스-엔지니어링이구나">&quot;아, 이게 하네스 엔지니어링이구나&quot;</h2>
<p>책을 따라가며 2인 팀으로 — 정말 최소한의 팀으로 — 하네스를 직접 만들어봤습니다. 만들어 돌려보는 순간 &quot;아, 이게 하네스 엔지니어링이구나&quot; 싶었습니다.</p>
<p>하네스는 스킬(Skill), 에이전트(Agent), CLAUDE.md 같은 요소로 작업 환경 자체를 미리 설정해두는 기술입니다. 이걸 적용해두니 프롬프트를 장황하게 길게 쓸 필요가 없었습니다. 그동안 저는 현재 저의 컨텍스트를 길게 작성해서 프롬프트로 알려주곤 했는데, 그게 필요가 없었습니다. 작업 환경을 미리 파일로 구조화해두니 실행 시점엔 한 문장으로 방아쇠만 당기면 되는 것이었습니다. 사용자 프롬프트는 한 문장이면 됐습니다. 이래서 사람들이 하네스를 만들고 프로젝트를 실행하는구나 싶었습니다.</p>
<h2 id="하네스의-세-기둥">하네스의 세 기둥</h2>
<p>책은 하네스를 세 가지 요소로 나눕니다. 누가(Agent), 어떻게(Skill), 언제 누구와(Orchestrator). 이 셋은 각각 독립적으로 설계되고, 실행 시점에만 맞물립니다.</p>
<ul>
<li><code>에이전트</code>는 &quot;누가 이 작업을 맡는가&quot;에 답합니다. 단순한 설정이 아니라 일종의 역할 계약서입니다. 그 에이전트가 무엇을 담당하고, 어떤 기준으로 판단하며, 누구와 어떻게 소통하는지를 명시한 살아 있는 문서입니다.</li>
<li><code>스킬</code>은 &quot;이 작업을 어떤 절차로 하는가&quot;에 답합니다. 반복되는 작업을 재사용 가능한 절차로 정리해둔 파일입니다. description 한 줄이 호출 여부를 결정합니다. 그래서 &quot;무엇을 하는지&quot;보다 &quot;언제 이 스킬을 써야 하는지&quot;를 명확히 적어주는 게 핵심입니다.</li>
<li><code>오케스트레이터</code>는 &quot;이 작업을 언제, 누구와 하는가&quot;에 답합니다. 핵심은 지시자가 아니라 지휘자라는 점입니다. 팀원 각자가 자신의 악기를 연주하도록 맡기는 것이 결국 최고의 연주를 만듭니다.</li>
</ul>
<p>객체지향에서 역할을 나누듯, AI에게도 역할을 분리해주는 것 — 그게 하네스의 출발점이라는 걸 배웠습니다.</p>
<h2 id="그래서-저도-하네스를-만들어보고-싶어졌습니다">그래서, 저도 하네스를 만들어보고 싶어졌습니다</h2>
<p>개념을 배우고 나니 직접 하네스를 만들어보고 싶다는 생각이 들었습니다. 거창한 것 말고, 제가 매주 반복적으로 하는 일 중에서 하나를 골라보기로 했습니다.</p>
<p>① 개발 블로그 스터디 discussion 만들기</p>
<ul>
<li>discussion 작성 에이전트</li>
<li>discussion 검토 에이전트</li>
<li>개발 블로그 스터디 discussion 스킬</li>
</ul>
<p>작성하는 에이전트와 검토하는 에이전트를 나누는 구조입니다. 책에서 강조하는 생성-검증 패턴에 자연스럽게 들어맞아서, 가장 먼저 만들어보기 좋겠다고 생각했습니다.</p>
<h1 id="마무리">마무리</h1>
<p>책을 읽으면서, 단어만 들어봤던 &#39;하네스&#39;라는 개념과 조금은 가까워졌습니다. 책에서 배운 여러 패턴과 하네스를 구현하는 방법들을 제대로 이해하려면 결국 직접 해보는 수밖에 없을 것 같습니다. 그래서 제가 매주 반복하는 일과 개인 프로젝트에 하나씩 적용해보려 합니다. 실제로 하네스를 만들어 본 기록은 따로 글로 남겨보겠습니다.</p>
<h1 id="읽으면서-찍은-사진">읽으면서 찍은 사진</h1>
<p>&lt;my-first-harness 예제 공부하기&gt;
<img src="https://velog.velcdn.com/images/soleil_lucy_75/post/526be8d4-d615-4a90-8fe4-91835118b169/image.png" alt="my-first-harness 예제 공부하기"></p>
<p>&lt;commit-message skill&gt;
<img src="https://velog.velcdn.com/images/soleil_lucy_75/post/8f247ac4-6e15-487a-923f-318838f77fc3/image.png" alt="commit-message skill"></p>
<p>&lt;하네스 실전 부분 읽는 모습&gt;
<img src="https://velog.velcdn.com/images/soleil_lucy_75/post/30651192-f40e-4919-9228-1a7dc37347ce/image.png" alt="하네스 실전 부분 읽는 모습"></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[텔레그램 봇 만들기 #2: 일정을 받아볼 방법을 고민하다 봇을 만들기까지]]></title>
            <link>https://velog.io/@soleil_lucy_75/daily-briefing-bot-development-2</link>
            <guid>https://velog.io/@soleil_lucy_75/daily-briefing-bot-development-2</guid>
            <pubDate>Sun, 28 Jun 2026 09:00:22 GMT</pubDate>
            <description><![CDATA[<p>지난 글에서는 &quot;왜 이 봇을 만들기로 했는가&quot;를 다뤘습니다. 할 일을 자꾸 미루는 패턴을 발견하고, &#39;일정이 눈앞에 보이게 하자&#39;는 생각에 도달한 끝에 다음과 같은 MVP 범위를 정했습니다.</p>
<p><strong>MVP 핵심 기능:</strong></p>
<ul>
<li><code>weekly_plan.md</code>를 읽어서 오늘 분량만 추출</li>
<li>AI가 오늘 맞춤 브리핑 작성</li>
<li>텔레그램으로 푸시</li>
<li>매일 자동 실행</li>
</ul>
<p>이번 글은 이 MVP를 <strong>실제로 어떻게 만들었는지</strong>에 대한 기록입니다. 특히 &quot;매일 아침 일정을 어떻게 받아볼까&quot;를 고민하다 텔레그램 봇에 도달한 과정과, 그 봇을 Claude와 함께 어떻게 제작했는지에 초점을 맞췄습니다.</p>
<h1 id="1-매일-아침-어떻게-받아볼까">1. 매일 아침 어떻게 받아볼까</h1>
<p>오전 알바 때문에 일찍 일어나는 편이라, 일어날 때쯤 그날 일정을 받아보고 싶었습니다. 오전 5시에 &quot;오늘 어떤 일을 하면 된다&quot;는 메시지를 받으면, 하루를 어떻게 보낼지 상상하면서 할 일을 덜 미루게 될 것이라고 생각했습니다.</p>
<p>그렇다면 어떤 메신저로 받을까?</p>
<p>카카오톡과 라인이 먼저 떠올랐습니다. 하지만 카카오톡엔 개인 메시지가, 라인엔 알바 업무 연락이 쌓여 있어서, 둘 다 브리핑을 받기엔 다른 대화에 묻힐 게 뻔했습니다. 일정과 관련된 메시지는 분리하는게 좋겠다 생각했습니다.</p>
<p>그러다 SNS에서 텔레그램을 개인 비서처럼 쓰는 사례를 보고, 텔레그램을 써봐야겠다고 생각했습니다.</p>
<p>게다가 텔레그램은 봇을 직접 만들어 쓸 수 있었습니다. 그렇게 <strong>&quot;텔레그램 봇을 만들어보자&quot;</strong>로 마음을 정했습니다.</p>
<h1 id="2-봇을-어떻게-설계했나">2. 봇을 어떻게 설계했나</h1>
<p>봇이 하는 일은 세 단계입니다.</p>
<ol>
<li>내 일주일 계획을 읽고</li>
<li>그중 오늘 것만 골라 메시지로 정리하고</li>
<li>텔레그램으로 보낸다</li>
</ol>
<h2 id="1단계--일주일-계획은-weekly_planmd-파일-하나에-둔다"><strong>1단계 — 일주일 계획은 <code>weekly_plan.md</code> 파일 하나에 둔다</strong></h2>
<p>봇이 일정 브리핑 메시지를 보내주기 위해서 &quot;오늘 뭘 해야 하는지&quot;를 알아야 하는데, 그 정보를 어디에 둘지가 고민했습니다. DB에 저장하는 선택지도 있었지만, 제 계획은 일주일에 한 번 일요일에만 작성하고, 일주일이 지난 계획은 따로 기억해둘 필요도 없었습니다. 그래서 DB를 쓰지 않고 <strong>마크다운 파일 하나만 두고 매주 새로 작성하는 방식</strong>이 더 낫다고 봤습니다.</p>
<p>파일 위쪽에는 이번 주 방향성과 목표가 있고, 그 아래에 요일별 상세 계획이 섹션으로 이어집니다. 봇이 실제로 뽑아 쓰는 건 이 요일별 섹션이고, 한 칸은 대략 아래와 같이 생겼습니다.</p>
<pre><code class="language-markdown">### 📅 5/12 (화) — 첫 지원의 날 / 지원용 자산 최소 개선

| 시간        | 일정                           |
| ----------- | ------------------------------ |
| 06:00~12:30 | 아르바이트                     |
| 14:30~17:00 | 메인 작업: 지원 1~2건 + README |

**오늘의 핵심:** 지원 + 지원용 자산 최소 개선.

- **최소 목표:** GitHub README 1개 수정 + 지원 1건
- **기본 목표:** 지원 2건
- **여유 목표:** 채용 공고 추가 탐색</code></pre>
<p>시간표뿐 아니라 &quot;오늘의 핵심&quot;, 그리고 최소, 기본, 여유 단계별 목표까지 한 덩어리로 들어 있습니다. 봇은 이런 요일 섹션 일곱 개 중 오늘 것만 골라내야 합니다.</p>
<h2 id="2단계--오늘-일정을-비서처럼-브리핑하는-일은-ai에게-맡긴다"><strong>2단계 — 오늘 일정을 비서처럼 브리핑하는 일은 AI에게 맡긴다</strong></h2>
<p>파일 하나에 일주일 치가 다 들어 있으니, 봇은 그중 오늘 분량만 뽑아 말해줘야 합니다. 계획표 원문은 표와 불릿이 빼곡해서 아침에 한눈에 들어오지 않습니다. 그래서 AI에게 &quot;오늘 날짜에 해당하는 부분을 찾아, 개인 비서가 일정을 브리핑하듯 정리해줘&quot;라고 맡겼습니다. 코드에서는 오늘 날짜와 요일을 한국 시간 기준으로 계산해, 계획표 전문과 함께 AI에 넘깁니다.</p>
<p>그 &quot;비서처럼&quot;을 말이 아니라 프롬프트에 작성해 두었습니다. 시스템 프롬프트는 CO-STAR 프레임워크로 짰고, 비서의 역할(계획표 내용만 옮기고 없는 일정은 만들지 않기)부터 출력 형식, 톤까지 모두 여기에 담았습니다.</p>
<h3 id="시스템-프롬프트">시스템 프롬프트</h3>
<pre><code class="language-text"># Context (배경)
사용자는 취업 준비 중이며, 매일 아침 KST 05:00에 이 브리핑을 텔레그램으로 받습니다.
사용자는 알바, 스터디, 러닝, 관계 유지를 병행하고 있고,
&quot;자기검열&quot; 경향(스스로를 가혹하게 평가하는 사고 패턴)이 있어
&quot;행동 기준 평가&quot;가 매우 중요합니다.
사용자에게는 이미 잘 짜여진 주간 계획표(weekly_plan.md)가 있으며,
브리핑은 그 계획표에서 오늘 분량만 뽑아 정리하는 역할입니다.

# Objective (목표)
주간 계획표에서 오늘 날짜 섹션을 정확히 찾아,
사용자가 &quot;오늘 무엇을 / 언제 / 얼마나 하면 되는지&quot;를
한눈에 파악하도록 정리한 브리핑 메시지를 작성합니다.
새로운 일정을 만들어내지 않고, 계획표에 적힌 내용을 충실히 옮깁니다.

# Style (스타일)
- 출력은 텔레그램 HTML 형식만 사용. 허용 태그: &lt;b&gt;, &lt;i&gt;, &lt;code&gt;
- 마크다운(#, ##, **, *, -)은 절대 사용하지 마세요. 텔레그램에서 깨집니다.
- 목록은 &quot;• &quot; (불릿 + 공백)으로 시작
- 섹션 헤더는 &quot;이모지 + &lt;b&gt;제목&lt;/b&gt;&quot; 형식 (예: 🎯 &lt;b&gt;목표&lt;/b&gt;)
- 응답에 코드 블록(```)이나 백틱을 쓰지 마세요
- 분량: 600~1000자 (한국어 글자 수 기준)

# Tone (톤)
따뜻하지만 단호함. 동기부여보다 명확함이 우선.
&quot;화이팅&quot;, &quot;할 수 있어!&quot; 같은 공허한 응원 금지.
사용자의 자기검열을 자극하지 않도록, &quot;행동 기준&quot;으로 말해주세요.
(예: &quot;지원 1건 제출 = 오늘 성공&quot; 처럼 결과가 아닌 행동으로)

# Audience (청중)
청중은 본인 1명. 취업 준비 중이며,
탈락 누적으로 &quot;지원 = 고통&quot;으로 학습된 상태입니다.
자기검열에 빠지기 쉬워 &quot;결과/숫자&quot;보다 &quot;행동/리듬&quot;으로 평가받기를 원합니다.

# Response (응답 형식)
반드시 아래 구조 그대로, 빈 줄 포함해서 출력하세요.

🌅 &lt;b&gt;{M/D(요일)} 일정 브리핑&lt;/b&gt;

🎯 &lt;b&gt;목표&lt;/b&gt;
• 최소 목표: ...
• 기본 목표: ...
• 여유가 된다면: ...

📋 &lt;b&gt;오늘의 할일&lt;/b&gt;
• (시간) 작업명

🚫 &lt;b&gt;회피 행동 경고&lt;/b&gt;
• ...

💪 &lt;b&gt;한마디&lt;/b&gt;
(2~3줄. 행동 기준 평가, 자기검열 경계, 차분한 응원)

규칙:
- 인사말 없이 바로 본문부터
- 위 5개 섹션 외 다른 섹션 추가 금지
- 계획표에 없는 일을 만들어 넣지 말 것</code></pre>
<h2 id="3단계--완성된-메시지를-텔레그램으로-보낸다"><strong>3단계 — 완성된 메시지를 텔레그램으로 보낸다</strong></h2>
<p>이렇게 AI가 써준 브리핑을 텔레그램으로 메시지를 보내줍니다. 읽고(1단계), 정리하고(2단계), 보내는(3단계) 구조입니다.</p>
<h1 id="3-claude와의-대화로-전체-구조-잡기">3. Claude와의 대화로 전체 구조 잡기</h1>
<p>방향은 정했지만 텔레그램 봇을 만들어본 적은 없었습니다. 그래서 인프라는 최소로 하고 일정은 마크다운으로 관리하면서, 매일 아침 브리핑을 보내는 봇을 어떻게 구성하면 좋을지 Claude와 이야기를 나눴습니다.</p>
<p>봇 토큰 발급부터 메시지 전송, 스케줄링까지 하나도 해본 적이 없었지만, 그 대화를 통해 아래와 같은 전체 설계를 잡을 수 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/22b74781-73b5-4e9d-b0a0-09fe1dd08d6c/image.png" alt="텔레그램 봇 시퀀스 다이어그램"></p>
<p>매일 새벽 5시에 GitHub Actions가 봇을 깨우면, 봇은 계획 파일에 오늘 날짜가 있는지 먼저 확인합니다. 있으면 AI가 쓴 브리핑을, 없으면 &quot;계획을 업데이트하세요&quot; 리마인더를 텔레그램으로 보냅니다.</p>
<h2 id="4-github-actions로-무료-스케줄-걸어놓기">4. GitHub Actions로 무료 스케줄 걸어놓기</h2>
<p>마지막으로, 매일 아침 5시에 메시지를 받기 위해 텔레그램 봇을 GitHub Actions로 스케줄에 걸어뒀습니다. 별도 서버 없이 무료로 매일 자동 실행할 수 있습니다.</p>
<pre><code class="language-yaml">on:
  schedule:
    - cron: &quot;0 20 * * *&quot; # UTC 20:00 = KST 05:00
  workflow_dispatch:</code></pre>
<h2 id="마치며">마치며</h2>
<p>스케줄을 걸어둔 다음 날, 아침 5시에 올 줄 알았던 메시지가 오지 않았습니다. 한참을 기다리다 확인해 보니, 한 시간 늦은 오전 6시에 도착해 있었습니다.</p>
<p>분명 cron은 KST 05:00에 맞춰뒀는데 왜 6시에 왔는지, 스케줄 설정이 잘못된 건지는 다음 글에서 직접 확인해 보려 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[텔레그램 봇 만들기 #1: 매일 아침 해야 할 일을 브리핑 해주는 개인 비서가 있었으면 좋겠다]]></title>
            <link>https://velog.io/@soleil_lucy_75/daily-briefing-bot-planning-1</link>
            <guid>https://velog.io/@soleil_lucy_75/daily-briefing-bot-planning-1</guid>
            <pubDate>Sun, 31 May 2026 10:33:47 GMT</pubDate>
            <description><![CDATA[<h1 id="문제-왜-나는-할-일을-자꾸-미룰까">문제: 왜 나는 할 일을 자꾸 미룰까?</h1>
<p>최근 들어 계획한 일들을 자꾸 미루는 저를 발견했습니다.</p>
<p>&quot;이대로는 안 되겠다&quot; 싶어서 일요일 저녁마다 다음 주 계획을 세우기 시작했습니다. 그런데 막상 평일이 되면 그 계획을 잘 실행하지 못했습니다.</p>
<p>가만히 들여다보니 패턴이 보였습니다. <strong>계획을 짜두기만 하고 눈앞에서 보이지 않으면, 결국 미루게 된다는 점</strong>이었습니다.</p>
<h2 id="떠오른-생각">떠오른 생각</h2>
<p>그러던 중 들었던 생각이 하나 있었습니다.</p>
<blockquote>
<p>&quot;내가 계획표를 찾아가는 게 아니라, <strong>계획표가 나를 찾아오면 되지 않을까?</strong>&quot;</p>
</blockquote>
<p>저는 휴대폰을 항상 손에 들고 있고, 알람이 오면 무조건 확인하는 습관이 있었습니다. 그렇다면 <strong>메신저로 그 날 해야 할 일을 정리해서 받아보면 어떨까?</strong> 라는 생각을 했습니다.</p>
<p>그래서 <code>매일 아침 5시, 일어나기도 전에 오늘 할 일을 텔레그램 메신저로 알려주는 봇</code>을 만들기로 결정했습니다.</p>
<h1 id="기획-매일-아침-할-일을-브리핑-해주는-비서가-있었으면-좋겠다">기획: 매일 아침 할 일을 브리핑 해주는 비서가 있었으면 좋겠다</h1>
<h3 id="내가-상상한-일정-브리핑-비서">내가 상상한 일정 브리핑 비서</h3>
<p>머릿속에서 상상한 일정 브리핑 비서는 아래와 같습니다.</p>
<ol>
<li>아무것도 안 해도 매일 아침 텔레그램 알림이 옴</li>
<li>열어보면 &quot;오늘은 이런 날이고, 이거 하나는 꼭 하자&quot; 라는 한 페이지짜리 브리핑</li>
<li>결과가 아니라 <strong>행동 기준</strong>으로 적혀 있음 (&quot;지원 1건 제출 = 오늘 성공&quot;)</li>
<li>회피 행동까지 미리 짚어줌 (&quot;README 먼저 완벽히 만들고 지원하자 → NO&quot;)</li>
</ol>
<h3 id="mvp-범위">MVP 범위</h3>
<p>처음부터 너무 크게 잡으면 끝내지 못할 거라는 걸 알아서 아래와 같이 MVP를 정했습니다.</p>
<p><strong>핵심 기능:</strong></p>
<ul>
<li>weekly_plan.md 를 읽어서 오늘 분량만 추출</li>
<li>AI가 오늘 맞춤 브리핑 작성</li>
<li>텔레그램으로 푸시</li>
<li>매일 자동 실행(스케줄)</li>
</ul>
<hr>
<p>다음 글에서는 이 MVP를 어떻게 설계하고 구현했는지에 대해 작성해보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[『만들면서 배우는 AI 에이전트 개발 입문+실전』으로 시작하는 AI 에이전트 공부]]></title>
            <link>https://velog.io/@soleil_lucy_75/building-ai-agents-book-review</link>
            <guid>https://velog.io/@soleil_lucy_75/building-ai-agents-book-review</guid>
            <pubDate>Sun, 24 May 2026 14:43:14 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>*&quot;한빛미디어 서평단 &lt;나는리뷰어다&gt; 활동을 위해서 책을 협찬받아 작성된 서평입니다.&quot;*</strong></p>
</blockquote>
<h1 id="책을-읽게-된-계기">책을 읽게 된 계기</h1>
<p>요즘 자주 들리는 &#39;에이전트&#39;라는 말이 정확히 무엇을 뜻하는지, 또 실제로는 어떻게 만드는 것인지 늘 궁금했습니다. 개념은 어렴풋이 알 것 같으면서도, 막상 직접 만들어 보려고 하면 어디서부터 손대야 할지 막막 했기 때문입니다.
그러던 중 이 책을 알게 되었는데, 단순히 개념만 설명하는 데서 그치지 않고 실제로 어떻게 만들면 되는지까지 다루는 책이라고 생각되어 선택하게 되었습니다.
특히 랭체인(LangChain)과 랭그래프(LangGraph) 같은 프레임워크가 실제로 어떻게 사용되는지 궁금했는데, 마침 책에 실습 형태로 정리되어 있어서 이 궁금증을 어느 정도 해결할 수 있을 거라고 기대했습니다.</p>
<h1 id="인사이트">인사이트</h1>
<h2 id="대표적인-추론-패턴-react와-reflection">대표적인 추론 패턴: ReAct와 Reflection</h2>
<p>에이전트 설계에 널리 활용되는 추론 패턴으로 ReAct와 Reflection이 있습니다.</p>
<h3 id="react">ReAct</h3>
<p>ReAct는 <code>추론</code>을 의미하는 <code>Reasoning</code>과 <code>행동</code>을 의미하는 <code>Acting</code>을 합친 말입니다. 문제에 대한 사고 과정과 실제 행동을 함께 수행하도록 유도함으로써, 보다 정확하고 근거 있는 답변을 생성하도록 설계된 방법론입니다. </p>
<p>단순히 답을 떠올리는 데서 끝나지 않고, 생각한 것을 행동으로 옮기고 그 결과를 다시 확인한다는 점이 핵심인 듯합니다.</p>
<p>구체적으로는 생각, 행동, 관찰의 3단계로 이루어집니다.</p>
<ul>
<li><strong><code>생각</code></strong>: 문제를 해결하기 위해 어떤 순서로 작업을 수행해야 할지 판단</li>
<li><strong><code>행동</code></strong>: 그 생각에 기반해 실제로 행동</li>
<li><strong><code>관찰</code></strong>: 행동의 결과를 확인</li>
</ul>
<p>그리고 다시 남은 작업에 대해 생각 → 행동 → 결과 관찰의 과정을 반복하면서 최적의 결과에 다가가도록 유도하는 방식입니다. 사람이 문제를 풀 때 한 번에 답을 내지 않고 시도하고 확인하며 조정하는 과정과 닮아 있다는 생각이 들었습니다.</p>
<h3 id="reflection">Reflection</h3>
<p>Reflection은 말 그대로 <code>&#39;반성&#39;</code>이라는 뜻으로, 초기 답변에 대한 반성을 통해 더 정확한 결과를 도출하도록 하는 기법입니다. </p>
<p>답변을 바로 내놓지 않고 <code>&quot;왜 그렇게 생각했는지&quot;</code> 근거를 설명하게 하여 오류를 줄이는 전략입니다.</p>
<h2 id="싱글-에이전트">싱글 에이전트</h2>
<p>싱글 에이전트는 <code>하나의 에이전트가 독립적으로 모든 추론과 행동을 수행하는 시스템</code>을 의미합니다. 이때 에이전트는 주어진 목적을 달성하기 위해 다음과 같은 일을 할 수 있어야 합니다.</p>
<ul>
<li>현재 상황을 해석하고</li>
<li>그에 맞는 행동을 스스로 결정하며</li>
<li>필요하다면 도구를 활용해 실제 실행까지 수행</li>
</ul>
<p>결국 싱글 에이전트 구조의 핵심은 하나의 판단 주체가 여러 능력을 얼마나 통합적으로 활용할 수 있는가에 있다고 정리할 수 있겠습니다.</p>
<h2 id="멀티-에이전트">멀티 에이전트</h2>
<p>멀티 에이전트는 여러 개의 에이전트가 각자의 판단을 수행하며 상호작용하는 구조를 의미합니다. 핵심은 에이전트 간에 의사결정과 정보 교환이 발생하는가입니다.</p>
<p>멀티 에이전트 구조는 여러 개의 싱글 에이전트를 조합해 하나의 문제를 해결하는 구조라고 볼 수 있는데, 각 에이전트는 자신에게 특화된 역할과 판단 기준을 가지고 독립적으로 동작하며, 그 결과를 종합해 최종 목표를 달성합니다.</p>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/c6837d37-f451-45df-bb38-ba87d8d81929/image.png" alt="싱글 에이전트와 멀티 에이전트 비교 표"></p>
<h2 id="랭그래프와-랭체인">랭그래프와 랭체인</h2>
<h3 id="랭체인">랭체인</h3>
<p>랭체인(LangChain)은 LLM 애플리케이션을 개발하기 위한 프레임워크</p>
<h3 id="랭그래프">랭그래프</h3>
<p>랭그래프(LangGraph)는 다양한 에이전트 시스템을 설계하고 구현하기 위한 프레임워크로, 시스템의 로직을 노드와 엣지로 구성된 그래프로 표현하는 것이 특징입니다.</p>
<ul>
<li><strong>노드</strong>: 에이전트나 기능 단위</li>
<li><strong>엣지</strong>: 노드 간의 실행 경로</li>
</ul>
<p>이렇게 그래프로 구조를 잡으면 대규모 에이전트 시스템에서도 논리적 복잡성을 효과적으로 관리할 수 있다고 합니다.</p>
<h1 id="개인적인-계획">개인적인 계획</h1>
<p>아직은 랭그래프가 익숙하지 않다 보니, 책을 한 번 더 읽으면서 랭그래프에 익숙해지면, 일상에서 불편하다고 느꼈던 것들을 에이전트를 만들어서 해결해 볼 생각입니다. 예를 들면 내 경험과 맞는 채용 공고를 여기저기서 찾아 모아 주는 에이전트가 있을 것 같습니다. 여러 사이트를 돌며 공고를 모으고, 내 경험과 맞는지 판단해 추려 주는 일은 혼자 손으로 하기엔 번거로운 작업이라 에이전트와 잘 어울릴 것 같습니다. 조금 더 가벼운 쪽으로는, 꽃을 선물하고 싶을 때 기념일이나 받는 사람이 누구인지 같은 조건을 참고해 어떤 꽃이 좋을지 추천해 주는 에이전트도 만들어 보면 재밌을 것 같습니다.</p>
<h1 id="공부하면서-찍은-사진">공부하면서 찍은 사진</h1>
<h2 id="랭그래프-실습1">[랭그래프 실습1]</h2>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/5efcfcdc-602c-44e6-8e61-18b18cd277a6/image.png" alt="랭그래프 실습1"></p>
<h2 id="랭그래프-실습2">[랭그래프 실습2]</h2>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/0f0fdd8c-fdd6-4724-aaa0-728cc3d982ec/image.png" alt="랭그래프 실습2"></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[『하루 30분 나는 제미나이로 돈을 번다』, 실제로 도움 되는 내용일까?]]></title>
            <link>https://velog.io/@soleil_lucy_75/gemini-ai-income-review</link>
            <guid>https://velog.io/@soleil_lucy_75/gemini-ai-income-review</guid>
            <pubDate>Sun, 26 Apr 2026 14:47:26 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>*&quot;한빛미디어 서평단 &lt;나는리뷰어다&gt; 활동을 위해서 책을 협찬받아 작성된 서평입니다.&quot;*</strong></p>
</blockquote>
<h1 id="책을-읽게-된-계기">책을 읽게 된 계기</h1>
<p>최근 생성형 AI를 활용해 부업을 시도하는 사례가 빠르게 증가하고 있다. 이러한 흐름 속에서, 단순한 호기심을 넘어 실제로 어떤 방식으로 수익이 만들어 지는지 확인하고자 『하루 30분 나는 제미나이로 돈을 번다』를 읽게 되었다. 특히 ‘하루 30분’이라는 시간 제약 속에서도 성과를 만들어낸다는 점이 인상적으로 다가왔다.</p>
<h1 id="프롬프트-기법에-대해-배우다">프롬프트 기법에 대해 배우다</h1>
<p>책을 읽기 전까지 나는 나름대로 AI를 잘 활용하고 있다고 생각했다. 예를 들어 개발 블로그 글의 제목과 목차를 추천받고 싶을 때, “시니어 프론트엔드 엔지니어”라는 페르소나를 부여하고, 내가 어떤 내용을 정리하려는지 설명하는 방식으로 프롬프트를 작성했다.</p>
<blockquote>
<p>책을 읽기 전 프롬프트</p>
<p>&quot;너는 시니어 프론트엔드 엔지니어야. 나는 Next.js v16 App Router를 공부하면서 Proxy.ts 파일에 대해 공식문서에서 읽고 배운 점을 개발 블로그에 정리하고자 해. 제목과 목차를 추천해줘.”</p>
</blockquote>
<p>하지만 책을 읽고 나니, 내가 작성하던 프롬프트는 전반적으로 추상적인 수준에 머물러 있었다는 것을 깨닫게 되었다. 역할과 목적은 전달했지만, 어떤 맥락에서 이 요청을 하게 되었는지, 어떤 형식과 톤으로 답변을 원하는지에 대한 정보는 빠져 있었기 때문이다. 그 결과 원하는 답변을 얻기까지 여러 번의 추가 질문을 반복해야 했다.</p>
<p>책에서는 <code>CO-STAR</code>, <code>CoT</code>, <code>ToT</code>, <code>최소-최대 프롬프트</code>, <code>산파술 프롬프트</code> 등 다양한 기법을 소개하는데, 그중에서도 CO-STAR 구조가 특히 인상적이었다. 맥락(Context), 목표(Objective), 스타일(Style), 톤(Tone), 대상(Audience), 응답(Response)을 명확히 설정하는 방식은, 단순히 질문을 던지는 것이 아니라 하나의 ‘요청서’를 작성하는 것에 가까웠다.</p>
<p>이 구조를 기준으로 보면, 기존의 나의 프롬프트는 페르소나와 목적 정도만 포함된 추상적인 형태였다. 반면 CO-STAR 방식으로 요청을 구성한다면, 불필요한 대화를 반복하지 않고도 원하는 결과를 얻을 수 있을 것이라는 확신이 들었다.</p>
<blockquote>
<p>CO-STAR 기법을 사용한 프롬프트</p>
<p>Context: Next.js v16 App Router를 학습하며 Proxy.ts 파일에 대한 공식 문서를 읽고 이해한 내용을 개발 블로그에 정리하려는 상황이다.</p>
<p>Objective: 개발 블로그 글의 제목과 목차를 추천받고 싶다.</p>
<p>Style: 기술 블로그 스타일로, 구조가 명확하고 흐름이 자연스럽게 이어지도록 구성</p>
<p>Tone: 사무적이고 설명 중심의 톤</p>
<p>Audience: Next.js를 학습 중인 주니어 개발자</p>
<p>Response: 3~5개의 제목 후보와, 각 제목에 맞는 목차를 마크다운 형식으로 제시</p>
</blockquote>
<h1 id="인사이트">인사이트</h1>
<p>AI를 활용한 수익화 방법이 궁금해 읽은 책이었지만, 실제로는 프롬프트 기법뿐만 아니라 나만의 경험을 콘텐츠로 수익화 하는 구조에 대해서도 생각해보는 계기가 되었다.</p>
<p>특히 마케팅과 홍보 전략에 대한 내용을 보면서, 강의 플랫폼에서 여러 강의를 묶어 판매하는 방식이나, 강의를 제공하는 사람들이 별도의 커뮤니티를 함께 운영하는 이유가 단순한 마케팅 전략이 아니라, 하나의 수익화 구조라는 점을 이해하게 되었다.</p>
<p>결국 많은 사람들이 자신의 경험과 지식을 단순히 공유하는 데 그치지 않고, 다른 사람에게 도움이 되는 형태로 가공하여 유료 콘텐츠로 전환하고 있었다.</p>
<p>나 역시 단순히 AI를 활용하는 수준에 머무르기보다, 지금까지의 경험을 정리하고 이를 다른 사람에게 전달 가능한 형태로 가공하는 시도를 해볼 필요가 있다고 판단했다. 우선 도움이 될 만한 경험부터 찾아봐야겠다!</p>
<h1 id="개인적인-적용-방향과-계획">개인적인 적용 방향과 계획</h1>
<p>앞으로는 AI를 사용할 때, 단순히 질문을 던지는 것이 아니라 구조화된 프롬프트를 작성하는 방식으로 접근해볼 계획이다. 특히 반복적으로 수행하는 작업을 중심으로 AI를 활용하는 경험을 쌓고자 한다.</p>
<p>구체적으로는 취업 준비 과정에서 반복되는 작업들을 자동화하는 방향을 고려하고 있다. 채용 공고를 탐색하고, 지원서를 작성하며, 기업에 맞춰 내용을 수정하는 과정은 많은 시간이 소요되는 과정에서 AI를 활용해서 효율적으로 처리할 수 있는 방법을 시도해보고자 한다.</p>
<p>취업 준비 기간이 길어지면서 집중도가 떨어지는 순간들이 반복되고 있는데, 채용 지원 루틴을 만들 수 있도록 AI가 일정 부분 도움을 줄 수 있을 것이라 기대하고 있다. 단순한 생산성 향상을 넘어, 지속적으로 행동할 수 있는 환경을 만드는 도구로 활용해보고자 한다.</p>
<h1 id="읽으며-기록한-내용">읽으며 기록한 내용</h1>
<h2 id="프롬프트-엔지니어링에-대해서">[프롬프트 엔지니어링에 대해서]</h2>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/0e788ebd-97f8-4963-8e57-6c963d820d14/image.png" alt="밑줄 긋기한 전자책"></p>
<h2 id="책에-나온-미션-1-수행해보기">[책에 나온 미션 1 수행해보기]</h2>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/47db93d2-b8be-4e8c-ad88-51f38c04d436/image.png" alt="책에 나온 미션 1 진행"></p>
<h2 id="책에-나온-미션-2-수행해보기">[책에 나온 미션 2 수행해보기]</h2>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/bd6aecd1-b664-4b72-a6dc-f216c9e1b6e2/image.png" alt="책에 나온 미션 2 진행"></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[페이지 접근마다 반복되는 로그인 체크, Next.js proxy.ts로 한 곳에서 관리하기]]></title>
            <link>https://velog.io/@soleil_lucy_75/nextjs-proxy-login-check-in-one-place</link>
            <guid>https://velog.io/@soleil_lucy_75/nextjs-proxy-login-check-in-one-place</guid>
            <pubDate>Sun, 12 Apr 2026 06:42:55 GMT</pubDate>
            <description><![CDATA[<h1 id="글을-쓰게-된-이유">글을 쓰게 된 이유</h1>
<p>프로젝트를 진행하다가 로그인 여부에 따라 페이지 접근을 제어하는 기능을 구현해야 했습니다. 처음에는 각 페이지 컴포넌트마다 로그인 체크 로직을 넣어야 하나 했는데, 보호해야 할 페이지가 여러 개이고 한 곳에서 관리하는 게 효율적이라는 생각이 들었습니다. 찾아보니 이런 공통 로직은 미들웨어로 한 곳에서 처리하는 게 일반적이었고, Next.js 16에서는 <code>proxy.ts</code> 파일이 그 역할을 한다는 것을 알게 되었습니다. 이 글은 그 과정에서 공부한 내용을 정리한 글입니다.</p>
<h1 id="문제-로그인-체크-로직-어디에-써야-할까">문제: 로그인 체크 로직, 어디에 써야 할까?</h1>
<p>제가 만든 서비스는 로그인한 사용자만 LLM 레시피 추출 기능을 사용하고, 마이페이지에서 레시피를 관리할 수 있도록 구현하고 싶었습니다. </p>
<p>로그인 여부를 체크해야 하는 페이지는 아래와 같습니다 — 사실상 서비스의 모든 페이지입니다.</p>
<ul>
<li>사용자가 추출하고 싶은 레시피 URL을 입력하는 메인 페이지</li>
<li>레시피 추출 결과를 확인하는 페이지</li>
<li>레시피를 단계별로 안내하는 요리 화면</li>
<li>마이페이지 — 요리 기록을 확인하는 페이지</li>
<li>마이페이지 — 요리 결과 통계 페이지</li>
<li>마이페이지 — 냉장고 재료 재고 관리 페이지</li>
<li>마이페이지 — 추출한 레시피 관리 페이지</li>
<li>마이페이지 — 개인 설정 페이지</li>
</ul>
<p>각 페이지 컴포넌트마다 로그인 체크 로직을 작성한다면 동일한 코드가 여러 곳에 흩어지고, 나중에 수정할 때도 모든 파일을 일일이 찾아가야 하는 문제가 생깁니다. 한 곳에서 관리할 수 있는 방법이 필요하다고 생각했습니다.</p>
<h1 id="해결-방안-미들웨어로-인증-체크를-한-곳에서-처리하기">해결 방안: 미들웨어로 인증 체크를 한 곳에서 처리하기</h1>
<p>한 곳에서 관리할 수 있는 방법을 찾다가 <code>미들웨어</code>라는 개념이 떠올랐습니다. <code>미들웨어</code>란 요청이 실제로 처리되기 전에 실행되는 중간 레이어입니다. 모든 페이지 요청이 미들웨어를 거쳐서 들어오기 때문에, 이 영역에서 로그인 여부를 체크하는 로직을 관리하면 좋겠다는 생각을 했습니다.</p>
<p>그래서 Next.js에서는 미들웨어를 어떻게 작성하는지 공식 문서를 찾아봤습니다.</p>
<p><a href="https://nextjs.org/docs/app/getting-started/proxy">Proxy | Next.js 공식 문서</a></p>
<h2 id="proxyts란">proxy.ts란?</h2>
<p>Next.js 16부터 기존의 <code>middleware.ts</code>는 <code>proxy.ts</code>로 이름이 바뀌었습니다. 기능은 동일하고, 이름이 역할을 더 잘 반영하도록 변경된 것입니다. <code>proxy.ts</code>는 요청이 완료되기 전에 코드를 실행할 수 있게 해주는 파일로, 요청을 가로채서 리다이렉트, 리라이트, 헤더 수정 등을 처리할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/df28c7ac-e904-4c4f-83ab-76ad9d5bf1ba/image.png" alt="proxy.ts 설명 시퀀스 다이어그램"></p>
<h2 id="proxyts를-사용하는-경우">proxy.ts를 사용하는 경우</h2>
<p>공식 문서에서는 아래와 같은 상황에서 사용을 권장합니다.</p>
<ul>
<li>모든 페이지 또는 일부 페이지의 헤더를 수정해야 할 때</li>
<li>A/B 테스트처럼 사용자 그룹에 따라 다른 페이지를 보여줘야 할 때</li>
<li>요청 정보를 기반으로 프로그래밍 방식의 리다이렉트가 필요할 때</li>
</ul>
<h2 id="proxyts-사용을-지양해야-하는-경우">proxy.ts 사용을 지양해야 하는 경우</h2>
<p><code>proxy.ts</code>는 모든 요청마다 실행되기 때문에 여기서 무거운 작업을 하면 전체 페이지 로딩 성능에 영향을 줍니다. 공식 문서에서 명시적으로 금지하는 경우는 아래와 같습니다.</p>
<ul>
<li>DB 조회나 외부 API 호출 같은 느린 데이터 페칭</li>
<li>완전한 세션 관리나 인증 솔루션으로의 사용</li>
<li>fetch에서 cache, revalidate, tags 옵션 사용</li>
</ul>
<p>복잡한 로직은 API Routes나 Server Component에서 처리하는 것이 적절합니다.</p>
<h2 id="proxyts에-로그인-체크-로직을-구현해도-될까">proxy.ts에 로그인 체크 로직을 구현해도 될까?</h2>
<p>제가 구현하려는 것은 쿠키에 저장된 토큰의 존재 여부만 확인하는 가벼운 체크입니다. DB 조회나 외부 API 호출 없이 빠르게 판단할 수 있기 때문에 적절한 케이스라고 판단해 <code>proxy.ts</code>에 로그인 체크 로직을 구현하기로 결정했습니다.</p>
<h1 id="결과">결과</h1>
<p>아래는 실제 프로젝트에 적용한 <code>proxy.ts</code> 코드입니다.
<a href="https://github.com/hyer0705/ACCIO-RECIPE/blob/main/src/proxy.ts">실제 코드 보러가기</a></p>
<pre><code class="language-tsx">import { withAuth } from &#39;next-auth/middleware&#39;;
import { NextResponse } from &#39;next/server&#39;;

export default withAuth(
  function middleware(req) {
    const { token } = req.nextauth;
    const { pathname } = req.nextUrl;

    // 인증은 되었으나 추가 정보 입력(isComplete)이 안 된 경우 /signup으로 리다이렉트
    if (token &amp;&amp; !token.isComplete &amp;&amp; pathname !== &#39;/signup&#39;) {
      return NextResponse.redirect(new URL(&#39;/signup&#39;, req.url));
    }

    return NextResponse.next();
  },
  {
    callbacks: {
      authorized: ({ token }) =&gt; !!token,
    },
    pages: {
      signIn: &#39;/login&#39;,
    },
  },
);

// 보호할 경로 목록 — /login, /api/*, /docs, /_next 는 제외
export const config = {
  matcher: [&#39;/((?!login|api|docs|_next/static|_next/image|favicon.ico).*)&#39;],
};</code></pre>
<p><code>config.matcher</code>로 proxy.ts가 실행될 경로를 지정하고, next-auth의 <code>withAuth</code>를 사용해 토큰 존재 여부를 확인합니다. 토큰이 없으면 자동으로 <code>/login</code>으로 리다이렉트되고, 토큰은 있지만 추가 정보 입력(<code>isComplete</code>)이 완료되지 않은 경우에는 <code>/signup</code>으로 보냅니다.</p>
<p>각 페이지 컴포넌트마다 로그인 체크 로직을 반복해서 작성하는 대신, 미리 더 나은 방법을 찾아보고 싶었습니다. <code>proxy.ts</code>를 도입한 덕분에 처음부터 로그인 체크 로직을 한 곳에서 관리할 수 있게 됐고, 보호할 경로를 추가하거나 변경할 때도 <code>proxy.ts</code>만 수정하면 됩니다.</p>
<h1 id="참고-자료">참고 자료</h1>
<ul>
<li><a href="https://nextjs.org/docs/app/getting-started/proxy">proxy.ts | Next.js 공식 문서</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AbortController, 진짜로 요청이 취소 될까? 직접 확인해보기]]></title>
            <link>https://velog.io/@soleil_lucy_75/abortcontroller-request-cancel-test</link>
            <guid>https://velog.io/@soleil_lucy_75/abortcontroller-request-cancel-test</guid>
            <pubDate>Sun, 29 Mar 2026 09:58:41 GMT</pubDate>
            <description><![CDATA[<h1 id="들어가기-전에">들어가기 전에</h1>
<p>이전에 <a href="https://velog.io/@soleil_lucy_75/abort-controller-cancel-api-request">API 요청을 취소하는 방법: AbortController로 백엔드 API, LLM 요청 중단하기</a> 글을 작성하면서, 불필요한 API 요청을 취소하기 위한 방법으로 <code>AbortController</code>를 사용하는 방법을 정리한 적이 있습니다.</p>
<p>당시에는 “요청을 취소할 수 있다”는 사용 방법 중심으로 이해하고 넘어갔지만, 문득 이런 의문이 들었습니다.</p>
<blockquote>
<p>Q. 정말로 요청이 취소되는 건가?</p>
</blockquote>
<p>단순히 클라이언트에서 요청을 무시하는 것인지, 아니면 실제로 네트워크 요청 자체가 중단되고 서버에도 영향이 있는지에 대해서는 명확하게 확인해본 적이 없었습니다.</p>
<p>그래서 이번 글에서는 <code>AbortController</code>를 사용했을 때 요청이 실제로 어떻게 취소되는지 확인해보려고 합니다.</p>
<h1 id="테스트-시나리오-검색어-입력-시-api-요청이-쌓이는-상황">테스트 시나리오: 검색어 입력 시 API 요청이 쌓이는 상황</h1>
<p>AbortController가 실제로 어떻게 동작하는지 확인하기 위한 테스트 시나리오로 “검색어 입력” 상황을 떠올렸습니다.</p>
<p>검색창에 글자를 입력할 때마다 API 요청이 발생하는 구조는 실제 서비스에서 자주 사용되는 패턴입니다. 특히 입력이 빠르게 변경되는 경우, 이전 요청이 아직 완료되지 않은 상태에서 새로운 요청이 계속 발생할 수 있습니다.</p>
<p>이때 더 이상 필요하지 않은 이전 요청을 그대로 두면, 불필요한 요청이 계속 쌓이거나 늦게 도착한 응답이 최신 상태를 덮어쓰는 문제가 발생할 수 있습니다. 이러한 상황에서 이전 요청을 취소하기 위한 방법으로 AbortController를 사용한다는 것을 기존에 작성했던 글에서 언급했습니다. 하지만 실제로 요청이 어떻게 취소 되는지에 대해서는 명확하게 검증해보지 못했습니다.</p>
<p>이번 글에서는 단순한 테스트 환경을 따로 구성하여, AbortController가 실제로 어떻게 동작 하는지 직접 확인해보기로 했습니다.</p>
<h1 id="테스트-환경-구성">테스트 환경 구성</h1>
<p>검색어 입력 시 쌓이는 Backend API 요청을 취소하는 동작을 확인하기 위해, 최대한 단순한 구조의 테스트 환경을 구성했습니다.</p>
<p>복잡한 프레임워크나 상태 관리 로직을 배제하고, AbortController의 동작 자체를 명확하게 확인 하는 데 집중하기 위해 다음과 같은 기술을 선택했습니다.</p>
<h2 id="사용-기술">사용 기술</h2>
<ul>
<li><strong>Node.js (Express)</strong></li>
</ul>
<pre><code>간단한 검색 API(`/search`)를 구현하기 위해 사용했습니다. 요청 마다 의도적으로 지연을 주어, 이전 요청이 완료되기 전에 새로운 요청이 발생하도록 구성했습니다.</code></pre><ul>
<li><strong>Vanilla JavaScript</strong></li>
</ul>
<pre><code>입력 이벤트와 API 요청, AbortController를 직접 제어하기 위해 사용했습니다.</code></pre><ul>
<li><strong>HTML5 / CSS3</strong></li>
</ul>
<pre><code>검색 입력창과 결과를 표현하기 위한 최소한의 UI를 구성했습니다.</code></pre><h2 id="폴더-구조">폴더 구조</h2>
<pre><code>.
├── package.json
├── package-lock.json
├── public              # 클라이언트 코드
│   ├── index.html      # 검색 입력 UI
│   ├── app.js          # AbortController 및 요청 처리 로직
│   └── style.css       # 스타일
└── src
    └── server.js       # Express 서버 및 /search API</code></pre><h2 id="시스템-아키텍처">시스템 아키텍처</h2>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/fd50cff0-f226-4c11-8dd3-6d448ebc521b/image.png" alt="시스템 아키텍처 다이어그램"></p>
<p>사용자의 검색어 입력이 변경될 때마다 새로운 API 요청이 발생하고, 이전에 진행 중이던 요청은 AbortController를 통해 취소되는 구조입니다.</p>
<h1 id="abortcontroller-적용-방식">AbortController 적용 방식</h1>
<p>AbortController를 적용한 방식을 설명해보겠습니다.</p>
<p>이번 테스트에서는 검색어 입력이 변경될 때마다 새로운 API 요청이 발생하도록 구현했습니다. 이때 이전 요청이 아직 완료되지 않은 상태라면, 해당 요청을 취소하도록 AbortController를 적용했습니다.</p>
<h2 id="1️⃣-프론트엔드에서-이전-요청-취소하기">1️⃣ 프론트엔드에서 이전 요청 취소하기</h2>
<p>이전 요청이 아직 완료되지 않은 상태에서 새로운 요청이 발생할 수 있기 때문에, 현재 진행 중인 요청이 있는 경우 새로운 요청을 보내기 전에 <code>abort()</code>를 호출하여 이전 요청을 취소하도록 구현했습니다.</p>
<pre><code class="language-jsx">if (currentController) {
  currentController.abort(&quot;새로운 검색 요청이 들어왔습니다.&quot;);
}</code></pre>
<p>이렇게 하면 사용자가 검색어를 빠르게 변경하더라도, 더 이상 필요하지 않은 이전 요청을 정리하고 가장 최근 요청만 유지할 수 있습니다.</p>
<h2 id="2️⃣-새로운-요청을-위한-abortcontroller-생성">2️⃣ 새로운 요청을 위한 AbortController 생성</h2>
<p>하나의 <code>AbortController</code> 객체는 하나의 요청 흐름과 연결된다고 보고 사용하는 것이 적절합니다. 그래서 새 요청을 보낼 때마다 새로운 <code>controller</code>를 생성하고, 이를 현재 요청의 기준점으로 사용했습니다.</p>
<pre><code class="language-jsx">currentController = new AbortController();</code></pre>
<p>이전 요청을 취소한 뒤에는 새로운 요청을 처리하기 위해 <code>AbortController</code>를 다시 생성 했으며, 이후 이 controller의 <code>signal</code>을 <code>fetch</code>에 전달하여 해당 요청과 연결했습니다.</p>
<h2 id="3️⃣-fetch-요청에-signal-전달">3️⃣ fetch 요청에 signal 전달</h2>
<p><code>abort()</code>를 호출하는 것만으로 요청이 취소되는 것이 아니라, <code>fetch</code>가 해당 <code>signal</code>과 연결되어 있어야 실제 취소 동작이 가능합니다.</p>
<p>그래서 생성한 <code>AbortController</code>의 <code>signal</code>을 <code>fetch</code> 요청에 전달하여, 요청과 controller를 연결했습니다.</p>
<pre><code class="language-jsx">const response = await fetch(`/search?q=${query}`, {
  signal: currentController.signal,
});</code></pre>
<p>이렇게 <code>signal</code>을 전달하면 이후 <code>abort()</code>가 호출되었을 때, 해당 요청을 중단할 수 있습니다.</p>
<h2 id="4️⃣-취소된-요청-처리">4️⃣ 취소된 요청 처리</h2>
<p>요청이 취소될 경우 <code>fetch</code>는 에러를 발생시키기 때문에, <code>try-catch</code>를 통해 취소된 요청과 일반 에러를 구분해서 처리했습니다.</p>
<pre><code class="language-jsx">try{
    // ...
} catch (error) {
  if (error.name === &quot;AbortError&quot;) {
    console.log(&quot;이전 요청이 취소되었습니다.&quot;);
    return;
  }

  console.error(error);
}</code></pre>
<h2 id="5️⃣-서버에서-연결-종료를-확인할-수-있도록-구성하기">5️⃣ <strong>서버에서 연결 종료를 확인할 수 있도록 구성하기</strong></h2>
<p>이번 테스트에서는 프론트엔드에서 요청을 취소하는 것뿐만 아니라, 서버에서도 연결 종료를 감지할 수 있도록 구성했습니다.</p>
<pre><code class="language-jsx">req.on(&quot;close&quot;, () =&gt; {
  console.log(`[server] client connection closed: q=&quot;${q}&quot;`);
});</code></pre>
<p><code>req.on(&quot;close&quot;)</code> 이벤트를 통해 클라이언트가 요청을 취소했을 때, 서버에서 연결이 종료 되었는지 확인할 수 있도록 했습니다.</p>
<p>또한 응답 전에 <code>3초</code> 지연을 넣어, 이전 요청이 완료되기 전에 새로운 요청이 충분히 발생할 수 있도록 구성했습니다.</p>
<p>이 설정을 통해 다음 두 가지를 확인할 수 있도록 했습니다.</p>
<ul>
<li>클라이언트에서 요청을 취소했을 때 서버 연결이 실제로 종료되는지</li>
<li>연결이 종료된 이후에도 서버 로직은 계속 실행되는지</li>
</ul>
<h2 id="전체-코드">전체 코드</h2>
<p>지금까지 설명한 내용을 포함한 전체 코드는 아래와 같습니다.</p>
<h3 id="프론트엔드">프론트엔드</h3>
<pre><code class="language-jsx">const searchInput = document.getElementById(&quot;searchInput&quot;);
const statusEl = document.getElementById(&quot;status&quot;);
const resultsEl = document.getElementById(&quot;results&quot;);

let currentController = null;
let requestId = 0;

// ...

async function search(query) {
  if (!query) {
    statusEl.textContent = &quot;검색어를 입력해 주세요.&quot;;
    resultsEl.innerHTML = &quot;&quot;;
    return;
  }

  if (currentController) {
    currentController.abort(&quot;새로운 검색 요청이 들어왔습니다.&quot;);
    console.log(&quot;[client] abort() 호출 후&quot;, {
      abortedAfter: currentController.signal.aborted,
      reason: currentController.signal.reason,
    });
  }

  currentController = new AbortController();
  const myController = currentController;
  const myRequestId = ++requestId;

  console.log(`[client] request start #${myRequestId}: &quot;${query}&quot;`);

  try {
    const response = await fetch(`/search?q=${encodeURIComponent(query)}`, {
      signal: myController.signal,
    });

    console.log(`[client] fetch 완료 #${myRequestId}`);

    const data = await response.json();

    console.log(`[client] response success #${myRequestId}:`, data);

    statusEl.textContent = `&quot;${data.query}&quot; 검색 완료`;
    renderResults(data.results);
  } catch (error) {
    console.log(`[client] catch 진입 #${myRequestId}`, error);

    if (error.name === &quot;AbortError&quot;) {
      console.log(`[client] request aborted #${myRequestId}: &quot;${query}&quot;`);
      console.log(&quot;[client] signal 상태:&quot;, {
        aborted: myController.signal.aborted,
        reason: myController.signal.reason,
      });
      return;
    }

    console.error(`[client] request failed #${myRequestId}:`, error);
    statusEl.textContent = &quot;에러가 발생했습니다.&quot;;
  }
}

searchInput.addEventListener(&quot;input&quot;, (event) =&gt; {
  const query = event.target.value.trim();
  search(query);
});</code></pre>
<h3 id="백엔드">백엔드</h3>
<pre><code class="language-jsx">// src/server.js (요청 취소 동작 확인용 코드)
app.get(&quot;/search&quot;, async (req, res) =&gt; {
  const q = String(req.query.q || &quot;&quot;).trim().toLowerCase();

  // 요청이 서버에 도달했는지 확인
  console.log(`[server] request received: q=&quot;${q}&quot;`);

  // 클라이언트가 요청을 취소했을 때 연결 종료 감지
  req.on(&quot;close&quot;, () =&gt; {
    console.log(`[server] client connection closed: q=&quot;${q}&quot;`);
  });

  // 일부러 지연을 주어 취소 상황을 만들기
  const delay = 3000;
  await new Promise((resolve) =&gt; setTimeout(resolve, delay));

  // 응답 생성 (연결이 끊겨도 실행됨)
  console.log(`[server] response sent: q=&quot;${q}&quot;, delay=${delay}ms`);

  res.json({
    query: q,
    delay,
  });
});</code></pre>
<h1 id="결과">결과</h1>
<h2 id="브라우저-개발자-도구-network-탭에서-확인">브라우저 개발자 도구 Network 탭에서 확인</h2>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/d5c04779-e07f-484b-ab54-8c6d636ae1b0/image.png" alt="브라우저 개발자 도구 네트워크 탭 결과 화면"></p>
<p>브라우저 개발자 도구의 Network 탭을 확인해보면, 다음과 같은 결과를 확인할 수 있습니다.</p>
<ul>
<li><code>search?q=a</code> → <code>(canceled)</code></li>
<li><code>search?q=ap</code> → <code>(canceled)</code></li>
<li><code>search?q=app</code> → <code>200</code></li>
</ul>
<p>이 결과를 통해 확인할 수 있는 점은 다음과 같습니다.</p>
<ul>
<li>이전 요청(<code>a</code>, <code>ap</code>)은 정상적으로 완료되지 않고 <strong>취소된 상태로 표시되었습니다.</strong></li>
<li>마지막 요청(<code>app</code>)만 정상적으로 응답(<code>200</code>)을 받았습니다.</li>
</ul>
<p>즉, AbortController를 통해 <strong>브라우저 레벨에서는 요청이 실제로 취소된 것처럼 동작하고 있음</strong>을 확인할 수 있었습니다.</p>
<h2 id="브라우저-개발자-도구-콘솔에서-확인">브라우저 개발자 도구 콘솔에서 확인</h2>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/3e436f5e-6729-462d-961c-e8599c8850c0/image.png" alt="브라우저 개발자 도구 클라이언트 콘솔 결과 화면"></p>
<p><code>abort()</code> 호출 이후 <code>signal.aborted</code> 값이 <code>true</code>로 변경되는 것을 통해, <code>AbortController</code> 자체는 정상적으로 동작하고 있음을 확인할 수 있었습니다.</p>
<pre><code class="language-bash">[client] abort() 호출 후
{abortedAfter: true, reason: &#39;새로운 검색 요청이 들어왔습니다.&#39;}</code></pre>
<p>마지막 요청인 <code>app</code>에 대해서는 아래와 같이 정상적으로 응답이 처리되는 것도 확인할 수 있었습니다.</p>
<pre><code class="language-bash">[client] request start #14: &quot;app&quot;
[client] fetch 완료 #14
[client] response success #14: { query: &#39;app&#39;, delay: 3000, results: Array(2) }</code></pre>
<p>클라이언트 콘솔에서는 다음 두 가지를 확인할 수 있었습니다.</p>
<ul>
<li>이전 요청은 <code>abort()</code> 호출 이후 취소 상태로 전환되었습니다.</li>
<li>가장 마지막 요청만 정상적으로 응답을 받아 화면에 반영되었습니다.</li>
</ul>
<h2 id="서버-로그에서-확인">서버 로그에서 확인</h2>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/0bb06b4b-598b-4bd7-8367-6e399ef4a0d5/image.png" alt="서버 로그 결과 화면"></p>
<p>서버 로그에서는 요청이 들어온 뒤 클라이언트 연결이 종료되는 흐름을 확인할 수 있었습니다.</p>
<pre><code class="language-bash">[server] request received: q=&quot;a&quot;
[server] client connection closed: q=&quot;a&quot;

[server] request received: q=&quot;ap&quot;
[server] client connection closed: q=&quot;ap&quot;</code></pre>
<p>이를 통해 클라이언트에서 요청을 취소했을 때 서버에서도 연결 종료를 감지할 수 있음을 확인했습니다.</p>
<p>또한 아래와 같이 응답 전송 로그도 함께 확인할 수 있었습니다.</p>
<pre><code>[server] response sent: q=&quot;a&quot;, delay=3000ms, count=8
[server] response sent: q=&quot;ap&quot;, delay=3000ms, count=6
[server] response sent: q=&quot;app&quot;, delay=3000ms, count=2</code></pre><p>즉, 클라이언트 연결은 종료 되더라도 서버 로직 자체는 계속 실행될 수 있음을 확인할 수 있었습니다.</p>
<hr>
<p>처음에는 <code>client connection closed</code> 로그를 보고 요청 자체가 서버에서 처리되지 않은 것이라고 생각했습니다.</p>
<p>하지만 로그를 자세히 살펴보면 <code>request received</code> 이후 <code>client connection closed</code>가 발생하고, 그 이후에도 <code>response sent</code> 로그가 출력되는 것을 확인할 수 있었습니다.</p>
<p>이를 통해 요청은 이미 서버에서 처리되기 시작한 상태이며, <code>AbortController</code>는 서버 작업을 중단시키는 것이 아니라 클라이언트가 해당 요청의 응답을 더 이상 받지 않도록 연결을 종료하는 방식으로 동작한다는 점을 확인할 수 있었습니다.</p>
<h1 id="정리-abortcontroller에-대해-알게-된-것">정리: AbortController에 대해 알게 된 것</h1>
<p>글을 작성하면서 다음과 같은 내용을 배울 수 있었습니다.</p>
<ul>
<li>하나의 요청 흐름마다 별도의 AbortController를 사용하는 방식이 명확하다.</li>
<li>AbortController는 요청 자체를 제거하는 것이 아니라, 클라이언트가 해당 요청의 응답을 더 이상 처리하지 않도록 만드는 방식으로 동작한다.</li>
<li>AbortController로 요청을 취소하더라도, 서버에서 이미 시작된 작업은 자동으로 중단되지 않는다.</li>
<li>서버 작업까지 중단하려면, 클라이언트 연결 종료를 감지한 뒤 별도의 중단 로직을 직접 구현해야 한다.</li>
</ul>
<p><code>AbortController</code>는 “요청을 완전히 취소하는 도구”라기보다, “클라이언트 관점에서 요청을 정리하는 도구”에 가깝다고 이해했습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[API 요청을 취소하는 방법: AbortController로 백엔드 API, LLM 요청 중단하기]]></title>
            <link>https://velog.io/@soleil_lucy_75/abort-controller-cancel-api-request</link>
            <guid>https://velog.io/@soleil_lucy_75/abort-controller-cancel-api-request</guid>
            <pubDate>Sun, 15 Mar 2026 10:22:19 GMT</pubDate>
            <description><![CDATA[<h1 id="문제-상황">문제 상황</h1>
<p>개인 프로젝트에서 유튜브 영상 URL을 입력하면 영상 속 레시피를 분석해 재료와 조리 과정을 구조화된 데이터로 추출하는 기능을 구현했습니다.</p>
<p>사용자가 유튜브 링크를 입력하면 해당 영상을 분석하고 레시피 정보를 추출하기 위해 LLM API를 호출하는 구조였습니다.</p>
<p>하지만 이 과정에서 응답을 받기까지 약 <strong>20~30초 정도의 시간이 소요</strong>되기도 했습니다.</p>
<p>테스트를 진행하다 보니 다음과 같은 상황이 종종 발생했습니다.</p>
<ul>
<li>분석 시간이 예상보다 길어져 <strong>중간에 요청을 중단하고 싶을 때</strong></li>
<li><strong>유튜브 링크를 잘못 입력해</strong> 요청을 취소하고 다시 시도하고 싶을 때</li>
<li>새로운 링크로 <strong>다시 분석을 요청하고 싶을 때</strong></li>
</ul>
<p>하지만 한 번 보낸 API 요청은 기본적으로 클라이언트에서 쉽게 취소할 수 없었습니다.</p>
<p>이 경우 더 이상 필요하지 않은 요청도 계속 서버에서 처리될 수 있었습니다. 특히 LLM 기반 분석 작업은 응답 시간이 길어질 수 있기 때문에, 사용자가 요청을 중단할 수 있는 UX가 중요하다고 느꼈습니다.</p>
<p>그래서 <strong>진행 중인 API 요청을 사용자가 직접 중단할 수 있는 방법을 찾게 되었습니다.</strong></p>
<h1 id="해결-방법-abortcontroller">해결 방법: AbortController</h1>
<p>문제를 해결하기 위해 LLM API 요청을 취소할 수 있는 방법을 찾아보았습니다.</p>
<p>조사해보니 JavaScript에서는 <code>AbortController</code>라는 Web API를 사용해 진행 중인 비동기 작업을 중단할 수 있다는 것을 알게 되었습니다.</p>
<p><code>AbortController</code>는 <code>fetch</code>와 같은 네트워크 요청에 취소 신호를 전달하여 요청을 중단할 수 있도록 해주는 인터페이스입니다.</p>
<p>이를 활용하면 다음과 같은 상황에서 API 요청을 취소할 수 있습니다.</p>
<ul>
<li>사용자가 페이지를 이탈했을 때</li>
<li>새로운 요청이 발생했을 때 이전 요청 취소</li>
<li>사용자가 직접 “취소” 버튼을 눌렀을 때</li>
</ul>
<p>이 기능을 활용하면 불필요한 네트워크 요청을 줄이고 사용자 경험을 개선할 수 있습니다.</p>
<h1 id="abortcontroller란">AbortController란?</h1>
<p><code>AbortController</code>는 진행 중인 비동기 작업을 중단할 수 있도록 해주는 Web API입니다. 대표적으로 <code>fetch</code>와 같은 HTTP 요청을 취소할 때 많이 사용됩니다.</p>
<p>일반적으로 브라우저에서 API 요청을 보내면, 요청이 시작된 이후에는 클라이언트에서 이를 직접 취소하기 어렵습니다. 하지만 <code>AbortController</code>를 사용하면 진행 중인 요청을 명시적으로 중단(abort)할 수 있습니다.</p>
<p><code>AbortController</code>는 크게 두 가지 요소로 구성됩니다.</p>
<ul>
<li>AbortController: 요청 취소를 제어하는 컨트롤러</li>
<li>AbortSignal: 취소 신호를 전달하는 객체</li>
</ul>
<p>먼저 <code>AbortController</code> 인스턴스를 생성한 뒤, 해당 객체의 <code>signal</code>을  API 요청에 전달합니다. 이후 필요할 때 <code>abort()</code> 메서드를 호출하면 요청이 중단됩니다.</p>
<pre><code class="language-jsx">const controller = new AbortController();

fetch(&quot;/api/data&quot;, {
  signal: controller.signal,
});

// 요청 취소
controller.abort();</code></pre>
<p><code>abort()</code>가 호출되면 해당 요청은 중단되고, <code>fetch</code>의 Promise는 <code>AbortError</code>와 함께 <code>reject</code> 됩니다.</p>
<p>이러한 방식으로 AbortController는 네트워크 요청, 스트림 처리, 응답 데이터 소비 등의 비동기 작업을 중간에 취소할 수 있는 메커니즘을 제공합니다.</p>
<h2 id="스트림-처리와-응답-데이터-소비란-무엇일까">스트림 처리와 응답 데이터 소비란 무엇일까?</h2>
<p>AbortController는 단순히 HTTP 요청 자체만 취소하는 것이 아니라, 요청 이후에 이어지는 데이터 처리 과정도 함께 중단할 수 있습니다.</p>
<p>예를 들어 <code>fetch</code> 요청이 완료된 뒤에도 다음과 같은 작업이 이어질 수 있습니다.</p>
<h3 id="1️⃣-스트림-처리">1️⃣ 스트림 처리</h3>
<p>일부 API는 데이터를 한 번에 보내지 않고 조각(chunk) 단위로 나누어 전송합니다. 대표적인 예가 LLM 응답 스트리밍입니다.</p>
<p>예를 들어 AI 응답이 다음과 같이 순차적으로 도착할 수 있습니다.</p>
<pre><code>오늘은
바스크 치즈케이크
레시피를
알려드리겠습니다.</code></pre><p>이런 경우 클라이언트는 데이터를 스트림으로 계속 읽어들이게 됩니다.</p>
<p>만약 사용자가 중간에 “응답 중단” 버튼을 누르면 <code>AbortController</code>를 통해 진행 중인 스트림 읽기를 중단할 수 있습니다.</p>
<h3 id="2️⃣-응답-데이터-소비">2️⃣ 응답 데이터 소비</h3>
<p><code>fetch</code>로 받은 응답은 보통 다음과 같은 메서드를 통해 데이터로 변환하는 과정이 필요합니다.</p>
<pre><code class="language-jsx">const response = await fetch(&quot;/api/v1/recipe/1&quot;);
const data = await response.json();</code></pre>
<p>여기서 <code>response.json()</code>은 응답 데이터를 파싱하는 비동기 작업입니다. 만약 응답 데이터가 매우 크거나 처리 시간이 길다면, 이 과정 역시 <code>AbortController</code>로 중단할 수 있습니다.</p>
<h1 id="언제-사용하는가">언제 사용하는가?</h1>
<p><code>AbortController</code>는 사용자 인터랙션이 빠르게 변하는 상황에서 특히 유용합니다. 대표적으로 다음과 같은 경우에 사용됩니다.</p>
<h2 id="1-이전-api-요청을-취소해야-할-때">1. 이전 API 요청을 취소해야 할 때</h2>
<p>검색창 자동완성이나 필터 기능처럼 사용자가 빠르게 입력을 변경하는 경우, 이전 요청의 결과는 더 이상 필요하지 않을 수 있습니다.</p>
<p>예를 들어 사용자가 &quot;cheese&quot;를 검색하는 과정에서 다음과 같은 요청이 연속적으로 발생할 수 있습니다.</p>
<pre><code>입력: c
GET /api/v1/search?query=c

입력: ch
GET /api/v1/search?query=ch

입력: che
GET /api/v1/search?query=che

입력: chee
GET /api/v1/search?query=chee

입력: chees
GET /api/v1/search?query=chees

입력: cheese
GET /api/v1/search?query=cheese</code></pre><p>이때 이전 요청이 취소되지 않으면 불필요한 요청이 계속 서버로 전송됩니다. 또한 응답 순서가 뒤바뀌면서 오래 걸린 요청의 결과가 UI를 덮어쓰는 문제도 발생할 수 있습니다.</p>
<p>이런 경우 새 요청을 보내기 전에 이전 요청을 abort하여 문제를 방지할 수 있습니다.</p>
<h2 id="2-사용자가-요청을-직접-취소할-수-있도록-할-때">2. 사용자가 요청을 직접 취소할 수 있도록 할 때</h2>
<p>API 응답 시간이 긴 작업에서는 사용자가 작업을 중단할 수 있는 UX가 필요합니다.</p>
<p>응답이 오래 걸리는 작업의 경우 사용자는 결과를 기다리다가 중간에 작업을 취소하고 싶을 수 있습니다. 예를 들어 요청 시간이 예상보다 길어지거나, 더 이상 해당 작업이 필요하지 않다고 판단하는 상황이 있을 수 있습니다.</p>
<p>이때 사용자가 요청을 중단할 수 있는 방법이 없다면 불필요한 요청이 계속 서버에서 처리될 수 있습니다. 따라서 사용자에게 요청을 취소할 수 있는 인터페이스를 제공하는 것이 중요합니다.</p>
<p>특히 AI / LLM 응답은 생성 과정이 길어질 수 있기 때문에 사용자가 응답을 기다리다가 중간에 생성을 중단하고 싶어하는 상황이 자주 발생합니다.</p>
<p>예를 들어 다음과 같은 상황입니다.</p>
<ul>
<li>파일 업로드</li>
<li>대용량 데이터 분석</li>
<li>AI/LLM 응답 생성</li>
</ul>
<p>이러한 기능에서는 응답 시간이 길어질 수 있기 때문에, 사용자가 “취소” 버튼을 통해 진행 중인 작업을 중단할 수 있도록 하는 UX가 자주 사용됩니다.</p>
<h2 id="3-페이지-이동이나-컴포넌트-언마운트-시">3. 페이지 이동이나 컴포넌트 언마운트 시</h2>
<p>사용자가 페이지를 떠났는데도 이전 요청이 계속 실행되는 경우가 있습니다. 이 경우 다음과 같은 문제가 발생할 수 있습니다.</p>
<ul>
<li>불필요한 네트워크 요청</li>
<li>메모리 사용 증가</li>
<li>이미 사라진 화면을 업데이트 하려는 오류</li>
</ul>
<h3 id="추가-설명-메모리-사용-증가">추가 설명: 메모리 사용 증가</h3>
<p>API 요청이 완료되지 않은 상태에서 페이지를 떠나더라도, 해당 요청과 관련된 Promise, 콜백, 응답 데이터 처리 로직은 메모리에 남아 계속 실행될 수 있습니다.</p>
<p>예를 들어 사용자가 어떤 페이지에서 데이터를 요청한 뒤 곧바로 다른 페이지로 이동했다고 가정해 보겠습니다.</p>
<pre><code class="language-jsx">useEffect(() =&gt; {
  fetch(&quot;/api/v1/recipe/1&quot;)
    .then(res =&gt; res.json())
    .then(data =&gt; {
      setData(data);
    });
}, []);</code></pre>
<p>이때 네트워크 요청은 여전히 진행 중입니다.</p>
<p>만약 이런 요청이 반복적으로 발생하면 불필요한 비동기 작업이 계속 쌓이게 되고, 브라우저 메모리 사용량이 증가할 수 있습니다.</p>
<p>특히 다음과 같은 상황에서 문제가 더 커질 수 있습니다.</p>
<ul>
<li>사용자가 페이지를 빠르게 이동하는 경우</li>
<li>검색 입력처럼 요청이 자주 발생하는 경우</li>
<li>대용량 데이터를 처리하는 API인 경우</li>
</ul>
<p>따라서 더 이상 필요하지 않은 요청은 AbortController로 중단하여 불필요한 리소스 사용을 줄이는 것이 좋습니다.</p>
<h3 id="추가-설명-이미-사라진-화면을-업데이트-하려는-오류">추가 설명: 이미 사라진 화면을 업데이트 하려는 오류</h3>
<p>페이지를 떠났거나 컴포넌트가 사라진 뒤에도 API 요청이 완료되면, 더 이상 존재하지 않는 화면을 업데이트하려는 코드가 실행될 수 있습니다.</p>
<p>예를 들어 다음과 같은 코드가 있다고 가정해 보겠습니다.</p>
<pre><code class="language-jsx">useEffect(() =&gt; {
  fetch(&quot;/api/v1/recipe/1&quot;)
    .then(res =&gt; res.json())
    .then(data =&gt; {
      setData(data);
    });
}, []);</code></pre>
<p>만약 사용자가 API 응답이 오기 전에 다른 페이지로 이동하면, 해당 컴포넌트는 이미 언마운트된 상태가 됩니다.</p>
<p>그런데 요청이 늦게 완료되어 <code>setData(data);</code> 코드가 실행됩니다.</p>
<p>이때 React에서는 이미 사라진 컴포넌트의 상태를 업데이트 하려는 문제가 발생합니다.</p>
<h3 id="정리">정리</h3>
<p>요청을 취소하지 않으면 다음과 같은 문제가 발생할 수 있습니다.</p>
<ul>
<li>더 이상 필요 없는 비동기 작업이 계속 실행되어 리소스를 낭비할 수 있음</li>
<li>이미 사라진 컴포넌트를 업데이트하려는 코드가 실행되어 경고나 버그가 발생할 수 있음</li>
</ul>
<p>따라서 페이지 이동이나 컴포넌트 언마운트 시 진행 중인 요청을 정리하는 것이 중요합니다.</p>
<blockquote>
<p>컴포넌트 언마운트?</p>
<p>사용자가 페이지를 이동하면 해당 화면을 구성하던 요소들은 화면에서 제거됩니다.
React에서는 이러한 상태를 <code>‘컴포넌트가 언마운트 되었다’</code>고 표현합니다.</p>
</blockquote>
<p>이러한 문제를 방지하기 위해 진행 중인 비동기 요청을 중단할 수 있는 방법이 필요합니다. JavaScript에서는 AbortController를 사용해 이러한 요청을 취소할 수 있습니다.</p>
<h2 id="4-llm스트리밍-응답을-중단할-때">4. LLM/스트리밍 응답을 중단할 때</h2>
<p>최근에는 AI API 호출을 중단하기 위해서도 <code>AbortController</code>가 많이 사용됩니다.</p>
<p>LLM 응답은 다음과 같은 특징이 있습니다.</p>
<ul>
<li>응답 시간이 길다</li>
<li>스트리밍 방식으로 데이터를 받는다</li>
<li>사용자가 중간에 취소하고 싶을 수 있다</li>
</ul>
<p>이때 AbortController를 사용하면 진행 중인 AI 응답을 즉시 중단할 수 있어 사용자 경험을 개선할 수 있습니다.</p>
<h1 id="실제로-사용해보기">실제로 사용해보기</h1>
<p>앞에서 살펴본 것처럼 AbortController를 사용하면 진행 중인 API 요청을 중단할 수 있습니다.</p>
<p>이번에는 실제로 <code>AbortController</code>를 사용해 LLM API 요청을 취소하는 방법을 살펴보겠습니다.</p>
<h2 id="1-abortcontroller-생성하기">1. AbortController 생성하기</h2>
<p><code>AbortController</code> 인스턴스를 생성합니다.</p>
<pre><code class="language-jsx">const controller = new AbortController();</code></pre>
<p><code>AbortController</code> 객체는 요청을 취소하는 역할을 하며, 여기서 생성된 <code>signal</code>을 API 요청에 전달해 취소 신호를 보낼 수 있습니다.</p>
<h2 id="2-api-요청에-signal-전달하기">2. API 요청에 signal 전달하기</h2>
<p><code>fetch</code> 요청을 보낼 때 <code>signal</code>을 함께 전달합니다.</p>
<pre><code class="language-jsx">const controller = new AbortController();

fetch(&quot;/api/v1/recipes/extract&quot;, {
  method: &quot;POST&quot;,
  body: JSON.stringify({ url: youtubeUrl }),
  signal: controller.signal,
});</code></pre>
<h2 id="3-요청-중단하기">3. 요청 중단하기</h2>
<p>요청을 중단하려면 <code>AbortController</code>의 <code>abort()</code> 메서드를 호출하면 됩니다.</p>
<pre><code class="language-jsx">controller.abort();</code></pre>
<p>이 메서드가 호출되면 진행 중인 fetch 요청이 즉시 취소됩니다.</p>
<h2 id="4-요청-취소-에러-처리하기">4. 요청 취소 에러 처리하기</h2>
<p>요청이 취소되면 <code>fetch</code>는 <code>AbortError</code>를 발생시킵니다. 따라서 이를 구분해 처리하는 것이 좋습니다.</p>
<pre><code class="language-jsx">try {
  const response = await fetch(&quot;/api/v1/recipes/extract&quot;, {
    method: &quot;POST&quot;,
    body: JSON.stringify({ url: youtubeUrl }),
    signal: controller.signal,
  });

  const data = await response.json();
} catch (error) {
  if (error.name === &quot;AbortError&quot;) {
    console.log(&quot;요청이 취소되었습니다.&quot;);
  } else {
    console.error(error);
  }
}</code></pre>
<p>이렇게 AbortController를 사용하면 사용자가 원할 때 진행 중인 API 요청을 중단할 수 있습니다.</p>
<p>프론트엔드에서 <code>AbortController</code>로 요청을 취소하면 서버에서는 요청의 abort signal을 통해 클라이언트 연결이 종료된 것을 감지할 수 있으며, 이를 활용해 진행 중인 작업을 중단할 수 있습니다.</p>
<p>이를 통해 사용자가 요청을 취소했을 때 네트워크 요청뿐 아니라 서버에서 수행 중이던 크롤링이나 LLM 분석 작업도 함께 중단되어 불필요한 리소스 사용을 줄일 수 있습니다.</p>
<h1 id="알게된-점">알게된 점</h1>
<p>이번 글을 통해 <code>AbortController</code>를 사용하면 진행 중인 백엔드 API나 LLM API 요청을 클라이언트에서 중단할 수 있다는 것을 알게 되었습니다.</p>
<p>특히 사용자와의 인터랙션이 빈번하게 발생하는 브라우저 환경에서는, 더 이상 필요하지 않은 요청을 취소함으로써 불필요한 네트워크 요청을 줄일 수 있습니다.</p>
<p>LLM과 같이 응답 시간이 긴 API를 사용하는 경우, 사용자가 요청을 중간에 취소할 수 있는 기능을 제공하는 것이 사용자 경험을 개선하는 데 도움이 된다는 점도 확인할 수 있었습니다.</p>
<h1 id="참고-자료">참고 자료</h1>
<p><a href="https://developer.mozilla.org/ko/docs/Web/API/AbortController">AbortController | MDN Docs</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[제4회 2026 블레이버스 MVP 개발 해커톤 참여 후기]]></title>
            <link>https://velog.io/@soleil_lucy_75/blaybus-mvp-hackathon-review</link>
            <guid>https://velog.io/@soleil_lucy_75/blaybus-mvp-hackathon-review</guid>
            <pubDate>Sun, 08 Mar 2026 09:10:41 GMT</pubDate>
            <description><![CDATA[<h1 id="해커톤-참여-계기">해커톤 참여 계기</h1>
<p>그동안 개발 공부는 꾸준히 해왔지만, 공부한 내용을 바탕으로 실제로 무언가를 만들어보는 경험은 많지 않았습니다. 그래서 2026년에는 해커톤에 적극적으로 참여해, 그동안 배운 개발 지식을 활용해 실제 서비스를 만들어보자는 목표를 세우게 되었습니다.</p>
<p>해커톤은 짧은 시간 안에 문제를 해결해야 하는 환경이기 때문에 그 과정에서 많은 것을 배울 수 있다는 이야기를 종종 들었습니다. 그래서 참여할 수 있는 해커톤이 있는지 찾아보던 중 <a href="https://www.blaybus.com/activity/626/home">제 4회 2026 블레이버스 MVP 개발 해커톤</a>을 알게 되었습니다.</p>
<p><code>블레이버스 MVP 개발 해커톤</code>은 실제 창업팀의 아이디어를 바탕으로 MVP를 개발하는 형태의 해커톤이었습니다. 개인 혹은 팀 단위로 참가 신청이 가능했고, 개인 참가자의 경우 자율 팀빌딩 기간 동안 팀을 구성해야 MVP 개발 과정에 참여할 수 있었습니다.</p>
<p>팀 구성 조건은 다음과 같았습니다.</p>
<ul>
<li>PM 1명 이상</li>
<li>디자이너 1명 이상</li>
<li>개발자 2명 이상 (최대 10명)</li>
</ul>
<p>그동안 PM이나 디자이너와 실질적으로 협업해본 경험이 거의 없었기 때문에, 이번 기회를 통해 다양한 역할의 사람들과 협업해보고 싶다는 생각에 참가를 신청하게 되었습니다.</p>
<h1 id="진행-과정-및-결과">진행 과정 및 결과</h1>
<h2 id="팀-구성">팀 구성</h2>
<ul>
<li>PM 1명</li>
<li>디자이너 1명</li>
<li>백엔드 개발자 2명</li>
<li>프론트엔드 개발자 2명</li>
</ul>
<p>제가 속한 팀 <code>777</code>은 총 6명으로 구성되었습니다.</p>
<h2 id="프로젝트">프로젝트</h2>
<p>우리 팀은 창업팀 도사의 아이템인 <code>SIMVEX: 공학 학습용 웹 기반 3D 기계 부품 뷰어</code> MVP를 구현하게 되었습니다.</p>
<p>해커톤에서는 두 가지 아이템 중 하나를 선택할 수 있었는데, 그중 SIMVEX 서비스에는 AI 어시스턴트 기능이 포함되어 있었습니다. 학습자가 기계 부품을 공부하면서 궁금한 점이 생기면 AI에게 채팅으로 질문을 하고 도움을 받을 수 있는 기능이었습니다.</p>
<p>개인적으로 <strong>AI가 포함된 서비스를 만들어보는 경험이 더 흥미롭게 느껴졌기 때문에</strong> 팀원들을 설득했고, 결국 우리 팀은 <code>SIMVEX</code> 아이템을 선택해 MVP 개발을 진행하게 되었습니다.</p>
<h2 id="나의-역할">나의 역할</h2>
<p>저는 프론트엔드 개발자로 참여했습니다.</p>
<p>프로젝트에서는 3D 렌더링을 제외한 대부분의 프론트엔드 기능을 담당했습니다.</p>
<ul>
<li>학습 기계/장비 조회</li>
<li>부품 정보 조회</li>
<li>측면 서브 노트(메모 기능)</li>
<li>서브 AI 어시스턴트</li>
<li>랜딩 페이지</li>
</ul>
<p>3D 렌더링과 관련된 기능은 다른 프론트엔드 개발자 분이 관심이 있다고 하셔서 자연스럽게 역할이 나뉘게 되었습니다.</p>
<p>[실제 개발 화면]
<img src="https://velog.velcdn.com/images/soleil_lucy_75/post/937c660b-c1ba-4786-898d-90324710af08/image.png" alt="실제 개발 화면"></p>
<h2 id="결과">결과</h2>
<p>도사팀의 <code>SIMVEX: 공학 학습용 웹 기반 3D 기계 부품 뷰어</code> 아이템을 선택한 팀이 30팀이 넘었고, 파이널 데이에서는 그중 15팀만 발표 기회가 주어졌습니다.</p>
<p>다행히도 제가 속한 팀 <code>777</code>은 파이널 데이에 발표할 팀으로 선정되어 오프라인 행사에 참여해 우리 팀의 결과를 발표할 수 있는 기회를 얻게 되었습니다.</p>
<p>열심히 개발도 하고 발표도 준비했지만, 아쉽게도 수상까지 이어지지는 못했습니다.</p>
<h1 id="배운-점-및-아쉬운-점">배운 점 및 아쉬운 점</h1>
<p>파이널데이에서 발표 후 심사위원분들께 아래와 같은 질문을 받았습니다.</p>
<blockquote>
<p>Q. 본인 팀만의 차별점은 무엇인가요?
Q. 3D 성능 개선을 위해 어떤 노력을 했나요?
Q. 기술적으로 어려웠던 점은 무엇이었나요?</p>
</blockquote>
<p>우리 팀은 주로 “이런 기능을 구현했습니다”라는 흐름으로 발표를 준비했기 때문에 이러한 질문을 받았을 때 머리가 새하얘졌고, 명확하게 답변하지 못했습니다.</p>
<p>돌이켜보면 “MVP 기본 요구사항을 기한 내에 구현하기”라는 목표에 너무 집중했던 것 같습니다. 그 과정에서 성능 개선이나 최적화에 대해서는 충분히 고민하지 못했습니다.</p>
<p>다른 팀들도 같은 질문을 받았는데, 그들의 답변을 들으면서 많은 것을 느낄 수 있었습니다. 많은 팀들이 단순히 기능을 구현했다는 것에 그치지 않고,</p>
<ul>
<li>성능을 개선하기 위해 어떤 시도를 했는지</li>
<li>사용자의 학습 경험을 어떻게 더 좋게 만들 수 있을지</li>
<li>사용자가 불편하게 느낄 수 있는 부분은 무엇인지</li>
</ul>
<p>와 같은 <code>사용자 관점</code>과 <code>기술적 고민</code>을 함께 설명하고 있었습니다.</p>
<p>이번 경험을 통해 느낀 점은 다음과 같습니다.</p>
<p><strong>아쉬운 점</strong></p>
<ul>
<li>기본 요구사항 구현에만 지나치게 집중했던 점</li>
</ul>
<p><strong>배운 점</strong></p>
<ul>
<li>기능 구현뿐만 아니라 왜 그렇게 구현했는지 설명할 수 있어야 한다는 것</li>
<li>성능이나 최적화와 같은 기술적인 고민도 함께 필요하다는 것</li>
</ul>
<h1 id="마지막으로">마지막으로</h1>
<p>해커톤이 끝난 지 벌써 한 달이 지났습니다. 바로 후기를 써야겠다고 생각했지만 미루다 보니 이제야 정리하게 되었습니다.</p>
<p>이번 해커톤을 통해 웹에서 3D 렌더링을 구현할 때 <code>Three.js</code> 같은 라이브러리를 활용한다는 것, 그리고 3D 에셋을 화면에 렌더링하는 과정을 간접적으로나마 경험해볼 수 있었습니다.</p>
<p>또 하나 크게 느낀 점은 기능 구현만이 전부는 아니라는 것이었습니다. 짧은 해커톤이라 하더라도</p>
<ul>
<li>성능이나 최적화는 어떻게 할 수 있을지</li>
<li>사용자가 실제로 사용하기 편한 기능인지</li>
<li>왜 이 기능이 필요한지</li>
</ul>
<p>같은 질문에 대해 고민해보는 과정이 필요하다는 것을 배울 수 있었습니다.</p>
<p>이번 해커톤을 통해 해커톤에서 무엇을 어필해야 하는지도 조금은 알게 된 것 같습니다. 단순히 문제를 해결하는 것뿐만 아니라 <strong>“왜 이런 방식으로 해결했는지”</strong>까지 설명할 수 있어야 한다는 점이 중요하다고 느꼈습니다.</p>
<p>다음에 또 해커톤에 참여하게 된다면, 기능 구현뿐만 아니라 성능, 사용자 경험, 그리고 기술적 선택의 이유까지 더 깊이 고민해보고 싶습니다.</p>
<p>[우리 팀 부스 명패]
<img src="https://velog.velcdn.com/images/soleil_lucy_75/post/aaa40de5-194c-4e3d-b840-6e912df17200/image.jpeg" alt="팀 777 명패"></p>
<h1 id="참고">참고</h1>
<ul>
<li><a href="https://github.com/blaybus-777">팀 777 GitHub Repository</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[길벗 코딩 자율학습단 19기 참여 후기 - AI 에이전트]]></title>
            <link>https://velog.io/@soleil_lucy_75/review-gilbut-coding-study-ai-agent</link>
            <guid>https://velog.io/@soleil_lucy_75/review-gilbut-coding-study-ai-agent</guid>
            <pubDate>Wed, 11 Feb 2026 13:53:39 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/cc6460d7-5889-46ec-acee-b4199af486da/image.jpg" alt="밑바닥부터 배우는 AI 에이전트"></p>
<blockquote>
<p>&quot;밑바닥부터 배우는 AI 에이전트&quot; 한 문장으로 요약하기</p>
<p>랭그래프(LangGraph), 랭체인(LangChain)과 같은 프레임워크에 의존하지 않고, 파이썬만으로 5가지 워크플로 패턴을 구현하며 AI 에이전트를 만드는 방법을 배우는 책이다.</p>
</blockquote>
<h1 id="코딩-자율학습단에-참여한-이유">코딩 자율학습단에 참여한 이유</h1>
<p>작년, 유튜브에서 한 실리콘밸리 개발자의 인터뷰 영상을 보게 되었습니다. 그 영상에서 들은 한 문장이 기억에 남았습니다.</p>
<blockquote>
<p>&quot;AI 에이전트를 만드는 것부터 시작해보세요.”</p>
</blockquote>
<p>그 말을 계기로, 단순히 생성형 AI를 사용하는 수준을 넘어 ‘목표를 설정하고 스스로 작업을 수행하는 시스템’인 AI 에이전트를 직접 만들어보고 싶다는 생각이 들었습니다.</p>
<p>그러던 중 ‘길벗 코딩 자율학습단 19기’ 모집 소식을 접했고, 마침 과정에 AI 에이전트 관련 도서가 포함되어 있어 참여를 결심하게 되었습니다.</p>
<h1 id="『밑바닥부터-배우는-ai-에이전트』를-읽고">『밑바닥부터 배우는 AI 에이전트』를 읽고</h1>
<p>책에서 정의하는 AI 에이전트는 다음과 같습니다.</p>
<blockquote>
<p>AI 에이전트?
주어진 목표를 달성하기 위해 외부 환경과 상호작용하며 자율적으로 행동하는 시스템</p>
</blockquote>
<p>책에서는 AI 에이전트를 구성하는 다섯가지 워크플로 패턴에 대해 설명합니다.</p>
<h2 id="1️⃣-프롬프트-체이닝">1️⃣ 프롬프트 체이닝</h2>
<p>최종 응답을 얻기 위해 작업을 단계별로 나누어 LLM을 순차적으로 호출하는 방식</p>
<h2 id="2️⃣-라우팅">2️⃣ 라우팅</h2>
<p>사용자의 질문을 분석해 여러 처리 경로 중 하나를 선택하는 방식</p>
<h2 id="3️⃣-병렬-처리">3️⃣ 병렬 처리</h2>
<p>여러 LLM을 동시에 호출해 다양한 응답을 생성하고, 이를 종합해 최적의 결과를 도출하는 방식</p>
<h2 id="4️⃣-오케스트레이터워커">4️⃣ 오케스트레이터–워커</h2>
<p>복잡한 작업을 여러 하위 작업으로 분해한 뒤, 각각을 개별 LLM 호출로 처리하고 결과를 종합하는 방식</p>
<h2 id="5️⃣-평가최적화">5️⃣ 평가–최적화</h2>
<p>두 LLM이 상호작용하며 응답을 평가하고 개선하는 방식</p>
<p>이 다섯 가지 패턴을 학습하면서, 그동안 사용해왔던 코딩 에이전트가 ‘평가–최적화’ 패턴을 기반으로 동작하고 있었구나라는 사실을 이해하게 되었습니다.</p>
<p>또한 ‘오케스트레이터–워커’ 패턴을 활용한다면, 1인 개발자도 하나의 팀처럼 역할을 분리해 서비스를 더 효율적으로 만들어낼 수 있지 않을까 하는 생각도 들었습니다.</p>
<h1 id="코딩-자율학습단-19기를-마무리하며">코딩 자율학습단 19기를 마무리하며</h1>
<p>우연히 알게 된 코딩 자율학습단이었지만, 마침 제가 궁금해하던 기술을 다룬 책이 있어 자연스럽게 참여하게 되었습니다.</p>
<p>책 자체는 비교적 얇은 편이라 부담은 적었지만, 혼자였다면 앞부분만 읽고 멈췄을 가능성이 높았을 겁니다.</p>
<p>특히 도움이 되었던 것은 4주 학습 플래너였습니다. 매일 어느 정도 분량을 읽어야 하는지 명확하게 제시되어 있어, 끝까지 완독할 수 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/defc1b3f-7189-45f4-8b1d-d95b88787691/image.png" alt="학습플래너 스크린샷"></p>
<p>또한 학습 일지를 작성하는 미션이 있어 “해야 하는 환경”이 자연스럽게 만들어졌고, 그 덕분에 꾸준히 읽을 수 있었습니다.
<img src="https://velog.velcdn.com/images/soleil_lucy_75/post/df7bc8b3-19bf-4356-b151-7437148e3aeb/image.png" alt="학습일지 목록 스크린샷"></p>
<p>AI 에이전트가 궁금하지만 복잡한 프레임워크부터 시작하기보다는, 파이썬만으로 기본 구조를 이해하며 만들어보고 싶은 분께 이 책을 추천합니다. 또한 혼자 공부하다가 자꾸 미루게 된다면, 완독할 수 있는 환경을 만들어주는 길벗 코딩 자율학습단도 함께 추천하고 싶습니다.</p>
<h1 id="참고자료">참고자료</h1>
<p><a href="https://cafe.naver.com/gilbutitbook?iframe_url_utf8=%2FArticleRead.nhn%253Fclubid%3D30327713%2526articleid%3D22078%2526referrerAllArticles%3Dtrue">길벗 코딩 자율학습단 20기 모집 공지</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[보물찾기 후기] 찾아라! "부트캠프 백엔드 개발자편 with 스프링부트"]]></title>
            <link>https://velog.io/@soleil_lucy_75/%EB%B3%B4%EB%AC%BC%EC%B0%BE%EA%B8%B0-%ED%9B%84%EA%B8%B0-%EC%B0%BE%EC%95%84%EB%9D%BC-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%ED%8E%B8-with-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8</link>
            <guid>https://velog.io/@soleil_lucy_75/%EB%B3%B4%EB%AC%BC%EC%B0%BE%EA%B8%B0-%ED%9B%84%EA%B8%B0-%EC%B0%BE%EC%95%84%EB%9D%BC-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%ED%8E%B8-with-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8</guid>
            <pubDate>Thu, 29 Jan 2026 03:02:30 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/a36b8c95-56c6-4540-9d94-dd24713d04f4/image.jpg" alt="&quot;부트캠프 백엔드 개발자편 with 스프링부트&quot; 표지"></p>
<h1 id="보물찾기-후기">보물찾기 후기</h1>
<p>취업 준비를 하던 중, 제가 활동 중인 개발 커뮤니티에서 진행된 특강 <em>‘한입 런치박스’</em>를 통해 한 강사님을 알게 되었습니다. 이후 강사님의 유튜브를 구독하고 라이브 방송에도 정기적으로 참여한 지 벌써 1년 정도가 되었네요.</p>
<p>그러던 중 강사님께서 책을 출간하셨다는 소식을 들었습니다.</p>
<p>01월 27일(화), 출간 파티 라이브에서 책 출간과 함께 이벤트 소식까지 전해주셔서 이렇게 후기를 남기게 되었습니다. 솔직히 이런 재미있는 이벤트를 기획해 오실 줄은 몰랐습니다.</p>
<p>출간 파티 라이브 다음 날, 곧바로 교보문고로 달려가 책을 찾았습니다. 분명 백엔드 관련 도서였는데, 의외로 ‘게임 개발’ 코너에 진열되어 있어 한참을 헤맸습니다.
책 제목만 약 5분 동안 뚫어지게 바라보다가 마침내 발견했는데, 그 순간 정말 보물을 찾은 기분이 들었습니다.</p>
<p>지금은 프론트엔드 위주로 공부하고 있어서, 나중에 백엔드 공부가 필요해질 때 이 책으로 도전해보려 합니다.</p>
<h1 id="보물찾기-인증">보물찾기 인증</h1>
<p>집 근처 교보문고에서
<img src="https://velog.velcdn.com/images/soleil_lucy_75/post/9bcbec7a-16fa-4e56-bbf7-944ebc3398b2/image.jpeg" alt="보물찾기 인증 사진"></p>
<p>강남 교보문고에서!
<img src="https://velog.velcdn.com/images/soleil_lucy_75/post/e79da22d-e48d-4c75-9b9d-0bf6f69cbbee/image.jpeg" alt="강남 교보문고에서 본 부트캠프 백엔드 개발자편 with 스프링부트 책"></p>
<h1 id="참고자료">참고자료</h1>
<p><a href="https://www.hanbit.co.kr/store/books/look.php?p_code=B3334990758">부트캠프 백엔드 개발자편 with 스프링부트 책 보러가기</a>
<a href="https://www.youtube.com/channel/UCxdunbb1wIvfufdo1NuLEvg">강사님 유튜브 보러가기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[1달 반이 지나서야 쓰는, TEO Conf 2025 스태프 회고]]></title>
            <link>https://velog.io/@soleil_lucy_75/1%EB%8B%AC-%EB%B0%98%EC%9D%B4-%EC%A7%80%EB%82%98%EC%84%9C%EC%95%BC-%EC%93%B0%EB%8A%94-TEO-Conf-2025-%EC%8A%A4%ED%83%9C%ED%94%84-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@soleil_lucy_75/1%EB%8B%AC-%EB%B0%98%EC%9D%B4-%EC%A7%80%EB%82%98%EC%84%9C%EC%95%BC-%EC%93%B0%EB%8A%94-TEO-Conf-2025-%EC%8A%A4%ED%83%9C%ED%94%84-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 16 Jan 2026 08:39:03 GMT</pubDate>
            <description><![CDATA[<p>벌써 TEO Conf 2025가 끝난지 1달 반이 지났습니다. 이제서야 컨퍼런스 스태프로 일했던 경험을 글로 남겨봅니다. </p>
<p>조금 늦은 후기이지만, TEO Conf 2025 스태프로 참가한 후기는 어떤지 읽어주시면 감사하겠습니다!</p>
<h1 id="teo-conf-2025-스태프가-되었다">TEO Conf 2025 스태프가 되었다</h1>
<p>TEO Conf 2025 스태프로 참여하게 된 계기는 정말 우연이었습니다. 테오가 운영하는 디스코드에서 컨퍼런스 스태프를 모집한다는 글을 보게 되었고, 아직 마감 전이라는 이야기를 듣고 테오에게 DM으로 지원 의사를 전했습니다. 운 좋게도 합류할 수 있었습니다.</p>
<p>8월에 킥오프 회의 참여 메일을 받고 나서야 “TEO Conf 2025 스태프구나”라는 실감이 되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/9cb9da4a-4bcf-42f0-b509-15d8e537e204/image.png" alt="테오에게 받은 킥오프 회의 참여 메일"></p>
<h2 id="스태프를-하고-싶었던-이유는">스태프를 하고 싶었던 이유는?</h2>
<p>TEO Conf는 지원한다고 해서 누구나 참가가 확정되는 컨퍼런스는 아니라고 들었습니다. 지원해서 떨어질 바에야, 차라리 만드는 쪽으로 참여해보자는 생각이 들었습니다. </p>
<p>이전에 원티드 하이파이브, 인프콘 등에서 스태프로 활동한 경험도 있었기 때문에, 이번에도 잘 해낼 수 있겠다는 자신감이 있었습니다.</p>
<h1 id="it-maker-팀에-들어가-컨퍼런스-랜딩-페이지를-만들게-되었다">IT MAKER 팀에 들어가 컨퍼런스 랜딩 페이지를 만들게 되었다</h1>
<h2 id="it-maker-팀이라는-이름은-어떻게-지어졌나요">IT MAKER 팀이라는 이름은 어떻게 지어졌나요?</h2>
<p>IT MAKER 팀이라는 이름은 사실 굉장히 즉흥적으로 만들어졌습니다.</p>
<p>스태프들이 각자 컨퍼런스에서 어떤 일을 하고 싶은지 얘기를 나누던 자리에서, 저는 <code>“컨퍼런스 랜딩 페이지를 개발하는 일을 하고 싶다”</code>고 말했습니다. 당시 팀 이름도 얘기해볼 수 있었는데, 처음에는 <code>“IT 개발 지원팀”</code>이라는 이름을 제안했다가 기획 및 운영을 담당하는 <code>“스파크팀”</code>, 굿즈를 담당하는 <code>“굿테리오팀”</code>, 컨퍼런스 연사자 소통 및 촬영을 맡은 <code>“온에어팀”</code> 등 멋있는 이름을 듣고 <code>“IT MAKER팀”</code>을 떠올렸습니다. 컨퍼런스 랜딩 페이지 제작에 관심 있는 스태프들이 모이고, <code>“IT MAKER팀”</code>이라는 이름을 제안했고, 다행히 그 이름이 그대로 채택되었습니다.</p>
<h2 id="컨퍼런스-랜딩-페이지-개발을-맡고-싶었던-이유">컨퍼런스 랜딩 페이지 개발을 맡고 싶었던 이유</h2>
<p>컨퍼런스 기획, 굿즈 제작, 연사자와의 커뮤니케이션, 촬영 등 다양한 역할이 있었지만, 처음부터 랜딩 페이지 개발 업무를 하고 싶다고 생각했습니다.</p>
<p>컨퍼런스 공식 홈페이지는 어떻게 만들어질까, 어떤 콘텐츠를 어떻게 보여줘야 참가자들이 컨퍼런스를 더 잘 이해하고 기대할 수 있을지를 고민하는 과정이 재밌을 것 같았습니다.</p>
<p>이전에 다른 기술 컨퍼런스에 참가할 때마다 홈페이지를 보며 일정을 계획하는 시간이 즐거웠기 때문에, 저도 참가자들에게 그런 경험을 제공하는 데 기여하고 싶었습니다.</p>
<h2 id="it-maker-팀에서-했던-일들">IT MAKER 팀에서 했던 일들</h2>
<p>IT MAKER 팀에서는 랜딩 페이지에 들어갈 콘텐츠를 스파크팀과 함께 기획하고, 우선순위에 따라 개발과 배포를 반복했습니다.</p>
<ul>
<li>1차 배포: 커밍순 페이지</li>
<li>2차 배포: TEO Conf 2025 신청 폼 연결, 세션 소개, FAQ</li>
<li>3차 배포: 전체 타임테이블, 장소 안내</li>
<li>4차 배포: 서브 후원사 목록 추가</li>
<li>컨퍼런스 종료 후: 발표 자료 다운로드 기능 추가</li>
</ul>
<h2 id="가장-기억에-남는-일">가장 기억에 남는 일</h2>
<p>첫 번째는 <code>TEO Conf 2025 홈페이지 첫 배포 과정</code>이었습니다.</p>
<p>기존 랜딩 페이지는 Vercel로 배포되어 있었는데, 계정 정보를 받아서 관리하는 것보다는 GitHub 하나로 관리하는게 편할 거 같았습니다. 이전 기수 운영진께 요청해 Vercel 배포를 끊고 GitHub Actions로 전환했습니다.</p>
<p>문제는 첫 배포에서 CSS, 이미지 등 정적 파일을 전혀 불러오지 못하는 오류가 발생했습니다. 처음에는 경로 문제를 의심했지만, 원인은 <code>Jekyll</code>이 <code>_(언더바)</code>로 시작하는 파일과 폴더를 자동으로 빌드 대상에서 제외하는 설정 때문이었습니다.</p>
<p><code>_next</code>, <code>_app</code>, <code>_data</code> 같은 폴더가 무시되면서 발생한 문제였고, <code>.nojekyll</code> 파일을 추가해 해결할 수 있었습니다. 이 문제로 새벽까지 고민하다가 늦게 잠들어 다음날 알바에 지각해서 기억에 납습니다. 결국 팀원 슈가가 원인을 찾아 해결해 주었습니다.</p>
<p>두 번째로 기억에 남는 일은 <code>세션 목록 UI 변경</code>에 관한 일이었습니다.</p>
<p>처음에는 이전 기수에서 사용했던 세션 UI를 그대로 가져와 구현했습니다. 하지만 배포를 앞두고 테오가 디자인 변경에 대한 의견을 주었습니다. SNS에 올라가 있는 컨퍼런스 세션 홍보용 카드 뉴스의 디자인을 활용하면 좋겠다는 제안이었습니다.</p>
<p>일정이 촉박한 상황이었지만, 커밍순 페이지를 디자인해 준 실버투스에게 급하게 세션 목록 디자인을 부탁했습니다. 다행히도 계획했던 2차 배포일 전에 UI를 수정해 예정대로 배포할 수 있었습니다.</p>
<p>다만 이 과정에서 “디자인 관련해서 IT MAKER 팀 외 다른 스태프분들에게도 미리 검토를 요청할 걸”, “결과적으로 일을 두 번 하게 된 건 아닐까?”라는 아쉬움이 남았습니다.</p>
<p>그래도 컨퍼런스 홈페이지를 방문한 사용자들에게 더 예쁜 디자인을 보여줄 수 있었다는 점에서는 결과적으로 만족스러웠습니다.</p>
<h1 id="컨퍼런스-당일-트랙-c에서-스태프로-일하게-되었다">컨퍼런스 당일, 트랙 C에서 스태프로 일하게 되었다</h1>
<h2 id="컨퍼런스-당일-맡았던-역할">컨퍼런스 당일 맡았던 역할?</h2>
<p>컨퍼런스 당일(토요일)에는 트랙 C에서 ‘분위기 메이커’ 역할을 맡았습니다.</p>
<p>낯선 사람과 1대1 대화는 비교적 잘하지만, 다수가 모인 자리에서는 말을 잘 못하는 편이라 조금 걱정이 되기도 했습니다.</p>
<p>다행히 트랙 C 참가자분들이 각자 팀원들과 활발히 대화를 나누며 분위기를 잘 만들어 주셨고, 저는 자연스럽게 타임 키퍼와 조명 담당을 맡은 제이를 도와 다양한 업무를 함께 하게 되었습니다.</p>
<ul>
<li>발표 시작 시 조명 조절</li>
<li>트랙 내부 온도 조절</li>
<li>연사자의 시간 초과 방지를 위한 타임 안내</li>
</ul>
<p>일요일에는 개인 일정이 있어 현장 운영을 돕지 못해 조금 아쉬웠습니다.</p>
<h2 id="현장에서-당황했던-순간">현장에서 당황했던 순간</h2>
<p>“테오의 고민 상담소”를 원래는 동시 송출로 진행하려 했지만, 그러지 못해서 트랙 A, B, C를 테오가 직접 이동하며 진행하기로 했습니다.</p>
<p>트랙 C에서는 선물 교환식과 럭키 드로우 이후 고민 상담소를 진행하게 되었습니다. 그러나 다른 트랙에서 일정이 지연되면서 준비한 프로그램을 모두 진행하고도 테오가 오지 않는 상황이 발생했습니다.</p>
<p>어떻게 시간을 끌어야 할지 고민을 했는데, 트랙 C의 MC를 맡아주신 루키가 자연스럽게 진행을 이어가며 분위기를 잘 살려주었습니다.</p>
<p>갓루키… 진심으로 감사했습니다!</p>
<h1 id="끝으로-다음에도-기회가-있다면-한-번-더-스태프로-참여하고-싶다">끝으로, 다음에도 기회가 있다면 한 번 더 스태프로 참여하고 싶다</h1>
<p>행사가 끝난 후 든 생각은 “내가 해보고 싶었던 일을 이번에도 잘 마무리했다” 였습니다.</p>
<p>낯을 가리고, 새로운 사람들과 일하면서 적응하는 데 시간이 조금 걸리긴 했지만 그럼에도 불구하고 “하길 잘했다”는 생각이 들었습니다.</p>
<p>다른 개발자들과 교류할 수 있었고, 컨퍼런스가 어떤 과정을 거쳐 만들어지는지도 직접 경험할 수 있었습니다. 또 저에게는 연예인 같은 존재인 시니어 개발자 테오와 함께 일하며 일하는 방식에 대해서도 많은 것을 배울 수 있었습니다.</p>
<p>컨퍼런스 기획을 시작할 때, “내가 참가자라면?”이라는 관점에서 생각하는 방법, 참가 신청 폼에서 객관식 질문은 앞에, 생각이 필요한 주관식 질문은 뒤에 배치하기, FigJam을 활용한 회의 방식까지 여러 부분에서 배운 점이 많았습니다.</p>
<p>이번 스태프 경험을 통해 저는 역시 “무언가를 만들어가는 일을 좋아하는 사람”이라는 것을 다시 한 번 깨닫게 되었습니다. 개발이든, 컨퍼런스든 기획부터 결과까지 함께 만들어가는 과정이 정말 재미있었습니다.</p>
<p>다음에도 스태프로 참여할 기회가 주어진다면 기꺼이 한 번 더 참여하고 싶습니다.</p>
<h2 id="teo-conf-2025-스태프-참여-후기--한줄로-표현하자면">TEO Conf 2025 스태프 참여 후기,  한줄로 표현하자면?</h2>
<blockquote>
<p>“스태프로 참여하며 일하는 방식을 배우고, 새로운 사람들을 만날 수 있었던 배움이 가득한 경험이었습니다”</p>
</blockquote>
<h1 id="참고">참고</h1>
<p><a href="https://www.teoconf.com/">2025 TEO Conf 공식 홈페이지</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[12월] 소프트웨어 아키텍처 The Basics(2판)]]></title>
            <link>https://velog.io/@soleil_lucy_75/12%EC%9B%94-%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-The-Basics2%ED%8C%90</link>
            <guid>https://velog.io/@soleil_lucy_75/12%EC%9B%94-%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-The-Basics2%ED%8C%90</guid>
            <pubDate>Sun, 28 Dec 2025 14:03:02 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>&quot;한빛미디어 서평단 &lt;나는리뷰어다&gt; 활동을 위해서 책을 협찬 받아 작성된 서평입니다.&quot;</strong></p>
</blockquote>
<h1 id="책-한눈에-보기">책 한눈에 보기</h1>
<p><img src="https://cdn-prod.hanbit.co.kr/books/6a07fbac-3b16-4d77-869b-c72df22804e7.jpg" alt="소프트웨어 아키텍처 The Basics(2판) 책 표지"></p>
<ul>
<li>책 제목: <strong>소프트웨어 아키텍처 The Basics(2판)</strong></li>
<li>저자: 마크 리처즈 , 닐 포드</li>
<li>번역: 류광 , 307번역랩</li>
<li>출간: 2025-12-01</li>
</ul>
<h1 id="책을-읽고나서">책을 읽고나서</h1>
<p>아직 아키텍처를 짤 실력은 안 되지만, 하루가 다르게 발전하는 생성형 AI를 보며 자연스레 위기감을 느꼈습니다. &#39;AI가 못 하는 게 과연 뭘까?&#39;를 고민하던 중, 선배 개발자들의 &quot;AI는 아직 설계(Design) 영역까지는 침범하지 못했다&quot;는 말이 떠올랐습니다. 그래서 선택하게 된 책이 바로 『소프트웨어 아키텍처 The Basics(2판)』입니다. 당장 아키텍트가 될 순 없더라도, 지금부터 조금씩 배우며 미래를 대비하고 싶었습니다.</p>
<p>책을 읽으며 가장 기억에 남는 것은 1장에 나오는 &#39;소프트웨어 아키텍처의 법칙&#39;이었습니다.</p>
<ul>
<li>&quot;소프트웨어 아키텍처의 모든 것은 트레이드오프이다.&quot;</li>
<li>&quot;어떻게(방법)보다 왜(이유)가 더 중요하다.&quot;</li>
<li>&quot;대부분의 아키텍처적 결정은 양자택일이 아니라 양극단 사이의 스펙트럼에 있는 한 지점이다.&quot;</li>
</ul>
<p>이 문장들을 읽는 순간, 제가 프론트엔드 개발을 공부하며 겪었던 수많은 시행착오가 떠올랐습니다. React 프로젝트를 하며 기술 선택의 기로에 놓일 때마다 &quot;트레이드오프를 따져라&quot;, &quot;왜 그 기술을 썼는지 설명해라&quot;, &quot;상황에 맞는 적합성을 따져라&quot;라는 피드백을 수없이 들었기 때문입니다. 그동안 공부하면서 파편적으로 들었던 조언들이 이 책을 통해 하나의 거대한 원칙으로 정리되는 느낌을 받았습니다.</p>
<p>또한, 흥미로웠던 점은 <strong>개발자와 아키텍트의 차이</strong>였습니다. 개발자는 기술적 깊이(Depth)가 중요해서 흔히 말하는 &#39;Deep Dive&#39;가 필요하지만, 아키텍트는 기술적 너비(Breadth)가 훨씬 중요하다는 것입니다. 개발자들의 블로그 포스트를 보면 ‘Deep Dive’한 콘텐츠들이 많은 이유를 알게 되었습니다.</p>
<p>책은 아키텍처 스타일부터 소프트 스킬까지 방대한 내용을 다룹니다. 주니어인 저에게는 이해하기 어려운 부분도 많았지만, 조급해하지 않고 천천히 내 것으로 만들려 합니다. 이 책이 AI 시대에 저를 지켜줄 단단한 방패가 되어주길 기대해 봅니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] Promise 객체: ECMAScript 명세서를 통해 이해하기]]></title>
            <link>https://velog.io/@soleil_lucy_75/JS-Promise-%EA%B0%9D%EC%B2%B4-ECMAScript-%EB%AA%85%EC%84%B8%EC%84%9C%EB%A5%BC-%ED%86%B5%ED%95%B4-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@soleil_lucy_75/JS-Promise-%EA%B0%9D%EC%B2%B4-ECMAScript-%EB%AA%85%EC%84%B8%EC%84%9C%EB%A5%BC-%ED%86%B5%ED%95%B4-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 27 Dec 2025 00:30:07 GMT</pubDate>
            <description><![CDATA[<h1 id="글을-정리하게-된-계기">글을 정리하게 된 계기</h1>
<p>강의를 통해 <code>Promise</code>객체를 사용하여 자바스크립트의 비동기 처리를 할 수 있다는 것을 배웠습니다. <code>Promise</code>객체가 &quot;비동기 처리를 도와준다&quot;, &quot;콜백 지옥을 해결해 준다&quot;는 이론은 이해했지만, 막상 <code>Promise</code>로 짜인 복잡한 코드를 마주하면 겁부터 나는 제 자신을 발견하곤 했습니다.</p>
<p>돌이켜보면 동작 원리를 제대로 이해하지 못한 채 사용하다 발생한 버그를 스스로 해결하지 못했던 기억 때문이었습니다. 그래서 이번 기회에 Promise를 제대로 정리하며, 그 막연한 두려움을 극복해보고자 합니다.</p>
<h1 id="정의">정의</h1>
<p>먼저 자바스크립트의 설계도인 ECMAScript 명세서(ECMA-262)에서는 Promise를 어떻게 정의하고 있는지 살펴보겠습니다.</p>
<blockquote>
<p><strong>27.2 Promise Objects</strong></p>
</blockquote>
<p>A Promise is an object that is used as a placeholder for the eventual results of a deferred (and possibly asynchronous) computation.</p>
<blockquote>
</blockquote>
<p><strong>[번역] 27.2 Promise Objects</strong></p>
<blockquote>
</blockquote>
<p>Promise는 지연된(그리고 아마도 비동기적인) 계산의 최종 결과를 담기 위한 자리 표시자(placeholder) 역할을 하는 객체입니다.</p>
<blockquote>
</blockquote>
<p>말이 어렵게 되어 있어 이해하기가 쉽지 않습니다. 쉽게 해석하자면 Promise는 &quot;아직 값이 도착하지 않았지만, 나중에 결과(성공 또는 실패)가 오면 채워 넣을 빈 그릇&quot;이라고 이해할 수 있습니다.</p>
<p>우리가 자바스크립트 엔진에게 이렇게 부탁하는 것과 같습니다.</p>
<blockquote>
<p>“이 작업은 시간이 좀 걸리니까, 결과가 나오면 이 객체(Promise)에 담아줘”</p>
</blockquote>
<h1 id="ecmascript-명세서에서-살펴-본-promise">ECMAScript 명세서에서 살펴 본 Promise</h1>
<p>명세서의 <strong>27.2 Promise Objects</strong> 부분을 보면 우리가 평소에 무심코 사용하던 Promise의 내부 동작이 상세히 기술되어 있습니다.</p>
<h2 id="promise의-3가지-상태-states"><strong>Promise의 3가지 상태 (States)</strong></h2>
<p>Promise 객체는 생성된 순간부터 소멸할 때까지 반드시 다음 세 가지 상태 중 하나를 가집니다.</p>
<ul>
<li><strong>pending (대기)</strong>: 아직 이행되거나 거부되지 않은 초기 상태입니다.</li>
<li><strong>fulfilled (이행)</strong>: 비동기 연산이 성공적으로 완료된 상태입니다.</li>
<li><strong>rejected (거부)</strong>: 비동기 연산이 실패한 상태입니다.</li>
</ul>
<p>이 중 이행(fulfilled) 또는 거부(rejected)된 상태를 합쳐서 <strong>결정된(settled)</strong> 상태라고 부릅니다.</p>
<h2 id="생성과-결정-constructor--resolving"><strong>생성과 결정 (Constructor &amp; Resolving)</strong></h2>
<p>우리는 <code>new Promise(executor)</code>를 통해 Promise를 만듭니다. 이때 전달하는 <code>executor</code> 함수는 엔진에 의해 즉시 호출되며, <strong>resolve와 reject라는 두 가지 함수를 인자로 받습니다.</strong></p>
<pre><code class="language-tsx">new Promise((resolve, reject) =&gt; {
    // 비동기 작업 수행...
    const isSuccess = true;

    if (isSuccess) {
        // 2. resolve 호출 -&gt; [[PromiseState]]가 &#39;fulfilled&#39;로 변경
        resolve(&quot;성공 결과 값&quot;);
    } else {
        // 3. reject 호출 -&gt; [[PromiseState]]가 &#39;rejected&#39;로 변경
        reject(&quot;실패 사유&quot;);
    }
});</code></pre>
<ul>
<li><code>resolve(value)</code>를 호출하면 Promise의 <code>[[PromiseState]]</code>는 <code>fulfilled</code>가 되고 결과값이 저장됩니다.</li>
<li><code>reject(reason)</code>를 호출하면 상태는 <code>rejected</code>가 되며 실패 이유가 저장됩니다.</li>
</ul>
<h2 id="내부-동작-reaction--job">내부 동작 (Reaction &amp; Job)</h2>
<p>Promise가 비동기 작업을 처리하는 객체라는 점은 명세서의 <strong>Reaction(반응)</strong>과 <strong>Job(작업)</strong> 시스템을 설명한 부분에서 알 수 있습니다.</p>
<h3 id="reaction-등록-예약">Reaction 등록 (예약)</h3>
<p>우리가 <code>.then()</code>이나 <code>.catch()</code>를 호출하면, 자바스크립트 엔진은 당장 코드를 실행하지 않습니다. 대신 <code>PromiseReaction Record</code>라는 기록을 만들어 내부 리스트(슬롯)에 저장해 둡니다. 일종의 &quot;대기표 발권&quot;입니다.</p>
<h3 id="job-예약-큐-등록">Job 예약 (큐 등록)</h3>
<p>비동기 작업이 끝나고 <code>resolve()</code>가 호출되면, 엔진은 저장해 두었던 <code>Reaction</code>들을 꺼냅니다. 그리고 이를 <code>NewPromiseReactionJob</code>이라는 작업 단위로 변환하여 <code>마이크로태스크 큐(Microtask Queue)</code>에 집어 넣습니다.</p>
<h3 id="실행-call-stack-비우기">실행 (Call Stack 비우기)</h3>
<p>큐에 들어간 Job들은 <strong>현재 실행 중인 코드(Call Stack)가 모두 끝난 뒤에야</strong> 비로소 실행됩니다. 이 명세에 따라, <strong><code>then()</code>으로 등록된 후속 작업</strong>은 현재 실행 중인 코드가 모두 종료된 후에야 비동기적으로 처리됩니다.</p>
<h1 id="이해를-돕기-위한-비유-배달-앱-주문">이해를 돕기 위한 비유: 배달 앱 주문</h1>
<p>우리가 흔히 사용하는 <code>배달 앱</code>을 예로 들어보겠습니다.</p>
<p><strong>상황:</strong> 당신은 짬뽕이 너무 먹고 싶어서 배달 앱으로 주문을 넣었습니다.</p>
<ol>
<li><strong>주문 완료 (<code>new Promise</code> &amp; <code>Pending</code>)</strong><ul>
<li>주문 버튼을 누르는 순간, 앱은 <strong>&quot;주문 접수 대기 중&quot;</strong> 또는 <strong>&quot;조리 중&quot;</strong> 상태가 됩니다.</li>
<li>아직 짬뽕(결과 값)은 내 손에 없지만, 앱 화면(Promise 객체)을 통해 주문이 진행되고 있다는 것을 알 수 있습니다. 이것이 바로 <strong>대기(Pending)</strong> 상태입니다.</li>
</ul>
</li>
<li><strong>배달 도착 (<code>Fulfilled</code> / <code>Resolve</code>)</strong><ul>
<li>조리가 끝나고 라이더가 도착했습니다. &quot;배달이 완료되었습니다&quot;라는 알림과 함께 문 앞에 짬뽕이 놓입니다.</li>
<li>이것이 <strong>이행(Fulfilled)</strong> 상태입니다. 이제 짬뽕(결과 값, Value)을 맛있게 먹으면 됩니다.</li>
</ul>
</li>
<li><strong>주문 취소 (<code>Rejected</code> / <code>Reject</code>)</strong><ul>
<li>갑자기 앱에서 알림이 뜹니다. *&quot;죄송합니다. 재료 소진으로 주문을 취소합니다.&quot;*</li>
<li>기다렸던 짬뽕은 오지 않았고, 대신 <code>취소 사유(에러 메시지)</code>를 받았습니다. 이것이 <strong>거부(Rejected)</strong> 상태입니다. 우리는 이 사유를 보고 다른 가게를 찾거나 포기해야 합니다.</li>
</ul>
</li>
</ol>
<h1 id="실제-활용-사례-github-api-데이터-통신">실제 활용 사례: GitHub API 데이터 통신</h1>
<p>Promise 객체를 활용해 GitHub 사용자 정보를 가져오는 예제입니다. 실무에서는 내장 함수인 <code>fetch</code>를 사용하면 훨씬 간단하지만, 이번에는 <strong>Promise의 내부 동작 원리를 확실히 이해하기 위해 직접 구현해봤습니다. 코드는 Gemini에게 도움을 받아 작성했습니다.</strong></p>
<h2 id="코드">코드</h2>
<p><a href="https://playcode.io/promise-example--04a34053-2323-5450-9b71-b91b87ec9cab">코드 보러가기</a></p>
<pre><code class="language-jsx">function getData(url) {
  return new Promise((resolve, reject) =&gt; {
    const xhr = new XMLHttpRequest();
    xhr.open(&quot;GET&quot;, url);

    xhr.onload = () =&gt; {
      if (xhr.status === 200) {
        resolve(JSON.parse(xhr.response));
      } else {
        reject(new Error(`요청 실패: ${xhr.status}`));
      }
    };

    xhr.onerror = () =&gt; {
      reject(new Error(&quot;네트워크 오류 발생&quot;));
    };

    xhr.send();
  });
}

getData(&quot;https://api.github.com/users/facebook&quot;)
  .then((user) =&gt; console.log(`성공: ${user.name}`))
  .catch((err) =&gt; console.error(`실패: ${err.message}`));</code></pre>
<h3 id="콘솔-화면">콘솔 화면</h3>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/6fd0f0de-81fb-46ca-8e63-b77bfdf3142f/image.png" alt="GitHub API 호출 후 화면"></p>
<h3 id="코드-해석">코드 해석</h3>
<ul>
<li><strong>Executor의 즉시 실행:</strong> <code>getData</code> 함수가 호출되는 순간, <code>new Promise</code>의 Executor 함수가 즉시 실행됩니다. 그 안의 <code>xhr.send()</code>가 실행되어 요청이 서버로 날아가고, 이때 Promise 객체는 <strong>Pending(대기)</strong> 상태가 됩니다.</li>
<li><strong>Resolve (성공 처리):</strong> 서버로부터 정상적인 응답(<strong>HTTP 상태 코드 200</strong>)이 오면 <code>resolve(data)</code>를 호출합니다. 이때 Promise는 <strong>Fulfilled(이행)</strong> 상태가 되고, 우리는 <code>.then()</code>을 통해 그 데이터를 받아볼 수 있습니다.</li>
<li><strong>Reject (실패 처리):</strong> 서버가 에러를 반환하거나(<strong>HTTP 상태 코드 404 등</strong>), 네트워크 연결이 끊기는 등의 문제(<code>onerror</code>)가 발생하면 <code>reject(Error)</code>를 호출합니다. 이때 Promise는 <strong>Rejected(거부)</strong> 상태가 되고, <code>.catch()</code>가 실행되어 에러를 처리하게 됩니다.</li>
</ul>
<h1 id="회고">회고</h1>
<p>이번 포스팅을 정리하며 Promise는 비동기 처리를 위한 객체임을 이해하게 되었습니다. &quot;Job이 등록되고 호출 스택이 모두 비워진 뒤, 마이크로태스크 큐를 통해 실행된다&quot;는 동작 원리를 알게 되었습니다. 이제 Promise로 짜인 복잡한 코드를 마주해도 더 이상 겁먹지 않고 그 내부 흐름을 명확히 읽어낼 수 있을 것 같습니다.</p>
<p>이번 학습 과정에서 <strong>Notebook LM</strong>의 도움을 받았습니다. ECMAScript 명세서를 소스로 등록해 필요한 내용만 빠르게 찾아냄으로써 학습 효율을 높일 수 있었습니다. 앞으로도 명세서를 분석할 때 종종 활용하게 될 것 같습니다.</p>
<h1 id="참고-자료">참고 자료</h1>
<ul>
<li><a href="https://tc39.es/ecma262/">ECMAScript® 2026 Language Specification</a></li>
<li><a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise | MDN Docs</a></li>
<li><a href="https://youtu.be/Xs1EMmBLpn4?si=NtViM_Y4Jb5hbSh8">JavaScript 시각화 - 약속 실행</a></li>
<li><a href="https://www.jsv9000.app/">JavaScript 동작을 확인해 볼 수 있는 사이트 | jsv9000.app</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] 이중 부정 연산자(!!) 활용법: undefined 속성과 빈 문자열 한 번에 검증하기]]></title>
            <link>https://velog.io/@soleil_lucy_75/JS-%EC%9D%B4%EC%A4%91-%EB%B6%80%EC%A0%95-%EC%97%B0%EC%82%B0%EC%9E%90-%ED%99%9C%EC%9A%A9%EB%B2%95-undefined-%EC%86%8D%EC%84%B1%EA%B3%BC-%EB%B9%88-%EB%AC%B8%EC%9E%90%EC%97%B4-%ED%95%9C-%EB%B2%88%EC%97%90-%EA%B2%80%EC%A6%9D%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@soleil_lucy_75/JS-%EC%9D%B4%EC%A4%91-%EB%B6%80%EC%A0%95-%EC%97%B0%EC%82%B0%EC%9E%90-%ED%99%9C%EC%9A%A9%EB%B2%95-undefined-%EC%86%8D%EC%84%B1%EA%B3%BC-%EB%B9%88-%EB%AC%B8%EC%9E%90%EC%97%B4-%ED%95%9C-%EB%B2%88%EC%97%90-%EA%B2%80%EC%A6%9D%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 21 Dec 2025 10:52:55 GMT</pubDate>
            <description><![CDATA[<h1 id="문제-데이터가-있거나-없거나-조건부-렌더링-어떻게-처리할까">문제: 데이터가 있거나 없거나! 조건부 렌더링, 어떻게 처리할까?</h1>
<p>TEO Conf 2025 행사를 성공적으로 마치고, 연사자들의 발표 자료를 홈페이지에 게시하는 후속 작업을 맡게 되었습니다.</p>
<p>구현 목표는 홈페이지의 &#39;세션 카드&#39; 내에서 발표자료 다운로드 기능을 제공하는 것이었는데, 기획 요구사항은 다음과 같았습니다.</p>
<ul>
<li><strong>발표자료 공개 동의 (자료 있음)</strong>: [다운로드] 버튼 제공</li>
<li><strong>발표자료 비동의/미제공 (자료 없음)</strong>: &#39;자료 미제공&#39; 라벨 표시</li>
</ul>
<p>이 요구사항을 기술적으로 구현하기 위해 기존 <code>Speaker</code> 타입에 <code>resourceUrl</code> 속성을 옵셔널(<code>?</code>)로 추가했습니다.</p>
<pre><code class="language-tsx">export interface Speaker {
  title: string
  desc: string
  name: string
  // ...
  resourceUrl?: string
}</code></pre>
<p>데이터가 있을 수도, 없을 수도 있는 이 상황에서 <strong>&quot;어떤 조건식을 써야 가장 효율적으로 렌더링을 분기할 수 있을까?&quot;</strong> 고민이 시작되었고, 이 과정에서 AI와 함께 찾은 해결책을 정리해 봅니다.</p>
<h1 id="목표-자료가-있으면-다운로드-버튼-없으면-자료-미제공-라벨-보여주기">목표: 자료가 있으면 &#39;다운로드 버튼&#39;, 없으면 &#39;자료 미제공 라벨&#39; 보여주기</h1>
<p>이번 구현의 목표는 <code>resourceUrl</code> 속성의 존재 여부에 따라 사용자에게 다른 UI를 보여주는 것입니다.</p>
<p>Gemini의 도움을 받아 기획한 UI 시안은 다음과 같습니다. 발표자료 공개 동의 여부에 따라 <strong>버튼</strong>과 <strong>라벨</strong>이 구분되어야 합니다.</p>
<h2 id="case-1-발표자료-공개-동의-자료-있음">CASE 1. 발표자료 공개 동의 (자료 있음)</h2>
<p><code>resourceUrl</code>이 존재할 경우, <strong>[발표자료 다운로드]</strong> 버튼이 활성화됩니다.</p>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/1389b8a6-7049-4f97-b556-cabaad96f8a7/image.png" alt=""></p>
<h2 id="case-2-발표자료-미동의-자료-없음">CASE 2. 발표자료 미동의 (자료 없음)</h2>
<p><code>resourceUrl</code>이 없거나 비어있을 경우, 사용자가 혼란스럽지 않도록 <strong>&#39;자료 미제공&#39;</strong> 라벨을 표시합니다.</p>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/1c2bb96b-01fb-4395-a868-7aa7f052d356/image.png" alt=""></p>
<h1 id="고민-resourceurl-여부-조건">고민: resourceUrl 여부 조건</h1>
<p><code>Speaker</code> 객체의 <code>resourceUrl</code> 속성 유무를 확인하여 조건부 렌더링을 구현해야 했습니다. 이 조건을 작성하는 과정에서 두 가지 방식을 두고 고민했습니다.</p>
<h2 id="1-in-연산자와-비동등-연산자-사용">1. <code>in</code> 연산자와 비동등 연산자(!==) 사용</h2>
<p>최근 강의를 통해 JavaScript 기본기를 복습하고 있었기에, 가장 먼저 <strong><code>in</code> 연산자</strong>가 떠올랐습니다.
먼저 객체 안에 속성이 존재하는지 확인하고, 혹시 존재하더라도 값이 비어있을 수 있으니 비동등 연산자(<code>!==</code>)로 한 번 더 검증하는 것이 안전하다고 생각했습니다.</p>
<p>그래서 제가 처음에 생각한 코드는 다음과 같았습니다.</p>
<pre><code class="language-tsx">&#39;resourceUrl&#39; in speaker &amp;&amp; speaker.resourceUrl !== &#39;&#39;</code></pre>
<h2 id="2-이중-부정-연산자-사용">2. 이중 부정 연산자(!!) 사용</h2>
<p>평소 AI와 페어 프로그래밍을 즐기는 저는, 이번에도 당시 최신 모델이었던 <strong>Gemini 3</strong>에게 조언을 구했습니다. 제 코드를 본 Gemini는 훨씬 간결한 이중 부정 연산자(<code>!!</code>)를 추천했습니다.</p>
<p>이중 부정 연산자는 <code>Falsy 값(거짓 같은 값)들을 한 번에 안전하게 처리할 수 있기 때문</code>입니다.</p>
<p>제가 처음에 작성한 조건(<code>!== &#39;&#39;</code>)은 &#39;빈 문자열&#39;은 막을 수 있지만, 만약 데이터가 <code>null</code>이나 <code>undefined</code>로 들어오는 예외 상황까지는 완벽하게 방어하지 못합니다. 이런 모든 케이스를 <code>&amp;&amp;</code> 연산자로 일일이 연결하다 보면 코드는 필연적으로 길고 지저분해질 수밖에 없습니다.</p>
<p>반면, <code>!!</code> 연산자는 <code>null</code>, <code>undefined</code>, <code>&quot;&quot;</code> 등 <code>데이터가 없는 모든 상태</code>를 한 번에 감지하여 깔끔하게 <code>false</code>로 처리해 준다는 장점이 있었습니다.</p>
<pre><code class="language-tsx">!!speaker.resourceUrl</code></pre>
<h1 id="해결-이중-부정-연산자-사용">해결: 이중 부정 연산자(!!) 사용</h1>
<p>결론적으로 저는 이중 부정 연산자(<code>!!</code>)를 채택했습니다.
코드가 간결해지고, 앞서 고민했던 <strong>Falsy 값(<code>undefined</code>, <code>null</code>, <code>&quot;&quot;</code>)들을 한 번에 안전하게 처리</strong>할 수 있다고 생각했기 때문입니다.</p>
<blockquote>
<p>이중 부정 연산자(!!)?</p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_NOT#double_not_!!">MDN 공식 문서</a>에 따르면, 자바스크립트에 !!라는 별도의 연산자는 존재하지 않습니다. 이는 논리 부정 연산자를 두 번 사용하여 값을 명시적으로 Boolean 타입으로 변환하는 기법입니다.</p>
</blockquote>
<p>동작 원리:</p>
<ol>
<li>첫 번째 <code>!</code>: 값을 Boolean으로 변환한 뒤, 그 값을 반전시킵니다.</li>
<li>두 번째 <code>!</code>: 반전된 값을 다시 반전시켜 원래 값의 참/거짓 속성을 회복합니다.<blockquote>
</blockquote>
Boolean(value) 생성자를 사용하는 것과 동일한 효과를 내며, 값이 <code>존재 하는지</code> 여부를 가장 간결하게 검증할 때 사용합니다.<blockquote>
</blockquote>
</li>
</ol>
<h2 id="실제-코드">실제 코드</h2>
<pre><code class="language-tsx">// Sessions (부모 컴포넌트) 일부
{sessions[activeTab].speakers.map((speaker, index) =&gt; (
  &lt;SessionCard
    key={`${activeTab}-${index}`}
    // ...
    hasMaterials={!!speaker.resourceUrl}
  /&gt;
))}

// SessionCard (자식 컴포넌트) 일부
{hasMaterials ? (
  &lt;a&gt;
    &lt;DownloadRoundedIcon/&gt;{&#39; &#39;}발표자료
  &lt;/a&gt;
) : (
  &lt;span&gt;자료 미제공&lt;/span&gt;
)}</code></pre>
<h1 id="회고">회고</h1>
<p>이번 기회에 Gemini 3를 사용하여 기획부터 구현까지 진행하며 생각지 못한 예외 케이스를 점검할 수 있었습니다.</p>
<p>사실 이 글을 작성하게 된 가장 큰 이유는 이중 부정 연산자 때문이었습니다. 익숙하지 않은 문법인데 글을 정리하면서 이해할 수 있었습니다.</p>
<p>또한, 이중 부정 연산자를 정리하면서 Boolean() 생성자가 같은 역할을 한다는 것을 알게 되었습니다. 다음엔 Boolean() 생성자를 공부해 봐야겠습니다.</p>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://github.com/TeoConference/TEOConf-FE/issues/116">Teo Conf 2025 홈페이지 발표자료 다운로드 기능 기획</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean/Boolean">Boolean Constructor | MDN Docs</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_NOT#double_not_!!">Logical Not Operator #!! | MDN Docs</a></li>
<li><a href="https://www.teoconf.com/">Teo Conf 2025 Home Page</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[11월] 바이브 코딩 너머 개발자 생존법]]></title>
            <link>https://velog.io/@soleil_lucy_75/11%EC%9B%94-%EB%B0%94%EC%9D%B4%EB%B8%8C-%EC%BD%94%EB%94%A9-%EB%84%88%EB%A8%B8-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%83%9D%EC%A1%B4%EB%B2%95</link>
            <guid>https://velog.io/@soleil_lucy_75/11%EC%9B%94-%EB%B0%94%EC%9D%B4%EB%B8%8C-%EC%BD%94%EB%94%A9-%EB%84%88%EB%A8%B8-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%83%9D%EC%A1%B4%EB%B2%95</guid>
            <pubDate>Sun, 30 Nov 2025 14:51:00 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>&quot;한빛미디어 서평단 &lt;나는리뷰어다&gt; 활동을 위해서 책을 협찬 받아 작성된 서평입니다.&quot;</strong></p>
</blockquote>
<h1 id="책-한눈에-보기">책 한눈에 보기</h1>
<p><img src="https://cdn-prod.hanbit.co.kr/books/c3bfebce-031f-4a97-9e34-d6b6ee2fd2d4.jpg" alt=""></p>
<ul>
<li>책 제목: <a href="https://www.hanbit.co.kr/store/books/look.php?p_code=B2408252176"><strong>바이브 코딩 너머 개발자 생존법</strong></a></li>
<li>저자: 애디 오스마니</li>
<li>번역: 강민혁</li>
<li>출간: 2025-11-10</li>
</ul>
<h1 id="책을-읽고나서">책을 읽고나서</h1>
<p>올해 초 Andrej Karpathy(OpenAI 공동 창립자이자 Tesla 전 AI 리더)가 X에서 제시한 &#39;Vibe Coding&#39;이라는 용어를 접한 후, 바이브 코딩에 큰 흥미를 느꼈습니다. 여름에는 바이브 코딩 해커톤 밋업에 참여해 &#39;냉장고 속 재료로 AI가 레시피를 추천해주는 웹 애플리케이션&#39;을 만들 정도로 적극 활용하고 있습니다.</p>
<p>AI로 프로토타입이나 MVP를 빠르게 만드는 것은 편리하지만, 한편으로는 AI로 인해 개발자 고용 시장이 얼어붙고 있는 상황에서 &#39;나는 어떻게 살아남을 수 있을까?&#39;라는 고민이 생겼습니다. 그러던 중 &#39;바이브 코딩 너머 개발자 생존법&#39;을 서평할 기회가 생겼고, 이 책이 내 고민을 조금이라도 덜어줄 수 있을지 궁금했습니다.</p>
<p>책은 바이브 코딩의 개념과 &#39;AI 보조 엔지니어링&#39;부터 시작해, 프롬프트 기법과 안티 패턴, 프로토타입/MVP 제작 방법을 알려줍니다. 이어서 실제 배포와 프로덕션 레벨 개발 단계를 설명하고, 마지막으로 보안, 윤리, 미래에 대한 이야기까지 다룹니다.</p>
<p>책을 읽고 긍정적으로 든 생각은 <strong>AI를 잘 활용하면 팀 프로젝트를 혼자서도 해낼 수 있겠다</strong>는 것이었습니다. 최근 나와 주변 사람들이 불편해하는 문제를 직접 해결해보고 싶다는 생각을 하고 있었는데, AI를 잘 활용하면 금방 만들 수 있을 것 같았습니다.</p>
<p>하지만 부정적인 생각도 들었습니다. 아직 개발자로서 커리어를 쌓지 못한 상황에서, 지금도 회사에 들어가지 못하고 있는데 몇 년 후면 회사 들어가기가 더 힘들어지는 건 아닐까 하는 걱정입니다. 책에서는 개발자가 점점 아키텍처 설계나 전략적 의사 결정에 더 집중하게 될 것이라고 언급합니다. 그런데 나는 아직 회사에서 실무 경험을 쌓지 못했는데, 어떻게 그런 레벨에 도달할 수 있을까요?</p>
<p>고민을 해결하려고 읽었지만, 오히려 고민이 더 생긴 기분입니다. 그래도 AI와 협업하는 방법과 주니어 레벨에서 AI를 활용한 학습법을 배웠으니, 남은 2025년에 실천해봐야겠습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] undefined와 null]]></title>
            <link>https://velog.io/@soleil_lucy_75/JS-undefined%EC%99%80-null</link>
            <guid>https://velog.io/@soleil_lucy_75/JS-undefined%EC%99%80-null</guid>
            <pubDate>Sun, 30 Nov 2025 04:03:09 GMT</pubDate>
            <description><![CDATA[<p>undefined와 null의 차이는 알고 있었지만, 항상 다른 분들이 정리한 글을 통해서만 이해했습니다. 이번 기회에 공식 문서를 직접 찾아보고 나만의 언어로 정리해두고 싶어 이 글을 작성합니다.</p>
<h1 id="undefined-정의">undefined 정의</h1>
<blockquote>
<p>4.4.13 undefined value</p>
<p>primitive value used when a variable has not been assigned a value</p>
</blockquote>
<p>변수에 값이 할당되지 않았을 때, 사용되는 primitive 값</p>
<h2 id="예시">예시</h2>
<pre><code class="language-javascript">// 변수에 값을 할당하지 않을 경우
let number;
console.log(number); // undefined

// =====

// API 응답에서 선택적 필드
const userProfile = {
  name: &quot;김철수&quot;,
  email: &quot;kim@example.com&quot;,
  // phone 필드 없음 (선택 사항)
};

console.log(userProfile.phone); // undefined
const displayPhone = userProfile.phone ?? &quot;전화번호 미등록&quot;;</code></pre>
<h1 id="null-정의">null 정의</h1>
<blockquote>
<p>4.4.15 null value</p>
<p>primitive value that represents the intentional absence of any object value</p>
</blockquote>
<p>어떤 값의 <code>의도적인 부재</code>를 나타내는 primitive 값</p>
<p><em>명세서에는 &#39;object value&#39;라고 표현되어 있지만, 실제로는 모든 타입의 값에 사용 가능합니다.</em></p>
<h2 id="예시-1">예시</h2>
<pre><code class="language-javascript">// React에서 로그인 전 사용자 상태
// null로 사용자 상태 초기화
const [currentUser, setCurrentUser] = useState(null);

useEffect(() =&gt; {
  if (currentUser === null) {
    router.push(&quot;/login&quot;); // 로그인 페이지로 이동
  }
}, [currentUser]);</code></pre>
<h1 id="undefined와-null의-차이">undefined와 null의 차이</h1>
<p>둘 다 <code>값이 없음</code>을 나타내지만, 할당 주체가 다릅니다.</p>
<ul>
<li><strong>undefined</strong>: 변수를 선언만 하고 값을 할당하지 않았을 때, JavaScript가 자동으로 넣어주는 값</li>
<li><strong>null</strong>: 개발자가 &quot;이 변수는 의도적으로 비어있다&quot;고 명시하고 싶을 때 직접 할당하는 값</li>
</ul>
<p>즉, undefined는 <code>자동</code>, null은 <code>의도적</code>입니다.</p>
<h2 id="예시-2">예시</h2>
<pre><code class="language-javascript">async function fetchUser(id) {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      return null; // 의도적으로 &quot;사용자 없음&quot;을 표현
    }
    return await response.json();
  } catch (error) {
    return null;
  }
}

const user = await fetchUser(123);

// null 체크: 명시적으로 &quot;사용자를 찾지 못함&quot;
if (user === null) {
  showErrorMessage(&quot;사용자를 불러올 수 없습니다&quot;);
}

// undefined 체크: user 객체에 nickname 필드가 없음
if (user?.nickname === undefined) {
  showErrorMessage(&quot;닉네임이 설정되지 않았습니다&quot;);
}</code></pre>
<h1 id="회고">회고</h1>
<p>undefined와 null의 차이는 여러 개발 블로그와 강의를 통해 알고 있었습니다. 하지만 &quot;이 내용은 어떤 정보를 통해서 작성된걸까?&quot;라는 의문을 갖고 있었습니다.</p>
<p>이번에 글을 정리하면서 <strong>JavaScript의 공식 표준 문서인 ECMAScript 명세서</strong>를 직접 확인해봐야겠다고 생각했습니다.</p>
<p>영어로 되어 있다는 이유로 계속 미뤄왔는데, 이번에 용기내서 null과 undefined 정의를 찾아보니 생각보다 어렵지 않았습니다.</p>
<p>앞으로는 JavaScript 문법이나 기능이 궁금할 때 ECMAScript <strong>명세서부터 확인하는 습관</strong>을 가져야겠습니다. 가장 정확한 출처라고 생각해서요.</p>
<h1 id="참고-자료">참고 자료</h1>
<ul>
<li><a href="https://tc39.es/ecma262/">ECMAScript® 2026 Language Specification</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/undefined">undefined | MDN Docs</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/null">null | MDN Docs</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] useEffect, 렌더링이 만드는 부수 효과를 다루는 법]]></title>
            <link>https://velog.io/@soleil_lucy_75/useEffect-%EB%A0%8C%EB%8D%94%EB%A7%81%EC%9D%B4-%EB%A7%8C%EB%93%9C%EB%8A%94-%EB%B6%80%EC%88%98%ED%9A%A8%EA%B3%BC%EB%A5%BC-%EB%8B%A4%EB%A3%A8%EB%8A%94-%EB%B2%95</link>
            <guid>https://velog.io/@soleil_lucy_75/useEffect-%EB%A0%8C%EB%8D%94%EB%A7%81%EC%9D%B4-%EB%A7%8C%EB%93%9C%EB%8A%94-%EB%B6%80%EC%88%98%ED%9A%A8%EA%B3%BC%EB%A5%BC-%EB%8B%A4%EB%A3%A8%EB%8A%94-%EB%B2%95</guid>
            <pubDate>Sun, 23 Nov 2025 04:48:15 GMT</pubDate>
            <description><![CDATA[<h1 id="what-effect와-useeffect의-개념">What: Effect와 useEffect의 개념</h1>
<h2 id="effect란">Effect란?</h2>
<p><code>Effect</code>는 <code>렌더링 자체에 의해 발생하는 부수 효과</code>를 말합니다. 버튼 클릭이나 폼 제출 같은 특정 이벤트가 아니라, 컴포넌트가 화면에 나타나는 것만으로 실행되는 작업을 의미합니다.</p>
<p>Effect는 <code>컴포넌트 렌더링 후 화면 업데이트가 완료된 시점에 실행</code>됩니다. 이 타이밍이 React가 관리하지 않는 외부 시스템, 즉 브라우저 API, 서버, 외부 라이브러리 같은 것들과 React 컴포넌트를 동기화하기에 적절하다고 공식 문서에서는 설명합니다.</p>
<blockquote>
<p>외부 시스템?</p>
</blockquote>
<p>React에 제어되지 않는 시스템들을 의미합니다. 예를 들어 브라우저 API(타이머, 웹 스토리지 등), 서버 연결, 써드 파티 라이브러리 등이 외부 시스템에 해당합니다.</p>
<blockquote>
</blockquote>
<h3 id="부수-효과란">부수 효과란?</h3>
<p>프로그래밍에서 <code>부수 효과(side effect)</code>란 <code>함수가 결괏값을 반환하는 것 외에 외부 세계에 어떤 영향을 주는 것</code>을 말합니다. 예를 들어 함수가 전역 변수를 수정하거나, 파일에 데이터를 쓰거나, 네트워크 요청을 보내거나, 콘솔에 로그를 출력하는 것들이 모두 부수 효과입니다.</p>
<h3 id="react에서-부수-효과를-다루는-방법">React에서 부수 효과를 다루는 방법</h3>
<p>React에서는 <code>컴포넌트 함수가 JSX를 반환하는 것</code>이 주 목적입니다. React는 컴포넌트가 순수 함수처럼 동작하길 기대합니다. API 호출, DOM 조작, 구독 설정 같은 작업들은 부수 효과에 해당합니다. </p>
<blockquote>
<p>React 컴포넌트의 순수성?</p>
</blockquote>
<p>React는 작성하는 모든 컴포넌트가 순수 함수라고 가정합니다. 순수 함수란 자신의 일에만 집중하는 함수를 말하는데, 호출되기 전에 존재했던 객체나 변수를 변경하지 않고, 같은 입력에 대해 항상 같은 결과를 반환합니다. 컴포넌트로 치면 같은 props와 state가 주어졌을 때 항상 같은 JSX를 반환해야 한다는 의미입니다.</p>
<blockquote>
</blockquote>
<p>컴포넌트를 순수하게 유지하면서도 부수 효과는 처리해야 하는 상황, 이 딜레마를 해결하는 것이 바로 <code>Effect</code>입니다. 부수 효과를 렌더링 과정과 분리해서, 렌더링이 완료된 후에 안전하게 실행할 수 있게 해줍니다. 이렇게 하면 컴포넌트는 순수성을 유지하면서도 필요한 부수 효과를 처리할 수 있습니다.</p>
<h3 id="예시-1-채팅방-서버-접속">예시 1: 채팅방 서버 접속</h3>
<p>가족 단체 채팅방에 들어가는 상황을 생각해봅시다. 채팅방 화면이 렌더링되면 자동으로 채팅 서버에 접속해야 하고, 채팅방을 나가서 채팅방 화면이 사라지면 자동으로 서버 연결을 해제해야 합니다. 클릭이나 입력 같은 사용자 행동 없이도 렌더링만으로 서버 접속이 일어나는데, 이것이 바로 부수 효과입니다.</p>
<h3 id="예시-2-페이지-제목-실시간-업데이트">예시 2: 페이지 제목 실시간 업데이트</h3>
<p>페이지가 렌더링되면 브라우저 탭의 제목을 현재 시간으로 계속 업데이트하는 기능을 생각해봅시다. 컴포넌트가 화면에 나타나면 1초마다 제목이 바뀌어야 하고, 컴포넌트가 사라지면 타이머를 정리해야 합니다. 이 역시 사용자의 어떤 이벤트도 필요 없이 페이지가 화면에 나타나기만 하면 실행됩니다.</p>
<h2 id="useeffect란">useEffect란?</h2>
<p><code>useEffect</code>는 <code>Effect를 구현하는 React Hook</code>입니다. Effect가 외부 시스템과 동기화하는 데 사용되기 때문에, useEffect를 “외부 시스템과 컴포넌트를 동기화하는 Hook”이라고도 표현합니다.</p>
<p>렌더링 후 실행할 부수 효과 코드를 useEffect의 콜백 함수에 작성하면, React가 화면을 그린 다음 해당 코드를 실행합니다. 간단한 예시를 보겠습니다.</p>
<pre><code class="language-js">useEffect(() =&gt; {
  // 렌더링이 완료된 후 실행
  console.log(&#39;페이지가 렌더링되었습니다&#39;);

  // 외부시스템(채팅 서버)와 연결
  const connection = connectToChatServer();

  // cleanup 함수: 컴포넌트가 사라질 때 실행
  return () =&gt; {
    connection.disconnect();
  };
});</code></pre>
<p>이렇게 useEffect를 사용하면 렌더링 타이밍에 맞춰 외부 시스템과의 동기화를 안전하게 처리할 수 있습니다. React는 컴포넌트가 화면에 나타날 때 Effect를 실행하고, 사라질 때는 cleanup 함수를 실행해서 정리 작업을 수행합니다.</p>
<h1 id="why-useeffect를-사용하는-이유">Why: useEffect를 사용하는 이유</h1>
<h2 id="컴포넌트와-외부-시스템과의-동기화를-위해">컴포넌트와 외부 시스템과의 동기화를 위해</h2>
<p>React 컴포넌트는 단순히 화면을 그리는 것 이상의 작업이 필요할 때가 많습니다. What 목차에서 나온 내용에서 외부 시스템, 즉 서버, 브라우저 API, 외부 라이브러리 같은 것들과 컴포넌트를 연결해야 하는 상황이 생깁니다.</p>
<p>예를 들어 채팅 애플리케이션을 만든다고 생각해봅시다. 채팅방 컴포넌트가 화면에 나타나면 자동으로 채팅 서버에 접속해야 하고, 사용자가 채팅방을 나가면 서버 연결을 끊어야 합니다. 또 다른 예로, 쇼핑몰에서 상품 상세 페이지를 보는 상황을 생각해봅시다. 상품 페이지가 렌더링되면 자동으로 해당 상품이 “최근 본 상품” 목록에 추가되어 있습니다. 사용자가 “추가” 버튼을 누를 필요 없이 페이지를 보는 것만으로 자동으로 저장됩니다.</p>
<p>이런 작업들의 공통점은 사용자가 버튼을 클릭하거나 무언가를 입력하는 행동과는 관계없이, 컴포넌트가 화면에 나타나는 것 자체가 트리거가 된다는 점입니다. React에서는 이렇게 컴포넌트의 상태(화면에 있음/없음)에 따라 외부 시스템(서버, 브라우저 API 등)의 상태도 함께 맞춰주는 것을 ‘동기화’라고 표현합니다. 그렇다면 이런 동기화 작업을 어디에 작성해야 할까요?</p>
<h3 id="렌더링-코드에-작성">렌더링 코드에 작성?</h3>
<p>컴포넌트의 렌더링 코드, 즉 JSX를 반환하는 함수 본문에 직접 작성하면 어떨까요? return 문 바로 위에 서버 접속 코드를 넣으면, 컴포넌트가 실행될 때 그 코드도 함께 실행될 것입니다.</p>
<pre><code class="language-js">function ChatRoom() {
  // connection은 채팅 서버와의 연결을 관리하는 객체라고 가정
  connection.connect(); // ❌
  return &lt;div&gt;채팅방&lt;/div&gt;;
}</code></pre>
<p>하지만 이 방법은 큰 문제가 있습니다. What 섹션에서 배웠듯이 React는 컴포넌트가 순수 함수여야 한다고 가정합니다. 순수 함수는 같은 입력에 항상 같은 출력을 반환하고, 외부 세계에 영향을 주지 않아야 합니다. 그런데 서버에 접속하는 것은 외부 세계에 영향을 주는 부수 효과입니다. 이는 컴포넌트의 순수성을 깨뜨립니다.</p>
<p>더 심각한 문제는 React가 컴포넌트를 여러 번 렌더링할 수 있다는 점입니다. 개발 모드에서는 의도적으로 두 번 렌더링하기도 하고, 상태가 변경되거나 부모 컴포넌트가 리렌더링되면 이 컴포넌트도 다시 렌더링됩니다. 렌더링될 때마다 서버 접속이 반복되면 이미 연결되어 있는데도 새로운 연결을 계속 만들게 됩니다. 게다가 컴포넌트가 화면에서 사라질 때 이 연결들을 정리할 방법도 없습니다. 연결이 쌓이면서 메모리 낭비가 발생하고 서버에도 부담을 주게 됩니다.</p>
<h3 id="이벤트-핸들러에-작성">이벤트 핸들러에 작성?</h3>
<p>그렇다면 이벤트 핸들러에 작성하면 어떨까요? 이벤트 핸들러는 사용자의 특정 행동에 반응해서 실행되는 코드입니다. 렌더링이 이미 끝나고 화면이 다 그려진 후에 실행되기 때문에, 렌더링의 순수성과는 관계가 없습니다. 그래서 이벤트 핸들러 안에서는 서버에 데이터를 보내거나 상태를 변경하는 등의 부수 효과를 자유롭게 처리할 수 있습니다.</p>
<pre><code class="language-js">function ChatRoom() {
  return (
    &lt;button onClick={() =&gt; connection.connect()}&gt;
      입장하기
    &lt;/button&gt;
  );
}</code></pre>
<p>이 코드는 문법적으로 문제가 없지만 우리가 원하는 동작을 구현할 수 없습니다. 우리는 사용자가 버튼을 클릭하면 접속하는 게 아니라, 채팅방 화면이 나타나면 자동으로 접속하길 원합니다. 이벤트 핸들러는 사용자의 특정 행동에만 반응하기 때문에, 컴포넌트가 화면에 나타나는 것과 같은 생명주기 이벤트를 처리할 수 없습니다.</p>
<h3 id="useeffect-훅에-작성">useEffect 훅에 작성!</h3>
<p>채팅방 컴포넌트가 렌더링되면 자동으로 서버에 접속하고 싶은데, 렌더링 코드에 넣으면 순수성을 위반하고, 이벤트 핸들러에 넣으면 자동 실행이 안 됩니다. 이 딜레마를 해결하는 것이 바로 useEffect입니다.</p>
<p>useEffect를 사용하면 렌더링의 순수성을 유지하면서도, 렌더링이 완료된 후에 부수 효과를 실행할 수 있습니다.</p>
<p>React의 처리 순서를 보면 이해가 쉽습니다:</p>
<ol>
<li>렌더링 단계: React는 컴포넌트 함수를 호출해서 어떤 JSX를 그려야 할지 계산합니다. 이전 화면과 비교해서 무엇을 바꿔야 하는지 알아냅니다. 이때 컴포넌트는 순수하게 유지됩니다.</li>
<li>화면 업데이트 단계(커밋 단계): 계산 결과를 실제 화면에 반영합니다. 브라우저가 실제로 보여주는 화면을 바꾸는 작업입니다.</li>
<li>화면 업데이트 후 단계: 화면 업데이트가 완료된 후에 useEffect가 실행됩니다.</li>
</ol>
<p>이 순서 덕분에 여러 가지 장점이 생깁니다.</p>
<ul>
<li>렌더링 순수성 유지: 부수 효과가 렌더링과 분리되므로, React가 안전하게 컴포넌트를 여러 번 호출하고 비교할 수 있습니다.</li>
<li>빠른 초기 화면 표시: 화면이 먼저 보이고 외부 시스템 작업은 나중에 처리되므로, 사용자는 빠르게 화면을 볼 수 있습니다.</li>
<li>안전한 DOM 접근: DOM이 완전히 준비된 상태에서 useEffect가 실행되므로, DOM을 안전하게 조작하거나 읽을 수 있습니다.</li>
<li>정리 작업 가능: useEffect는 컴포넌트가 화면에서 사라질 때 실행되는 정리 함수를 제공해서, 연결을 안전하게 해제할 수 있습니다.</li>
</ul>
<h2 id="정리-렌더링-순수성-유지와-외부-시스템과의-동기화">정리: 렌더링 순수성 유지와 외부 시스템과의 동기화</h2>
<p>useEffect를 사용하는 이유를 한 문장으로 정리하면, <code>React 컴포넌트의 순수성을 유지하면서도 외부 시스템과 안전하게 동기화</code>하기 위해서입니다. 렌더링 코드에 부수 효과를 넣으면 순수성이 깨지고 예측 불가능한 동작이 발생할 수 있습니다. 이벤트 핸들러는 사용자 행동에만 반응하므로 컴포넌트의 생명주기와 연결된 자동 동기화를 구현할 수 없습니다. useEffect는 이 두 가지 한계를 모두 극복하면서, 렌더링이 완료된 후 적절한 타이밍에 외부 시스템과의 동기화를 처리할 수 있게 해줍니다.</p>
<h1 id="how-useeffect-사용법">How: useEffect 사용법</h1>
<pre><code class="language-js">useEffect(setup, dependencies?)</code></pre>
<p>useEffect는 두 개의 매개변수를 받습니다.</p>
<h2 id="setup-함수">setup 함수</h2>
<p>첫 번째 매개변수인 <code>setup</code> 함수는 Effect의 로직이 포함된 함수입니다. 이 함수는 컴포넌트가 DOM에 추가된 후(마운트된 후)에 React가 실행합니다.</p>
<p>setup 함수 내부에서는 렌더링이 완료된 후 실행할 부수 효과 코드를 작성합니다. 위에서 설명했던 채팅 서버에 연결하는 작업도 setup 함수 내부에 작성합니다.</p>
<h3 id="cleanup-함수-선택사항">cleanup 함수 (선택사항)</h3>
<p><code>cleanup</code> 함수는 <code>Effect를 정리하는 함수로, 컴포넌트가 사라질 때 실행</code>됩니다. setup 함수는 선택적으로 cleanup 함수를 반환할 수 있습니다. 컴포넌트가 DOM에서 제거 되기 전(언마운트되기 전)에 React가 이 cleanup 함수를 실행합니다. cleanup 함수에서는 연결 해제, 타이머 정리, 이벤트 리스너 제거 같은 정리 작업을 수행합니다.</p>
<pre><code class="language-js">useEffect(() =&gt; {
  // setup: 부수 효과 실행
  const connection = connectToServer();

  // cleanup 함수 반환
  return () =&gt; {
    connection.disconnect();
  };
}, []);</code></pre>
<h2 id="dependencies-배열-선택사항">dependencies 배열 (선택사항)</h2>
<p>두 번째 매개변수는 <code>setup 함수 코드 내부에서 참조되는 모든 반응형 값들의 목록</code>입니다. 반응형 값에는 props, state, 그리고 컴포넌트 본문에 직접 선언된 모든 변수와 함수가 포함됩니다.</p>
<p>dependencies 배열은 선택사항이지만, 생략하거나 어떤 값을 넣느냐에 따라 Effect가 실행되는 시점이 달라집니다.</p>
<h3 id="dependencies에-따른-실행-시점">dependencies에 따른 실행 시점</h3>
<ol>
<li><p>빈 배열 <code>[]</code>을 전달하는 경우</p>
<pre><code class="language-js">useEffect(() =&gt; {
   console.log(&#39;컴포넌트가 마운트됐을 때 한 번만 실행&#39;);
}, []);</code></pre>
<p>빈 배열을 전달하면 컴포넌트가 마운트될 때만 Effect가 실행됩니다. 리렌더링이 발생해도 Effect는 다시 실행되지 않습니다. 초기 설정이나 한 번만 실행하면 되는 작업에 사용합니다.</p>
</li>
<li><p>dependencies를 생략하는 경우</p>
<pre><code class="language-js">useEffect(() =&gt; {
  console.log(&#39;컴포넌트가 렌더링될 때마다 실행&#39;);
});</code></pre>
<p>dependencies를 생략하면 컴포넌트가 렌더링될 때마다 Effect가 실행됩니다. 매번 리렌더링 후에 Effect가 실행되므로 주의해서 사용해야 합니다.</p>
</li>
<li><p>특정 값들을 배열에 전달하는 경우</p>
<pre><code class="language-js">useEffect(() =&gt; {
 console.log(&#39;userId가 변경될 때와 마운트될 때 실행&#39;);
 fetchUserData(userId);
}, [userId]);

// ——
useEffect(() =&gt; {
 console.log(&#39;userId 또는 postId가 변경될 때 실행&#39;);
}, [userId, postId]);</code></pre>
<p>배열에 특정 값을 전달하면 컴포넌트가 마운트될 때와 해당 값이 변경되어 리렌더링될 때 Effect가 실행됩니다. 여러 값을 넣을 수도 있으며, 배열 안의 값 중 하나라도 변경되면 Effect가 다시 실행됩니다.</p>
</li>
</ol>
<h1 id="when-실제-사용-예시">When: 실제 사용 예시</h1>
<h2 id="예시-1-데이터-페칭">예시 1: 데이터 페칭</h2>
<p>컴포넌트가 화면에 나타날 때 서버에서 데이터를 가져와야 할 때 사용합니다.</p>
<pre><code class="language-js">function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() =&gt; {
    let ignore = false;

    fetch(`https://api.example.com/users/${userId}`)
      .then(response =&gt; response.json())
      .then(data =&gt; {
        if (!ignore) {
          setUser(data);
        }
      });

    return () =&gt; {
      ignore = true;
    };
  }, [userId]);

  return &lt;div&gt;{user?.name}&lt;/div&gt;;
}</code></pre>
<ul>
<li><code>userId</code>가 변경될 때마다 해당 사용자의 데이터를 가져옵니다</li>
<li>cleanup 함수의 <code>ignore</code> 플래그는 race condition을 방지합니다. <code>userId</code>가 빠르게 변경될 때 이전 요청의 응답이 나중에 도착해도 무시됩니다<blockquote>
<p>race condition?</p>
<p>여러 비동기 작업이 경쟁하듯 실행될 때, 완료 순서가 예상과 달라서 잘못된 결과가 나오는 상황을 말합니다. 예를 들어 userId가 1에서 2로 바뀌었는데, userId 1의 API 응답이 userId 2의 응답보다 늦게 도착하면 화면에는 엉뚱하게 userId 1의 데이터가 표시됩니다.</p>
</blockquote>
</li>
</ul>
<h2 id="예시-2-외부-라이브러리-연동">예시 2: 외부 라이브러리 연동</h2>
<p>지도, 차트 같은 외부 라이브러리를 React 컴포넌트와 연결할 때 사용합니다.</p>
<pre><code class="language-js">function MapComponent({ latitude, longitude }) {
  const mapRef = useRef(null);
  const mapInstanceRef = useRef(null);

  // 지도 초기화
  useEffect(() =&gt; {
    mapInstanceRef.current = new MapLibrary.Map(mapRef.current, {
      center: { lat: latitude, lng: longitude }
    });

    return () =&gt; {
      mapInstanceRef.current.destroy();
    };
  }, []);

  // 위치 업데이트
  useEffect(() =&gt; {
    mapInstanceRef.current?.setCenter({ lat: latitude, lng: longitude });
  }, [latitude, longitude]);

  return &lt;div ref={mapRef} style={{ width: &#39;100%&#39;, height: &#39;400px&#39; }} /&gt;;
}</code></pre>
<ul>
<li>첫 번째 useEffect: 컴포넌트가 마운트될 때 지도를 생성하고, 언마운트될 때 cleanup에서 인스턴스를 제거합니다</li>
<li>두 번째 useEffect: <code>latitude</code>, <code>longitude</code>가 변경되면 지도의 중심 위치를 업데이트합니다</li>
<li>외부 라이브러리의 상태를 React의 props/state와 동기화합니다</li>
</ul>
<h1 id="회고">회고</h1>
<p>React강의를 듣다가 ‘useEffect 훅은 컴포넌트 렌더링의 부수 효과를 표현할 때 사용한다‘는 설명을 들었는데, 부수 효과가 정확히 무엇인지 이해하기 어려워서 이렇게 정리하게 되었습니다.</p>
<p>글을 정리하면서 세 가지를 이해할 수 있었습니다.</p>
<h2 id="부수-효과">부수 효과?</h2>
<p>React 컴포넌트는 순수 함수여야 합니다. 같은 props와 state가 주어지면 항상 같은 JSX를 반환해야 합니다. 컴포넌트의 본래 목적은 <code>화면을 그리는 것</code>이고, 이 렌더링과 직접 관련되지 않은 모든 작업들은 <code>부수 효과</code>라고 말합니다.</p>
<h2 id="외부-시스템과의-동기화">외부 시스템과의 동기화</h2>
<p><code>React가 제어하지 않는 시스템들</code>, 서버, 브라우저 API, 외부 라이브러리 같은 것들을 <code>외부 시스템</code>이라고 합니다. 컴포넌트가 화면에 나타나면 이런 외부 시스템과 연결하고, 사라지면 연결을 끊어야 합니다. <code>컴포넌트의 상태와 외부 시스템의 상태를 맞추는 것</code>을 <code>외부 시스템과의 동기화</code>라고 합니다.</p>
<h2 id="useeffect-훅을-사용하는-이유">useEffect 훅을 사용하는 이유?</h2>
<p>useEffect는 <code>컴포넌트의 렌더링 순수성을 유지</code>하면서도 <code>부수 효과를 안전하게 실행하고, cleanup 함수로 정리 작업까지 처리</code>할 수 있게 해주기 때문에 사용한다는 것을 배웠습니다.</p>
<p><code>데이터 패칭</code>이라는 작업은 부수 효과에 해당하기 때문에 useEffect훅을 사용하지는 이해할 수 있었습니다. React 컴포넌트는 순수하게 화면을 그리는 일에 집중하고, 그 외의 모든 부수 효과는 useEffect로 처리하도록 코드를 작성해야겠습니다.</p>
<h1 id="참고-자료">참고 자료</h1>
<ul>
<li><a href="https://ko.react.dev/reference/react/useEffect">useEffect | React Docs</a></li>
<li><a href="https://ko.react.dev/learn/synchronizing-with-effects">Effect로 동기화하기 | React Docs</a></li>
<li><a href="https://ko.react.dev/learn/keeping-components-pure">컴포넌트를 순수하게 유지하기 | React Docs</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] useRef Hook]]></title>
            <link>https://velog.io/@soleil_lucy_75/React-useRef-Hook</link>
            <guid>https://velog.io/@soleil_lucy_75/React-useRef-Hook</guid>
            <pubDate>Sun, 09 Nov 2025 10:07:41 GMT</pubDate>
            <description><![CDATA[<p><code>useRef</code> 훅을 이해하고 정리해두고 싶어 이 글을 작성하게 됐습니다. 최근 이정환님의 리액트 강의를 들으며 다시 공부하던 중 <code>useRef</code>가 등장했습니다. 그동안은 단순히 코드 예제를 따라 쓰기만 했지, 왜 그렇게 써야하는지? 동작하는지는 잘 몰랐습니다. 이번에 강의 내용과 리액트 공식 문서를 함께 참고하면서 개념을 정리해보려고 합니다.</p>
<h1 id="useref란">useRef란?</h1>
<p><code>useRef</code>는 렌더링에 필요하지 않는 값을 참조하거나 저장할 수 있는 React Hook입니다. 리렌더링 사이에서도 값이 유지되지만, 값이 바뀌더라도 컴포넌트를 다시 렌더링하지 않습니다.</p>
<pre><code class="language-jsx">const ref = useRef(initialValue);</code></pre>
<h2 id="매개변수">매개변수</h2>
<ul>
<li>initialValue: ref 객체의 <code>current</code> 프로퍼티 초기 설정값입니다. 여기에는 어떤 유형의 값이든 지정할 수 있습니다. 이 인자는 초기 렌더링 이후부터는 무시됩니다.</li>
</ul>
<h2 id="반환값">반환값</h2>
<p><code>useRef</code>는 단일 프로퍼티를 가진 객체를 반환합니다:</p>
<ul>
<li>current: 처음에는 전달한 initialValue로 설정됩니다. 나중에 다른 값으로 바꿀 수 있습니다. ref 객체를 JSX 노드의 ref 어트리뷰트로 React에 전달하면 React는 current 프로퍼티를 설정합니다.</li>
</ul>
<h2 id="초기화-예시">초기화 예시</h2>
<p>아래 코드에서 ref는 { current: 0 } 형태의 일반 JavaScript 객체로 생성됩니다. 이 current 값은 컴포넌트가 리렌더링되어도 유지됩니다.</p>
<pre><code class="language-jsx">const ref = useRef(0);
// ref = { current : 0 };</code></pre>
<h1 id="왜-필요할까">왜 필요할까?</h1>
<h2 id="렌더링을-유발하지-않고-값을-기억해야-할-때">렌더링을 유발하지 않고 값을 기억해야 할 때</h2>
<p>값을 저장해야 하지만 그 값이 변경되어도 화면을 다시 그릴 필요가 없을 때, useRef를 사용합니다. state와 달리 ref는 값이 바뀌어도 컴포넌트를 리렌더링하지 않습니다.</p>
<h1 id="언제-사용될까">언제 사용될까?</h1>
<h2 id="1️⃣-dom-요소에-포커스-주기">1️⃣ DOM 요소에 포커스 주기</h2>
<p>버튼 클릭 시 input에 자동으로 포커스를 맞출 때 사용합니다.</p>
<p><a href="https://playcode.io/react-playground--019a67ea-30a8-7308-a1f3-fda0779615f1">코드 실행해보러 가기</a></p>
<pre><code class="language-jsx">function SearchForm() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    &lt;&gt;
      &lt;input ref={inputRef} type=&quot;text&quot; /&gt;
      &lt;button onClick={handleClick}&gt;검색창 포커스&lt;/button&gt;
    &lt;/&gt;
  );
}</code></pre>
<h2 id="2️⃣-타이머-id-저장하기">2️⃣ 타이머 ID 저장하기</h2>
<p>setInterval로 시작한 타이머를 나중에 멈추기 위해 interval ID를 저장합니다.</p>
<p><a href="https://playcode.io/react-playground--019a67ed-a9a4-77b4-af9e-df196a3501ff">코드 실행해보러 가기</a></p>
<pre><code class="language-jsx">import { useState, useRef } from &#39;react&#39;;

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);
  const intervalRef = useRef(null);

  function handleStart() {
    setStartTime(Date.now());
    setNow(Date.now());

    clearInterval(intervalRef.current);
    intervalRef.current = setInterval(() =&gt; {
      setNow(Date.now());
    }, 10);
  }

  function handleStop() {
    clearInterval(intervalRef.current);
  }

  let secondsPassed = 0;
  if (startTime != null &amp;&amp; now != null) {
    secondsPassed = (now - startTime) / 1000;
  }

  return (
    &lt;&gt;
      &lt;h1&gt;Time passed: {secondsPassed.toFixed(3)}&lt;/h1&gt;
      &lt;button onClick={handleStart}&gt;
        Start
      &lt;/button&gt;
      &lt;button onClick={handleStop}&gt;
        Stop
      &lt;/button&gt;
    &lt;/&gt;
  );
}</code></pre>
<h2 id="3️⃣-검색창-디바운스-처리">3️⃣ 검색창 디바운스 처리</h2>
<p>사용자가 타이핑을 멈춘 후 일정 시간이 지나면 검색 API를 호출합니다. 매 입력마다 API를 호출하면 서버에 부담이 되므로, 타이머 ID를 ref에 저장해서 이전 타이머를 취소하고 새로운 타이머를 설정합니다.</p>
<p><a href="https://playcode.io/react-playground--019a67f3-063b-7629-aa98-e04d2ab1c8b4">코드 실행해보러 가기</a></p>
<pre><code class="language-jsx">function SearchInput() {
  const [keyword, setKeyword] = useState(&#39;&#39;);
  const timerRef = useRef(null);

  function handleChange(e) {
    const value = e.target.value;
    setKeyword(value);

    // 이전 타이머 취소
    if (timerRef.current) {
      clearTimeout(timerRef.current);
    }

    // 500ms 후 검색 API 호출
    timerRef.current = setTimeout(() =&gt; {
      // searchAPI(value);
      console.log(`검색 API 호출! 검색어: ${value}`);
    }, 500);
  }

  return (
    &lt;input 
      value={keyword}
      onChange={handleChange}
      placeholder=&quot;검색어를 입력하세요&quot;
    /&gt;
  );
}</code></pre>
<h2 id="4️⃣-폼-제출-중복-방지">4️⃣ 폼 제출 중복 방지</h2>
<p>사용자가 버튼을 여러 번 클릭해도 한 번만 제출되도록 방지합니다. 렌더링 없이 순수하게 중복 제출만 막고 싶을 때 ref를 사용합니다.</p>
<p><a href="https://playcode.io/example-payment--019a67ff-76b9-74e2-aaf9-e28a3ccaf0be">코드 실행해보러 가기</a></p>
<pre><code class="language-jsx">function PaymentForm() {
  const [formData, setFormData] = useState({});
  const isSubmittingRef = useRef(false);

  // 가짜 결제 API (2초 후 성공)
  const submitPayment = () =&gt; {
    return new Promise((resolve) =&gt; {
      setTimeout(() =&gt; {
      return resulve(&#39;api 호출 성공~&#39;);
      // return reject(&#39;api 호출 실패...&#39;);
            }, 2000);
    });
  };

  const handleSubmit = async () =&gt; {
    if (isSubmittingRef.current) return;

    isSubmittingRef.current = true;

    try {
      await submitPayment(formData);
      alert(&#39;결제 완료!&#39;);
    } catch (error) {
      alert(&#39;결제 실패&#39;);
    } finally {
      isSubmittingRef.current = false;
    }
  };

  return (
    &lt;button onClick={handleSubmit}&gt;
      결제하기
    &lt;/button&gt;
  );
}</code></pre>
<h1 id="주의할-점">주의할 점</h1>
<h2 id="1️⃣-ref-객체에-저장된-값이-렌더링에-사용된다면-변이하지-마세요">1️⃣ ref 객체에 저장된 값이 렌더링에 사용된다면 변이하지 마세요</h2>
<p><code>ref.current</code> 프로퍼티는 state와 달리 변이할 수 있습니다. 그러나 렌더링에 사용되는 객체(예: state의 일부)를 포함하는 경우 해당 객체를 변이 해서는 안 됩니다.</p>
<h2 id="2️⃣-ref-변경은-리렌더링을-트리거하지-않습니다">2️⃣ ref 변경은 리렌더링을 트리거하지 않습니다</h2>
<p><code>ref.current</code> 프로퍼티를 변경해도 React는 컴포넌트를 다시 렌더링하지 않습니다. <code>ref</code>는 <code>일반 JavaScript 객체</code>이기 때문에 React는 사용자가 언제 변경 했는지 알지 못합니다.</p>
<h2 id="3️⃣-렌더링-중에는-ref-객체를-읽거나-쓰지-마세요">3️⃣ 렌더링 중에는 ref 객체를 읽거나 쓰지 마세요</h2>
<p>초기화를 제외 하고는 렌더링 중에 <code>ref.current</code>를 쓰거나 읽지마세요. 이렇게 하면 컴포넌트의 동작을 예측할 수 없게 됩니다.</p>
<h2 id="4️⃣-strict-mode에서는-ref-객체가-두-번-생성된-후-하나가-버려집니다">4️⃣ Strict Mode에서는 ref 객체가 두 번 생성된 후 하나가 버려집니다</h2>
<p>Strict Mode에서 React는 컴포넌트 함수를 두 번 호출하여 의도하지 않은 변경을 찾을 수 있도록 돕습니다. 이는 개발 환경 전용 동작이며 Production 환경에는 영향을 미치지 않습니다. 그래서 ref 객체는 두 번 생성되고 그 중 하나는 버려집니다. 컴포넌트 함수가 순수하다면(그래야만 합니다), 컴포넌트의 로직에 영향을 미치지 않습니다.</p>
<blockquote>
<p>컴포넌트 함수가 순수하다?</p>
<p>같은 입력(props)에 대해 항상 같은 결과(JSX)를 반환하고, 외부 변수나 객체를 수정하지 않는 함수를 말합니다.</p>
</blockquote>
<h1 id="정리하면서">정리하면서</h1>
<p>useRef는 <strong>리렌더링을 유발하지 않으면서 값을 참조하거나 저장할 때 사용하는 훅</strong>이라는 걸 이해하게 되었습니다. useRef 훅의 가장 큰 특징은 값이 변경되어도 리렌더링을 유발하지 않는다는 점을 확실히 기억할 수 있게 되었습니다.</p>
<p>앞으로 프로젝트를 진행하면서 리렌더링 없이 무언가를 처리해야 하는 상황이 생기면 useRef를 떠올릴 수 있을 것 같습니다.</p>
<h1 id="참고-자료">참고 자료</h1>
<ul>
<li><a href="https://ko.react.dev/reference/react/useRef">useRef | React Official Docs</a></li>
<li><a href="https://ko.react.dev/learn/referencing-values-with-refs">Ref로 값 참조하기 | React Official Docs</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] Truthy와 Falsy]]></title>
            <link>https://velog.io/@soleil_lucy_75/JS-Truthy%EC%99%80-Falsy</link>
            <guid>https://velog.io/@soleil_lucy_75/JS-Truthy%EC%99%80-Falsy</guid>
            <pubDate>Sat, 01 Nov 2025 13:58:29 GMT</pubDate>
            <description><![CDATA[<p>리액트 강의를 듣다가 JavaScript의 Truthy와 Falsy에 대한 내용이 나왔습니다. <code>if (!null) {}</code> 같은 코드는 자주 사용했지만, null이 false로 평가되는 이유는 몰랐습니다. <code>Falsy</code>라는 개념으로 정의되어 있다는 걸 알게 되어 글로 정리해보려 합니다.</p>
<h1 id="정의-truthy와-falsy">정의: Truthy와 Falsy</h1>
<h2 id="boolean-context">Boolean context</h2>
<p><code>true</code> 또는 <code>false</code> 값이 필요한 상황을 말합니다. JavaScript에서는 조건문(if, else)이나 반복문(for, while) 등에서 Boolean context가 만들어집니다.</p>
<pre><code class="language-jsx">// if 조건문
if(조건) {}

// for 반복문
for(초기값; 조건; 증감) {}</code></pre>
<p>JavaScript는 Boolean context에서 타입 변환을 통해 어떤 값이든 Boolean 값으로 강제 변환합니다.</p>
<h2 id="falsy">Falsy</h2>
<p>Boolean context에서 <code>false</code>로 평가되는 값</p>
<h3 id="예시-값">예시 값</h3>
<ul>
<li>false</li>
<li>0</li>
<li>-0</li>
<li>0n</li>
<li>“”</li>
<li>null</li>
<li>undefined</li>
<li>NaN</li>
</ul>
<h2 id="truthy">Truthy</h2>
<p>Boolean context에서 <code>true</code>로 평가되는 값</p>
<h3 id="예시-값-1">예시 값</h3>
<p>falsy 값을 제외한 모든 값들</p>
<ul>
<li>true</li>
<li>{} (빈 객체)</li>
<li>[] (빈 배열)</li>
<li>“0” (문자열 “0”)</li>
<li>“false” (문자열 “false”)</li>
<li>Infinity</li>
<li>-Infinity</li>
<li>…</li>
</ul>
<h1 id="실전-활용">실전 활용</h1>
<h2 id="falsy-활용-예제">Falsy 활용 예제</h2>
<h3 id="nullundefined-체크">null/undefined 체크</h3>
<pre><code class="language-jsx">function displayName(person) {
    if(!person) {
        console.log(&quot;존재하지 않는 유저입니다.&quot;);
        return;
    }

    console.log(`user name: ${person.name}`);
}

const person = { name: &quot;Lu&quot; };
displayName(person);</code></pre>
<h3 id="로딩-상태-처리">로딩 상태 처리</h3>
<pre><code class="language-jsx">function PostList() {
    const { posts, isLoading } = usePostList();

    if(isLoading) return &lt;Loading /&gt;;
    if(!posts) return &lt;div&gt;데이터가 없습니다.&lt;/div&gt;;

    return(
        &lt;ul&gt;
            {posts.map(post =&gt; &lt;li key={post.id}&gt;{post.title}&lt;/li&gt;)}
        &lt;/ul&gt;
    );
}</code></pre>
<h2 id="truthy-활용-예제">Truthy 활용 예제</h2>
<h3 id="배열-객체-존재-확인">배열, 객체 존재 확인</h3>
<pre><code class="language-jsx">function processItems(items) {
    if(items &amp;&amp; items.length) {
        console.log(`${items.length}개의 아이템 처리 중...`);
        items.forEach(item =&gt; console.log(item));
        return;
    }

    console.log(&quot;처리할 아이템이 없습니다.&quot;);
}</code></pre>
<h3 id="조건부-렌더링">조건부 렌더링</h3>
<pre><code class="language-jsx">function UserProfile({ user }) {
    return (
        &lt;div&gt;
            {user &amp;&amp; &lt;h1&gt;{user.name}&lt;/h1&gt;}
            {user?.email &amp;&amp; &lt;p&gt;Email: {user.email}&lt;/p&gt;}
        &lt;/div&gt;
    );
}</code></pre>
<h1 id="참고-자료">참고 자료</h1>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Glossary/Falsy">Falsy | MDN Docs</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Glossary/Truthy">Truthy | MDN Docs</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>