<?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, 26 Jul 2026 14:10:33 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/review-claude-all-in-one-with-cowork-code-design</link>
            <guid>https://velog.io/@soleil_lucy_75/review-claude-all-in-one-with-cowork-code-design</guid>
            <pubDate>Sun, 26 Jul 2026 14:10:33 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>*&quot;한빛미디어 서평단 &lt;나는리뷰어다&gt; 활동을 위해서 책을 협찬받아 작성된 서평입니다.&quot;*</strong></p>
</blockquote>
<h1 id="책을-읽게된-계기">책을 읽게된 계기</h1>
<p>저는 클로드 프로 플랜을 결제해서 사용하고 있습니다. 개발 공부를 할 때, 그러니까 토이 프로젝트를 진행하거나 알고리즘 문제를 풀 때는 주로 클로드 코드를 사용하고, 채팅창은 블로그 글을 작성할 때 정도만 쓰고 있습니다.</p>
<p>그러다 보니 제가 쓰는 클로드는 사실상 코드와 채팅 두 가지에 머물러 있었습니다. 클로드 코워크와 클로드 디자인이 나왔다는 소식은 들었지만, 정작 제대로 사용해 본 적은 없었습니다.</p>
<p><a href="https://www.hanbit.co.kr/store/books/look.php?p_code=B3472808035">『클로드 올인원 with 코워크, 코드, 디자인』</a>은 제목 그대로 코워크, 코드, 디자인을 한 권에 담고 있고, 각 서비스에 어떤 일을 맡길 수 있는지를 실습을 통해 배울 수 있을 것 같았습니다. 익숙한 클로드 코드는 사용법을 점검하고, 아직 써보지 못한 코워크와 디자인은 사용 감을 익혀보자는 마음으로 이 책을 선택하게 되었습니다.</p>
<h1 id="인사이트">인사이트</h1>
<h2 id="이-일을-어디에-맡길-것인가">이 일을 어디에 맡길 것인가</h2>
<p>책은 초반부터 채팅·코워크·코드·디자인을 나란히 놓고 비교합니다. 읽으면서 제 나름대로 정리한 기준은 이렇습니다.</p>
<ul>
<li><strong>채팅</strong>: 글과 자료를 대화로 다루는 곳. 요약, 문서 다듬기, 아이디어 정리처럼 <strong>결과를 보면서 조금씩 고쳐 나가는 일</strong>에 맞습니다.</li>
<li><strong>코워크</strong>: 일을 통째로 맡기는 곳. 자료 수집, 보고서 작성처럼 여러 단계를 거치는 작업, <strong>중간 화면을 계속 들여다보지 않아도 되는 일</strong>에 어울립니다.</li>
<li><strong>코드</strong>: IT 작업을 자연어로 요청하는 곳. 파일 정리, 반복 작업 자동화처럼 <strong>내 컴퓨터의 실제 파일을 건드리는 일</strong>에 적합합니다.</li>
<li><strong>디자인</strong>: 시안과 슬라이드를 만드는 곳. 원페이저, 발표 슬라이드, SNS 이미지처럼 <strong>글이 화면 위의 배치로 바뀌어야 하는 일</strong>에 어울립니다.</li>
</ul>
<h2 id="한-번에-끝내려-하지-말-것">한 번에 끝내려 하지 말 것</h2>
<p>책의 실습을 따라 하다 보면 같은 패턴이 반복해서 나옵니다. <strong>한 번의 프롬프트로 끝내지 않는다</strong>는 것입니다.</p>
<p>글을 쓸 때라면 이런 식입니다.</p>
<ol>
<li>목차를 먼저 받고, 마음에 들면 그때 본문을 채우게 한다</li>
<li>작성된 글을 다시 검토하게 한다</li>
</ol>
<p>디자인으로 발표 자료를 만들 때도 똑같습니다. 자료에 들어갈 내용과 원하는 포맷을 먼저 주고 시안을 받은 뒤, 색상과 폰트 같은 세부를 수정 요청하고, 마지막에 검수합니다.</p>
<p>저는 그동안 CO-STAR 프레임워크를 사용해 긴 프롬프트 하나로 원하는 결과를 한 번에 얻어내려고 했습니다. 책의 실습을 따라 해보면서, 그러지 않아도 된다는 것을 깨닫게 되었습니다.</p>
<h2 id="프롬프트는-길지-않아도-된다">프롬프트는 길지 않아도 된다</h2>
<p>책에 나오는 예시 프롬프트가 대부분 짧습니다.</p>
<blockquote>
<p>예시</p>
<p>아래 안내를 더 부드럽고 명확하게 다듬어줘. 학부모 단체 문자로 보낼 문장이고, 너무 길지 않게 해줘.
내일 오후 4시 미술 수업은 재료 준비 때문에 10분 일찍 시작합니다. 늦지 않게 와주세요.</p>
</blockquote>
<p>앞서 말한 것처럼 저는 맥락, 목적, 대상, 톤, 출력 형식을 빼곡히 채운 프롬프트를 쓰는 쪽이었습니다. 그런데 책의 방식대로 짧게 요청하고 결과를 보면서 고쳐 나가니, 결과물이 딱히 나쁘지 않았습니다. 오히려 앞에서 다 정해놓고 시작하는 것보다, 첫 결과를 보고 나서야 &quot;아 이건 이렇게 가야겠다&quot;가 명확해지는 경우가 많았습니다.</p>
<h1 id="실습해본것">실습해본것</h1>
<h2 id="코워크로-anthropic-skill-guidepdf-파일-요약하기">코워크로 Anthropic-skill-guide.pdf 파일 요약하기</h2>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/fe43bcd8-d307-4c6f-8116-0db8cd3d5758/image.png" alt="클로드 코워크로 Anthropic-skill-guide.pdf 파일 요약 화면"></p>
<h2 id="코워크로-맛집-검색하기">코워크로 맛집 검색하기</h2>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/25e4d106-1e97-4f52-a145-1ebb379030b7/image.png" alt="클로드 코워크로 맛집 검색 화면"></p>
<h2 id="코워크로-도예-체험-안내문-검토해보기">코워크로 도예 체험 안내문 검토해보기</h2>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/2fdc02f1-0598-498e-9926-e57c9e4efdb4/image.png" alt="클로드 코워크로 도예 체험 안내문 검토 화면"></p>
<h1 id="참고">참고</h1>
<p><a href="https://www.hanbit.co.kr/store/books/look.php?p_code=B3472808035"> 『클로드 올인원 with 코워크, 코드, 디자인』 보러가기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LLM Wiki: 내가 모은 자료로 LLM이 만들고 관리하는 백과사전]]></title>
            <link>https://velog.io/@soleil_lucy_75/what-is-llm-wiki</link>
            <guid>https://velog.io/@soleil_lucy_75/what-is-llm-wiki</guid>
            <pubDate>Sat, 25 Jul 2026 14:36:11 GMT</pubDate>
            <description><![CDATA[<h1 id="들어가며-책에서-보고-궁금해진-단어-llm-wiki">들어가며: 책에서 보고 궁금해진 단어 “LLM Wiki”</h1>
<p>『클로드 올인원 with 코워크, 코드, 디자인』이라는 책을 읽다가 ‘LLM Wiki’라는 단어를 봤습니다. 처음 보는 단어는 아니었습니다. 이전에 들어봤던 단어인데, 그때는 이해하지 않은 상태로 이런게 있구나 하고 그냥 지나쳤습니다.
이번에 갑자기 호기심이 생겨 이 단어의 출처인 Andrej Karpathy의 gist를 찾아 읽고, 나만의 언어로 이해한 내용을 정리해 글로 쓰고자 합니다.</p>
<h1 id="개념-llm-wiki란">개념: LLM Wiki란?</h1>
<p>LLM Wiki는 내가 모은 자료를 LLM이 읽고 정리해서, 나만 보는 백과사전 한 권으로 계속 만들어 나가는 방식입니다. 위키백과처럼 문서들이 서로 링크로 이어져 있고, 새 자료가 들어올 때마다 관련된 문서들이 최신 정보로 수정됩니다.</p>
<p>기존 위키백과와 다른 점은, 편집을 사람이 아닌 LLM이 맡아서 유지보수 비용이 거의 들지 않는다는 점입니다. 사람이 관리하는 위키는 문서가 늘어날수록 손이 못 미쳐 방치되기 쉽지만, LLM은 지치지 않고 매번 관련 문서를 찾아 고칩니다. 다만 이게 완전히 저절로 굴러가는 건 아니라서, 나중에 다시 다룰 &quot;점검(lint)&quot; 과정이 따로 필요합니다.</p>
<h2 id="ragretrieval-augmented-generation-검색-증강-생성">RAG(Retrieval-Augmented Generation, 검색 증강 생성)</h2>
<p>RAG는 질문이 들어오면 자료에서 관련 있는 부분을 먼저 찾아온 뒤, 그 내용을 참고해 답을 만드는 방식입니다. LLM이 원래 몰랐던 내용도 자료만 붙여주면 답할 수 있게 해줍니다. NotebookLM, ChatGPT 파일 업로드 등이 여기에 해당됩니다.</p>
<p>잘 작동하지만, 질문할 때마다 LLM이 자료를 처음부터 다시 뒤져서 답변해 줍니다. 즉 파일 자체는 남아 있어도, 그 파일을 읽고 이해한 결과는 남지 않습니다. 이게 실제로는 세 가지 문제로 이어집니다.</p>
<ul>
<li><strong>매번 같은 일을 반복합니다.</strong> 어제 &#39;LLM Wiki&#39;라는 개념을 이해하려고 자료 5개를 붙여서 좋은 답을 받았어도, 오늘 비슷한 질문을 하면 LLM은 그 5개 자료를 또 처음부터 읽고 관련 부분을 다시 찾아야 합니다.</li>
<li><strong>답이 매번 달라질 수 있습니다.</strong> 그날 어떤 자료를 붙였는지, 질문을 어떻게 던졌는지에 따라 결과가 달라져서, 어제 받은 좋은 답을 오늘도 똑같이 받는다는 보장이 없습니다.</li>
<li><strong>여러 자료를 엮어야 하는 질문일수록 약해집니다.</strong> 다섯 개 문서를 종합 해야 나오는 통찰이나, &quot;A 자료와 B 자료가 서로 다른 얘기를 하네&quot; 같은 발견은 그 대화창을 벗어나면 사라집니다. 자료는 계속 쌓이는데, 그 자료에 대한 이해는 깊어지지 않습니다.</li>
</ul>
<h2 id="llm-wiki는-무엇이-다른가">LLM Wiki는 무엇이 다른가</h2>
<p>LLM Wiki는 질문할 때 정리하지 않고, 자료를 넣을 때 백과사전을 만들듯 미리 정리를 해둡니다. 새 자료가 들어오면 LLM이 그걸 읽고, 관련된 기존 문서들을 찾아가서 고칩니다. 문서에 새 사실을 덧붙이고, 요약 문서를 다시 쓰고, 예전에 적어둔 내용과 어긋나는 부분이 있으면 표시해 둡니다.</p>
<p>그래서 질문이 들어오면, LLM은 여전히 관련 문서를 찾아 읽는 과정을 거치지만 — 흩어진 원자료 조각을 뒤지는 게 아니라 이미 정리되고 서로 연결된 위키 문서를 찾아 읽습니다. 이렇게 나온 좋은 답변이나 비교, 새로 발견한 연결점은 대화 속에 묻히지 않고 위키의 새 문서로 편입되어, 다음 질문에서도 다시 쓰일 자산이 됩니다.</p>
<p>이 구조에서 사람의 역할은 자료를 고르고, 탐구 방향을 정하고, 좋은 질문을 던지는 것입니다. 자료를 읽고 요약하고 교차 참조를 관리하는 잡일은 LLM이 전담합니다.</p>
<p>정리하면, LLM Wiki는 매번 새로운 채팅이나 세션에서 파일을 업로드해 그 안에서 답을 찾는 방식이 아닙니다. 미리미리 정보를 정리해두고 계속 최신으로 유지해서, 사용자의 질문에 답변을 해줍니다.</p>
<h1 id="구조-llm-wiki는-무엇으로-이루어지나">구조: LLM Wiki는 무엇으로 이루어지나?</h1>
<p>보통 LLM에게 자료를 던져주면 질문할 때마다 처음부터 다시 찾아 조각을 맞춥니다. LLM Wiki는 다릅니다. 자료를 읽을 때 그 내용을 위키에 쌓아두고, 다음 질문부터는 정리된 위키를 바로 씁니다. 지식이 매번 새로 발견되는 게 아니라 쌓입니다.
이걸 가능하게 하기 위해 원자료(raw sources), 위키(정리된 문서 모음), 스키마(schema) 총 3개의 층 구조입니다.</p>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/2128e9f2-1e40-4951-a138-7a7bd960b235/image.png" alt="LLM Wiki Architecture"></p>
<h2 id="1-원자료raw-sources--손대지-않는-원본">1. 원자료(Raw sources) — 손대지 않는 원본</h2>
<p>내가 모아둔 자료 그 자체입니다. 스크랩한 웹 기사, 논문 PDF, 회의록, 책 읽으며 남긴 메모, 이미지나 데이터 파일 같은 것들이 그대로 쌓여 있는 폴더입니다.
LLM은 여기서 자료들을 읽기만 할 뿐 한 글자도 수정하지 않습니다. 원본이 그대로 남아 있어야 나중에 &quot;이 문장이 어디서 나온 얘기지?&quot; 하고 되짚어볼 수 있고, 위키가 잘못 정리 됐을 때 처음부터 다시 만들 수도 있기 때문입니다.</p>
<h2 id="2-위키wiki--llm이-쓰고-내가-읽는-곳">2. 위키(Wiki) — LLM이 쓰고, 내가 읽는 곳</h2>
<p>LLM이 원자료를 읽고 만들어낸 마크다운 문서들이 서로 링크로 이어져 쌓이는 계층입니다. 자료 하나하나의 요약 문서, 인물이나 조직 같은 개별 대상을 다루는 문서, 개념을 설명하는 문서, 여러 자료를 비교한 문서, 그리고 위키 전체의 지도 역할을 하는 개요 문서와 지금까지의 결론을 담은 종합 문서 같은 것들입니다.</p>
<p>이 층은 온전히 LLM이 관리합니다. 문서를 새로 만들고, 새 자료가 들어오면 고치고, 문서끼리의 참조를 관리하고, 앞뒤가 맞는지 챙기는 일을 전부 LLM이 합니다.</p>
<p>여기에는 개별 주제를 다루는 문서 말고도, 위키 전체를 훑는 역할을 하는 특별한 문서가 두 개 더 있습니다. 어떤 문서가 있는지 보여주는 목록인 <code>index.md</code>와, 언제 무슨 작업을 했는지 남기는 기록인 <code>log.md</code>입니다. 문서가 몇 개 없을 때는 없어도 그만이지만, 위키가 커질수록 LLM과 내가 이 안에서 길을 잃지 않게 해주는 파일들입니다.</p>
<h2 id="3-스키마schema--llm에게-주는-편집-지침">3. 스키마(Schema) — LLM에게 주는 편집 지침</h2>
<p><code>CLAUDE.md</code>나 <code>AGENTS.md</code> 같은 파일 하나에, 이 위키가 어떻게 생겼고 LLM이 어떻게 일해야 하는지를 적어둔 문서입니다.</p>
<p>문서 종류는 무엇이 있는지, 제목은 어떤 규칙으로 짓는지, 새 자료가 들어오면 어떤 순서로 처리 하는지, 질문에 답할 때는 무엇부터 읽는지 같은 내용이 들어갑니다.</p>
<p>앞의 두 층과 달리 이 층은 사람과 LLM이 같이 만들어 갑니다. 처음부터 완벽하게 쓸 수 없고, 쓰다 보면 &quot;이건 이렇게 하는 게 낫겠다&quot; 싶은 게 생길 때마다 규칙으로 추가해 나가게 됩니다.</p>
<h2 id="층을-나누는-이유">층을 나누는 이유?</h2>
<p>세 층을 나누는 이유는 누가 무엇을 소유 하는지 명확히 하기 위해서입니다. </p>
<p>원자료는 내가 고르고, 위키는 LLM이 쓰고, 스키마는 둘이 함께 다듬습니다. </p>
<p>이 경계가 흐려지면 문제가 생깁니다. LLM이 원본을 고치기 시작하면 무엇이 사실이고 무엇이 LLM의 해석인지 구분할 수 없게 되고, 내가 위키를 직접 손보기 시작하면 결국 사람이 관리하는 위키가 되어, 질문할 때마다 원자료에서 관련 부분을 다시 찾고 그걸 다시 종합해서 정리해야 하는 처음의 문제로 되돌아갑니다.</p>
<h1 id="운영-llm-wiki는-어떻게-굴러가는가">운영: LLM Wiki는 어떻게 굴러가는가?</h1>
<p>LLM Wiki는 원자료, 위키, 스키마라는 구조로 이루어져 있습니다. 이 구조는 세 가지 동작으로 운영됩니다.</p>
<p>자료를 넣고(Ingest), 질문을 던지고(Query), 가끔 전체를 점검하는(Lint) 것입니다. 이 세 가지 동작이 맞물려 돌아가면서, 원자료, 위키, 스키마라는 세 층이 하나의 백과사전이 되어 갑니다.</p>
<h2 id="자료-넣기-ingest">자료 넣기 (Ingest)</h2>
<p>새로운 자료 하나를 원자료 폴더에 넣고 LLM에게 처리하라고 시키면, LLM은 그 자료를 읽고, 핵심 내용이 뭔지 나와 이야기를 나눈 뒤, 그 자료를 요약하는 문서를 씁니다. 그러고 나서 인덱스를 갱신하고, 이 자료와 관련된 기존 문서들을 찾아가서 고치고, 마지막으로 로그에 처리 기록을 남깁니다.</p>
<p>Karpathy는 자료를 하나씩 넣으면서 그때그때 요약을 확인하고 방향을 잡아주는 방식을 선호한다고 말합니다. 하지만 감독을 덜 하며 여러 자료를 한 번에 몰아서 넣는 것도 가능합니다. 어느 쪽이 나에게 맞는 방식인지는 결국 써보면서 정해서, 스키마에 규칙으로 적어두면 됩니다.</p>
<h2 id="질문하기-query">질문하기 (Query)</h2>
<p>질문을 던지면 LLM은 목록 문서를 먼저 훑어 어떤 문서를 볼지 고르고, 그 문서들을 읽어 답을 만듭니다. 앞서 RAG 부분에서 말했던 &quot;매번 원자료를 처음부터 뒤지는&quot; 과정과 다른 점은, 이미 정리되고 서로 연결된 위키 문서에서 출발한다는 것입니다. 답변의 형태도 자유롭습니다. 마크다운 문서일 수도, 비교 표일 수도, 슬라이드나 차트일 수도 있습니다.</p>
<p>좋은 답변은 다시 위키의 새 문서로 편입될 수 있습니다. 질문에 답하다가 얻은 비교나 통찰, 새로 발견한 연결점은 그 대화 안에서 사라지지 않고, 위키에 남아 다음 질문에서 다시 쓰일 자산이 됩니다.</p>
<h2 id="점검하기-lint">점검하기 (Lint)</h2>
<p>주기적으로 LLM에게 위키 전체를 건강검진 해달라고 시키는 단계입니다. 이때 LLM이 찾는 것들은 대략 이렇습니다.</p>
<ul>
<li>문서와 문서 사이의 모순</li>
<li>새 자료가 들어오면서 낡아버린 예전 주장</li>
<li>어디서도 링크를 받지 못하는 고아 문서</li>
<li>분명 중요한데 아직 독립된 문서가 없는 개념</li>
<li>빠져 있는 상호 참조</li>
<li>웹 검색으로 채울 수 있는 데이터의 공백</li>
</ul>
<p>자료 넣기(Ingest) 단계가 아무리 꼼꼼해도 위키가 커지면 어딘가는 어긋나기 마련이고, 점검하기(Lint) 단계가 그 어긋난 부분을 주기적으로 찾아 표시해주지 않으면 위키는 조용히 부정확해질 수 있습니다. LLM은 이 점검 과정에서 다음에 찾아볼 만한 자료나 던져볼 만한 질문을 먼저 제안해주기도 합니다.</p>
<h2 id="indexmd와-logmd">index.md와 log.md</h2>
<p>앞의 세 동작을 보면 계속 같은 두 파일이 등장합니다. 자료를 넣을 때 갱신하는 인덱스와, 처리 기록을 남기는 로그입니다.</p>
<p><code>index.md</code>는 내용 중심의 목록입니다. 위키의 모든 문서를 링크와 한 줄 요약으로 정리한 카탈로그로, 자료를 넣을 때마다 갱신되고 질문에 답할 때 가장 먼저 읽힙니다.</p>
<p><code>log.md</code>는 시간 중심의 기록입니다. 자료를 넣고, 질문하고, 점검한 이력을 뒤에 계속 덧붙이기만 하는 파일입니다. 각 항목을 <code>## [2026-07-25] ingest | 자료 제목</code> 처럼 일정한 형식으로 시작하면 grep이나 tail로 최근 기록만 뽑아볼 수 있습니다.</p>
<blockquote>
<p><code>grep</code>: 파일에서 특정 패턴이 들어간 줄만 뽑아내는 명령어
<code>tail</code>: 파일의 마지막 몇 줄만 보여주는 명령어</p>
</blockquote>
<p>인덱스는 &quot;지금 위키에 무엇이 있는가&quot;에, 로그는 &quot;지금까지 무슨 일이 있었는가&quot;에 답하는 파일입니다.</p>
<h2 id="정리">정리</h2>
<p>정리하면, Ingest가 지식을 쌓고, Query가 그 지식을 꺼내 쓰면서 다시 쌓고, Lint가 전체가 어긋나지 않게 지켜줍니다. 이 세 동작으로 LLM Wiki를 운영하고, 이를 통해 RAG의 한계인 답변이 지식으로 쌓이지 않는 문제를 풀어냅니다.</p>
<h1 id="마무리-나는-어떻게-사용할까">마무리: 나는 어떻게 사용할까?</h1>
<p>여기까지 정리하고 나서 정작 &quot;그래서 나는 뭘로 만들지?&quot;라는 생각이 들었습니다.</p>
<p>Karpathy는 팀 위키, 몇 달짜리 연구 등의 예시를 들어줬는데 제 일상에 적용해보고자 찾아보니 아무것도 떠오르지 않았습니다.</p>
<p>많은 분들이 업무에 적용하시는 걸 보다 보니, 알바를 하고 있는 제게도 적용하면 좋을 예시가 하나 떠올랐습니다.</p>
<p>저는 B마트에서 크루로 일하고 있습니다. 입고, 출고, 재고 실사 같은 업무를 처음 배우던 시기에 업무 매뉴얼 같은 건 없었습니다. 선임 크루들이 옆에서 입으로 알려주시는 게 전부였고, 그게 저는 조금 불편했습니다. 다음 근무 때 쉽게 기억이 나지 않아서 다시 물어볼 때가 많더라고요.</p>
<p>그래서 배운 걸 잊지 않으려고 퇴근 후에 노션에 업무일지를 작성했습니다. 업무일지 덕분에 업무를 금방 익힐 수 있었습니다. 문제는 기록해두기만 했다는 것입니다. 일지가 쌓일수록 원하는 내용을 찾는 일은 오히려 더 어려워졌습니다. 잊지 않으려고 쓴 기록인데, 필요한 순간에 꺼내 보기 어려웠습니다.</p>
<p>LLM Wiki를 여기에 적용한다면 이런 흐름이 될 것 같습니다.</p>
<ul>
<li>Ingest: 교육받은 내용을 마크다운으로 남깁니다.</li>
<li>Query: 업무 중에 헷갈리면 문서를 뒤지는 대신 그냥 물어봅니다. &quot;Q. 출고 준비는 어떻게 해?&quot;</li>
<li>Lint: 새 업무를 배워 문서를 갱신했을 때, 기존 내용과 충돌하는 부분을 잡아냅니다. &quot;Q. 재고 실사 문서에는 A로 되어 있는데 방금 추가한 내용은 B입니다. 어느 쪽이 현재 기준인가요?&quot;</li>
</ul>
<p>알바 업무를 예시로 들었지만, 개발자로 일하게 될 때에도 업무를 이런 식으로 정리하면 편할 것 같습니다. 왜 사람들이 업무에 LLM Wiki를 도입하는지 이제 알 것 같습니다.</p>
<h1 id="출처">출처</h1>
<p><a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f">karpathy/llm-wiki.md | Gist</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[텔레그램 봇 만들기 #3: GitHub Actions 스케줄이 제시간에 실행되지 않는 문제 해결하기]]></title>
            <link>https://velog.io/@soleil_lucy_75/daily-briefing-bot-schedule-delay-3</link>
            <guid>https://velog.io/@soleil_lucy_75/daily-briefing-bot-schedule-delay-3</guid>
            <pubDate>Sat, 11 Jul 2026 10:19:01 GMT</pubDate>
            <description><![CDATA[<p>매일 아침 일정을 브리핑해주는 텔레그램 봇을 만들었습니다. GitHub Actions의 schedule 기능으로 매일 새벽 5시에 돌게 걸어두고, 이제 아침마다 아침마다 그날 할 일이 뭐가 있는지 파악하고 하루를 미리 그려볼 수 있겠구나 싶었습니다. 그런데 메시지는 오전 5시가 아니라 매일 6시가 넘어서야 도착했습니다.</p>
<h1 id="문제-매일-오전-5시에-오기로-한-브리핑-메시지가-1시간-넘게-늦게-도착한다">문제: 매일 오전 5시에 오기로 한 브리핑 메시지가 1시간 넘게 늦게 도착한다</h1>
<p>GitHub Actions 워크플로는 아래와 같습니다.</p>
<pre><code class="language-yaml"># .github/workflows/daily-briefing.yml
name: Daily briefing

on:
  schedule:
    # 매일 한국 시간(KST) 새벽 5시 정각에 실행
    # GitHub Actions는 UTC로 동작하므로 UTC 20:00 = KST 다음날 05:00
    - cron: &quot;0 20 * * *&quot;
  workflow_dispatch:

#...</code></pre>
<p>GitHub Actions의 cron은 UTC 기준이니까 UTC 20:00은 KST로 다음날 오전 5시가 맞습니다.</p>
<p>그런데 실제로 메시지가 도착한 시각과 Actions 실행 이력을 일주일치 대조해보니 아래와 같았습니다.</p>
<p>[7/5~7/11 까지의 Actions 실행 이력]</p>
<table>
<thead>
<tr>
<th>날짜</th>
<th>기대 시각</th>
<th>Actions run 시작</th>
<th>실행 소요</th>
<th>텔레그램 수신</th>
<th>지연</th>
</tr>
</thead>
<tbody><tr>
<td>7/5</td>
<td>05:00</td>
<td>06:06</td>
<td>19s</td>
<td>06:06</td>
<td>+66분</td>
</tr>
<tr>
<td>7/6</td>
<td>05:00</td>
<td>06:11</td>
<td>18s</td>
<td>06:11</td>
<td>+71분</td>
</tr>
<tr>
<td>7/7</td>
<td>05:00</td>
<td>06:42</td>
<td>16s</td>
<td>06:42</td>
<td>+102분</td>
</tr>
<tr>
<td>7/8</td>
<td>05:00</td>
<td>06:35</td>
<td>24s</td>
<td>06:35</td>
<td>+95분</td>
</tr>
<tr>
<td>7/9</td>
<td>05:00</td>
<td>06:17</td>
<td>20s</td>
<td>06:17</td>
<td>+77분</td>
</tr>
<tr>
<td>7/10</td>
<td>05:00</td>
<td>06:35</td>
<td>23s</td>
<td>06:35</td>
<td>+95분</td>
</tr>
<tr>
<td>7/11</td>
<td>05:00</td>
<td>06:13</td>
<td>19s</td>
<td>06:13</td>
<td>+73분</td>
</tr>
</tbody></table>
<p>[Actions 실행 이력 스크린샷]</p>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/4adfcbf0-33da-4151-b0f7-8834e7dbc889/image.png" alt="Actions 실행 이력 스크린샷"></p>
<p>Actions 실행 이력을 보니 지연 폭이 매일 다르다는 걸 볼 수 있습니다. 66분, 71분, 102분 등 들쭉날쭉하고 하루도 제시간에 실행된 적이 없습니다.</p>
<p>실행 시간은 평균 20초로 멀쩡했고, run 시작 시각과 텔레그램 수신 시각이 일치하는 걸 보니 스케줄이 지연되는 거라고 생각했습니다.</p>
<h1 id="원인-github-actions의-schedule-이벤트는-부하가-높은-시간대정각-등에-지연될-수-있다">원인: GitHub Actions의 schedule 이벤트는 부하가 높은 시간대(정각 등)에 지연될 수 있다</h1>
<p>GitHub Actions 스케줄이 왜 지연되는지 확인하기 위해 공식 문서를 읽어 봤습니다.</p>
<p>GitHub Docs의 <a href="https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule">Events that trigger workflows — schedule</a> 섹션 상단 Note에 아래와 같은 내용이 있습니다.</p>
<blockquote>
<p>schedule 이벤트는 GitHub Actions 워크플로 실행 부하가 높은 기간에 지연될 수 있다. 부하가 높은 시간대에는 매 정시(시각의 시작)가 포함된다. 부하가 충분히 높으면 대기 중인 일부 작업이 드롭될 수도 있다. 지연 가능성을 줄이려면 정시가 아닌 다른 분(minute)에 실행되도록 예약하라. (요약 번역)</p>
</blockquote>
<p>GitHub Actions의 cron은 “이 시각에 실행해준다”는 약속이 아니라 “이 시각에 큐에 넣고, 여유가 되면 실행한다”는 방식입니다. 정각에 cron을 걸어두는 사람이 많아 매 정시마다 큐가 몰리고, 제 워크플로도 그 혼잡에 밀려 매일 1시간 넘게 늦고 있었던 겁니다.</p>
<p>정리: 정시는 부하가 높은 시간대이기 때문에 제 시간에 스케줄이 실행되지 않을 수 있다.</p>
<h1 id="해결-방법-비교">해결 방법 비교</h1>
<p>일정 브리핑 메시지를 오전 5시에 받아보고 싶기 때문에 해결 방법을 찾아보았습니다. 개인용 봇이라 무료로 해결할 수 있는 방법만 추렸습니다.</p>
<h2 id="방법-1-cron의-분minute을-정시가-아닌-시간으로-옮기기">방법 1. cron의 분(minute)을 정시가 아닌 시간으로 옮기기</h2>
<p>공식 문서에 언급되어 있는 방법으로 정시가 아닌 다른 분으로 시간을 옮겨 지연 시간을 최소화합니다.</p>
<p><code>0 20 * * *</code>(KST 05:00)을 예를 들어 <code>23 19 * * *</code>(KST 04:23)처럼 어중간한 분으로 옮기면 정각 혼잡을 피할 수 있습니다.</p>
<h2 id="방법-2-외부-무료-cron-서비스에서-github-workflow_dispatch-api-호출">방법 2. 외부 무료 cron 서비스에서 GitHub workflow_dispatch API 호출</h2>
<p>workflow yml 파일에 이미 있는 <code>workflow_dispatch</code> 트리거를 활용하는 방법입니다. cron-job.org나 Cloudflare Workers 같은 외부 서비스가 매일 정해진 시각에 GitHub API를 호출해서 워크플로를 직접 깨웁니다. <code>workflow_dispatch</code>는 스케줄 큐를 타지 않고 이벤트 즉시 실행되기 때문에 지연되지 않을 가능성이 높습니다.</p>
<h2 id="방법-3-항상-켜져-있는-컴퓨터로-옮겨서-직접-실행-pythonanywhere-라즈베리파이-등">방법 3. 항상 켜져 있는 컴퓨터로 옮겨서 직접 실행 (PythonAnywhere, 라즈베리파이 등)</h2>
<p>GitHub Actions를 떠나서, 항상 켜져 있는 컴퓨터에서 텔레그램 봇을 실행하는 방법입니다. 컴퓨터 운영체제에 내장된 예약 실행 기능(crontab)을 이용합니다.</p>
<p>저는 5시 정각에 가깝게 받고 싶어서 방법 1로는 부족했고, 그렇다고 라즈베리파이 같은 컴퓨터를 텔레그램 봇 하나 때문에 상시로 돌리는 건 과하다고 생각했습니다. 그래서 <code>방법 2(외부 cron 서비스에서 workflow_dispatch API 호출)</code>을 사용하기로 결정했습니다. 외부 서비스로는 Cloudflare Workers 무료 플랜을 사용하기로 했습니다.</p>
<h1 id="해결-cloudflare-worker의-cron-trigger로-workflow_dispatch-호출">해결: Cloudflare Worker의 Cron Trigger로 workflow_dispatch 호출</h1>
<p>전체 구조는 아래와 같습니다.</p>
<pre><code>Cloudflare Cron Trigger (UTC 20:00 = KST 05:00 정각 발화)
  → Worker의 scheduled 핸들러 실행
  → GitHub API request: POST .../workflows/daily-briefing.yml/dispatches
  → GitHub이 workflow_dispatch 이벤트로 run 즉시 생성 (스케줄 큐 우회)
  → briefing.py 실행 
  → 텔레그램 수신</code></pre><h2 id="github-fine-grained-pat-발급">GitHub Fine-grained PAT 발급</h2>
<p>외부에서 GitHub API로 워크플로를 깨우기 위해서는 토큰이 필요합니다.</p>
<p>Settings → Developer settings → Personal access tokens → Fine-grained tokens에서 토큰을 발급합니다.</p>
<ul>
<li>Repository access: Only select repositories(텔레그램 봇 저장소만)</li>
<li>Permissions: Repository permissions → Actions: Read and Write</li>
<li>Expiration: Custom(26.12.31)</li>
</ul>
<h2 id="github-actions-워크플로-수정">GitHub Actions 워크플로 수정</h2>
<p>워크플로 yml에서는 <code>schedule</code> 블록을 제거했습니다.</p>
<p>남겨두면 Cloudflare cron trigger로 1회 + GitHub Actions Schedule Event 1회 하루에 총 2번 브리핑이 오게 됩니다.</p>
<pre><code class="language-yaml"># .github/workflows/daily-briefing.yml
name: Daily briefing

on:
  workflow_dispatch:

#...</code></pre>
<h2 id="cloudflare-worker-설정">Cloudflare Worker 설정</h2>
<p><a href="https://dash.cloudflare.com/">dash.cloudflare.com</a>에서 Workers &amp; Pages → Create → Hello World 템플릿으로 Worker를 하나 만들고, Edit code로 들어가 기본 코드를 아래로 통째로 교체했습니다.</p>
<pre><code class="language-jsx">export default {
  async scheduled(event, env, ctx) {
    ctx.waitUntil(triggerBriefing(env));
  },
};

async function triggerBriefing(env) {
  const url = `https://api.github.com/repos/${env.GITHUB_OWNER}/${env.GITHUB_REPO}/actions/workflows/daily-briefing.yml/dispatches`;

  const res = await fetch(url, {
    method: &quot;POST&quot;,
    headers: {
      &quot;Authorization&quot;: `Bearer ${env.GITHUB_TOKEN}`,
      &quot;Accept&quot;: &quot;application/vnd.github+json&quot;,
      &quot;X-GitHub-Api-Version&quot;: &quot;2022-11-28&quot;,
      // GitHub API는 User-Agent 헤더가 없으면 403을 반환한다.
      // Workers의 fetch는 기본 User-Agent를 붙이지 않으므로 반드시 명시.
      &quot;User-Agent&quot;: &quot;daily-briefing-trigger&quot;,
      &quot;Content-Type&quot;: &quot;application/json&quot;,
    },
    body: JSON.stringify({ ref: &quot;main&quot; }),
  });

  // 성공 시 GitHub은 204 No Content를 반환
  if (res.status === 204) {
    console.log(&quot;workflow_dispatch OK&quot;);
    return;
  }

  // 실패하면 텔레그램으로 직접 알림을 보낸다
  const body = await res.text();
  console.error(`workflow_dispatch failed: HTTP ${res.status} - ${body}`);

  if (env.TELEGRAM_BOT_TOKEN &amp;&amp; env.TELEGRAM_CHAT_ID) {
    await fetch(
      `https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/sendMessage`,
      {
        method: &quot;POST&quot;,
        headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
        body: JSON.stringify({
          chat_id: env.TELEGRAM_CHAT_ID,
          text: `⚠️ 데일리 브리핑 트리거 실패\nHTTP ${res.status}\n${body.slice(0, 300)}`,
        }),
      }
    );
  }
}</code></pre>
<p>코드 배포 후 Settings → Variables and Secrets에서 변수를 등록했습니다. 변수는 <code>GITHUB_TOKEN</code>(Secret), <code>GITHUB_OWNER</code>, <code>GITHUB_REPO</code>, 그리고 실패 알림용 <code>TELEGRAM_BOT_TOKEN</code>(Secret), <code>TELEGRAM_CHAT_ID</code> 다섯 개입니다.</p>
<p>마지막으로 Settings → Triggers → Cron Triggers에 <code>0 20 * * *</code>를 등록했습니다.</p>
<h2 id="테스트-결과">테스트 결과</h2>
<p>UTC 20:00까지 기다릴 수는 없으니, cron을 잠시 <code>*/5 * * * *</code>(5분마다)로 바꿔서 테스트를 진행했습니다.</p>
<p>워크플로가 두 번 실행되고 브리핑 메시지까지 도착하는 것을 확인한 뒤, cron을 원래대로 돌려놓았습니다.</p>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/ba0c9b9e-ae66-4cda-b6b7-6d6b7688ee6c/image.png" alt="테스트용 Actions 워크플로 실행 결과"></p>
<h1 id="마무리">마무리</h1>
<p>아직 테스트까지만 해보고 실제로 새벽 5시에 일정 브리핑 메시지가 오는지 확인은 못했습니다. 이 글을 쓰면서 같이 고쳤기 때문입니다. 내일 아침에는 처음으로 제시간에 온 일정 브리핑을 받아볼 수 있을 것 같습니다. 결과는 추후에 덧붙이도록 하겠습니다.</p>
]]></description>
        </item>
        <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>
<h1 id="4-github-actions로-무료-스케줄-걸어놓기">4. GitHub Actions로 무료 스케줄 걸어놓기</h1>
<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>
<h1 id="마치며">마치며</h1>
<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>
    </channel>
</rss>