<?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 Apr 2026 14:47:26 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[『하루 30분 나는 제미나이로 돈을 번다』, 실제로 도움 되는 내용일까?]]></title>
            <link>https://velog.io/@soleil_lucy_75/gemini-ai-income-review</link>
            <guid>https://velog.io/@soleil_lucy_75/gemini-ai-income-review</guid>
            <pubDate>Sun, 26 Apr 2026 14:47:26 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>*&quot;한빛미디어 서평단 &lt;나는리뷰어다&gt; 활동을 위해서 책을 협찬받아 작성된 서평입니다.&quot;*</strong></p>
</blockquote>
<h1 id="책을-읽게-된-계기">책을 읽게 된 계기</h1>
<p>최근 생성형 AI를 활용해 부업을 시도하는 사례가 빠르게 증가하고 있다. 이러한 흐름 속에서, 단순한 호기심을 넘어 실제로 어떤 방식으로 수익이 만들어 지는지 확인하고자 『하루 30분 나는 제미나이로 돈을 번다』를 읽게 되었다. 특히 ‘하루 30분’이라는 시간 제약 속에서도 성과를 만들어낸다는 점이 인상적으로 다가왔다.</p>
<h1 id="프롬프트-기법에-대해-배우다">프롬프트 기법에 대해 배우다</h1>
<p>책을 읽기 전까지 나는 나름대로 AI를 잘 활용하고 있다고 생각했다. 예를 들어 개발 블로그 글의 제목과 목차를 추천받고 싶을 때, “시니어 프론트엔드 엔지니어”라는 페르소나를 부여하고, 내가 어떤 내용을 정리하려는지 설명하는 방식으로 프롬프트를 작성했다.</p>
<blockquote>
<p>책을 읽기 전 프롬프트</p>
<p>&quot;너는 시니어 프론트엔드 엔지니어야. 나는 Next.js v16 App Router를 공부하면서 Proxy.ts 파일에 대해 공식문서에서 읽고 배운 점을 개발 블로그에 정리하고자 해. 제목과 목차를 추천해줘.”</p>
</blockquote>
<p>하지만 책을 읽고 나니, 내가 작성하던 프롬프트는 전반적으로 추상적인 수준에 머물러 있었다는 것을 깨닫게 되었다. 역할과 목적은 전달했지만, 어떤 맥락에서 이 요청을 하게 되었는지, 어떤 형식과 톤으로 답변을 원하는지에 대한 정보는 빠져 있었기 때문이다. 그 결과 원하는 답변을 얻기까지 여러 번의 추가 질문을 반복해야 했다.</p>
<p>책에서는 <code>CO-STAR</code>, <code>CoT</code>, <code>ToT</code>, <code>최소-최대 프롬프트</code>, <code>산파술 프롬프트</code> 등 다양한 기법을 소개하는데, 그중에서도 CO-STAR 구조가 특히 인상적이었다. 맥락(Context), 목표(Objective), 스타일(Style), 톤(Tone), 대상(Audience), 응답(Response)을 명확히 설정하는 방식은, 단순히 질문을 던지는 것이 아니라 하나의 ‘요청서’를 작성하는 것에 가까웠다.</p>
<p>이 구조를 기준으로 보면, 기존의 나의 프롬프트는 페르소나와 목적 정도만 포함된 추상적인 형태였다. 반면 CO-STAR 방식으로 요청을 구성한다면, 불필요한 대화를 반복하지 않고도 원하는 결과를 얻을 수 있을 것이라는 확신이 들었다.</p>
<blockquote>
<p>CO-STAR 기법을 사용한 프롬프트</p>
<p>Context: Next.js v16 App Router를 학습하며 Proxy.ts 파일에 대한 공식 문서를 읽고 이해한 내용을 개발 블로그에 정리하려는 상황이다.</p>
<p>Objective: 개발 블로그 글의 제목과 목차를 추천받고 싶다.</p>
<p>Style: 기술 블로그 스타일로, 구조가 명확하고 흐름이 자연스럽게 이어지도록 구성</p>
<p>Tone: 사무적이고 설명 중심의 톤</p>
<p>Audience: Next.js를 학습 중인 주니어 개발자</p>
<p>Response: 3~5개의 제목 후보와, 각 제목에 맞는 목차를 마크다운 형식으로 제시</p>
</blockquote>
<h1 id="인사이트">인사이트</h1>
<p>AI를 활용한 수익화 방법이 궁금해 읽은 책이었지만, 실제로는 프롬프트 기법뿐만 아니라 나만의 경험을 콘텐츠로 수익화 하는 구조에 대해서도 생각해보는 계기가 되었다.</p>
<p>특히 마케팅과 홍보 전략에 대한 내용을 보면서, 강의 플랫폼에서 여러 강의를 묶어 판매하는 방식이나, 강의를 제공하는 사람들이 별도의 커뮤니티를 함께 운영하는 이유가 단순한 마케팅 전략이 아니라, 하나의 수익화 구조라는 점을 이해하게 되었다.</p>
<p>결국 많은 사람들이 자신의 경험과 지식을 단순히 공유하는 데 그치지 않고, 다른 사람에게 도움이 되는 형태로 가공하여 유료 콘텐츠로 전환하고 있었다.</p>
<p>나 역시 단순히 AI를 활용하는 수준에 머무르기보다, 지금까지의 경험을 정리하고 이를 다른 사람에게 전달 가능한 형태로 가공하는 시도를 해볼 필요가 있다고 판단했다. 우선 도움이 될 만한 경험부터 찾아봐야겠다!</p>
<h1 id="개인적인-적용-방향과-계획">개인적인 적용 방향과 계획</h1>
<p>앞으로는 AI를 사용할 때, 단순히 질문을 던지는 것이 아니라 구조화된 프롬프트를 작성하는 방식으로 접근해볼 계획이다. 특히 반복적으로 수행하는 작업을 중심으로 AI를 활용하는 경험을 쌓고자 한다.</p>
<p>구체적으로는 취업 준비 과정에서 반복되는 작업들을 자동화하는 방향을 고려하고 있다. 채용 공고를 탐색하고, 지원서를 작성하며, 기업에 맞춰 내용을 수정하는 과정은 많은 시간이 소요되는 과정에서 AI를 활용해서 효율적으로 처리할 수 있는 방법을 시도해보고자 한다.</p>
<p>취업 준비 기간이 길어지면서 집중도가 떨어지는 순간들이 반복되고 있는데, 채용 지원 루틴을 만들 수 있도록 AI가 일정 부분 도움을 줄 수 있을 것이라 기대하고 있다. 단순한 생산성 향상을 넘어, 지속적으로 행동할 수 있는 환경을 만드는 도구로 활용해보고자 한다.</p>
<h1 id="읽으며-기록한-내용">읽으며 기록한 내용</h1>
<h2 id="프롬프트-엔지니어링에-대해서">[프롬프트 엔지니어링에 대해서]</h2>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/0e788ebd-97f8-4963-8e57-6c963d820d14/image.png" alt="밑줄 긋기한 전자책"></p>
<h2 id="책에-나온-미션-1-수행해보기">[책에 나온 미션 1 수행해보기]</h2>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/47db93d2-b8be-4e8c-ad88-51f38c04d436/image.png" alt="책에 나온 미션 1 진행"></p>
<h2 id="책에-나온-미션-2-수행해보기">[책에 나온 미션 2 수행해보기]</h2>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/bd6aecd1-b664-4b72-a6dc-f216c9e1b6e2/image.png" alt="책에 나온 미션 2 진행"></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[페이지 접근마다 반복되는 로그인 체크, Next.js proxy.ts로 한 곳에서 관리하기]]></title>
            <link>https://velog.io/@soleil_lucy_75/nextjs-proxy-login-check-in-one-place</link>
            <guid>https://velog.io/@soleil_lucy_75/nextjs-proxy-login-check-in-one-place</guid>
            <pubDate>Sun, 12 Apr 2026 06:42:55 GMT</pubDate>
            <description><![CDATA[<h1 id="글을-쓰게-된-이유">글을 쓰게 된 이유</h1>
<p>프로젝트를 진행하다가 로그인 여부에 따라 페이지 접근을 제어하는 기능을 구현해야 했습니다. 처음에는 각 페이지 컴포넌트마다 로그인 체크 로직을 넣어야 하나 했는데, 보호해야 할 페이지가 여러 개이고 한 곳에서 관리하는 게 효율적이라는 생각이 들었습니다. 찾아보니 이런 공통 로직은 미들웨어로 한 곳에서 처리하는 게 일반적이었고, Next.js 16에서는 <code>proxy.ts</code> 파일이 그 역할을 한다는 것을 알게 되었습니다. 이 글은 그 과정에서 공부한 내용을 정리한 글입니다.</p>
<h1 id="문제-로그인-체크-로직-어디에-써야-할까">문제: 로그인 체크 로직, 어디에 써야 할까?</h1>
<p>제가 만든 서비스는 로그인한 사용자만 LLM 레시피 추출 기능을 사용하고, 마이페이지에서 레시피를 관리할 수 있도록 구현하고 싶었습니다. </p>
<p>로그인 여부를 체크해야 하는 페이지는 아래와 같습니다 — 사실상 서비스의 모든 페이지입니다.</p>
<ul>
<li>사용자가 추출하고 싶은 레시피 URL을 입력하는 메인 페이지</li>
<li>레시피 추출 결과를 확인하는 페이지</li>
<li>레시피를 단계별로 안내하는 요리 화면</li>
<li>마이페이지 — 요리 기록을 확인하는 페이지</li>
<li>마이페이지 — 요리 결과 통계 페이지</li>
<li>마이페이지 — 냉장고 재료 재고 관리 페이지</li>
<li>마이페이지 — 추출한 레시피 관리 페이지</li>
<li>마이페이지 — 개인 설정 페이지</li>
</ul>
<p>각 페이지 컴포넌트마다 로그인 체크 로직을 작성한다면 동일한 코드가 여러 곳에 흩어지고, 나중에 수정할 때도 모든 파일을 일일이 찾아가야 하는 문제가 생깁니다. 한 곳에서 관리할 수 있는 방법이 필요하다고 생각했습니다.</p>
<h1 id="해결-방안-미들웨어로-인증-체크를-한-곳에서-처리하기">해결 방안: 미들웨어로 인증 체크를 한 곳에서 처리하기</h1>
<p>한 곳에서 관리할 수 있는 방법을 찾다가 <code>미들웨어</code>라는 개념이 떠올랐습니다. <code>미들웨어</code>란 요청이 실제로 처리되기 전에 실행되는 중간 레이어입니다. 모든 페이지 요청이 미들웨어를 거쳐서 들어오기 때문에, 이 영역에서 로그인 여부를 체크하는 로직을 관리하면 좋겠다는 생각을 했습니다.</p>
<p>그래서 Next.js에서는 미들웨어를 어떻게 작성하는지 공식 문서를 찾아봤습니다.</p>
<p><a href="https://nextjs.org/docs/app/getting-started/proxy">Proxy | Next.js 공식 문서</a></p>
<h2 id="proxyts란">proxy.ts란?</h2>
<p>Next.js 16부터 기존의 <code>middleware.ts</code>는 <code>proxy.ts</code>로 이름이 바뀌었습니다. 기능은 동일하고, 이름이 역할을 더 잘 반영하도록 변경된 것입니다. <code>proxy.ts</code>는 요청이 완료되기 전에 코드를 실행할 수 있게 해주는 파일로, 요청을 가로채서 리다이렉트, 리라이트, 헤더 수정 등을 처리할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/df28c7ac-e904-4c4f-83ab-76ad9d5bf1ba/image.png" alt="proxy.ts 설명 시퀀스 다이어그램"></p>
<h2 id="proxyts를-사용하는-경우">proxy.ts를 사용하는 경우</h2>
<p>공식 문서에서는 아래와 같은 상황에서 사용을 권장합니다.</p>
<ul>
<li>모든 페이지 또는 일부 페이지의 헤더를 수정해야 할 때</li>
<li>A/B 테스트처럼 사용자 그룹에 따라 다른 페이지를 보여줘야 할 때</li>
<li>요청 정보를 기반으로 프로그래밍 방식의 리다이렉트가 필요할 때</li>
</ul>
<h2 id="proxyts-사용을-지양해야-하는-경우">proxy.ts 사용을 지양해야 하는 경우</h2>
<p><code>proxy.ts</code>는 모든 요청마다 실행되기 때문에 여기서 무거운 작업을 하면 전체 페이지 로딩 성능에 영향을 줍니다. 공식 문서에서 명시적으로 금지하는 경우는 아래와 같습니다.</p>
<ul>
<li>DB 조회나 외부 API 호출 같은 느린 데이터 페칭</li>
<li>완전한 세션 관리나 인증 솔루션으로의 사용</li>
<li>fetch에서 cache, revalidate, tags 옵션 사용</li>
</ul>
<p>복잡한 로직은 API Routes나 Server Component에서 처리하는 것이 적절합니다.</p>
<h2 id="proxyts에-로그인-체크-로직을-구현해도-될까">proxy.ts에 로그인 체크 로직을 구현해도 될까?</h2>
<p>제가 구현하려는 것은 쿠키에 저장된 토큰의 존재 여부만 확인하는 가벼운 체크입니다. DB 조회나 외부 API 호출 없이 빠르게 판단할 수 있기 때문에 적절한 케이스라고 판단해 <code>proxy.ts</code>에 로그인 체크 로직을 구현하기로 결정했습니다.</p>
<h1 id="결과">결과</h1>
<p>아래는 실제 프로젝트에 적용한 <code>proxy.ts</code> 코드입니다.
<a href="https://github.com/hyer0705/ACCIO-RECIPE/blob/main/src/proxy.ts">실제 코드 보러가기</a></p>
<pre><code class="language-tsx">import { withAuth } from &#39;next-auth/middleware&#39;;
import { NextResponse } from &#39;next/server&#39;;

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

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

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

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

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

let currentController = null;
let requestId = 0;

// ...

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

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

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

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

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

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

    const data = await response.json();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    xhr.send();
  });
}

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

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

// =====

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

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

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

const user = await fetchUser(123);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    isSubmittingRef.current = true;

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

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

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

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

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

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

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

    console.log(&quot;처리할 아이템이 없습니다.&quot;);
}</code></pre>
<h3 id="조건부-렌더링">조건부 렌더링</h3>
<pre><code class="language-jsx">function UserProfile({ user }) {
    return (
        &lt;div&gt;
            {user &amp;&amp; &lt;h1&gt;{user.name}&lt;/h1&gt;}
            {user?.email &amp;&amp; &lt;p&gt;Email: {user.email}&lt;/p&gt;}
        &lt;/div&gt;
    );
}</code></pre>
<h1 id="참고-자료">참고 자료</h1>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Glossary/Falsy">Falsy | MDN Docs</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Glossary/Truthy">Truthy | MDN Docs</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[10월] 밑바닥부터 시작하는 웹 브라우저]]></title>
            <link>https://velog.io/@soleil_lucy_75/10%EC%9B%94-%EB%B0%91%EB%B0%94%EB%8B%A5%EB%B6%80%ED%84%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-%EC%9B%B9-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80</link>
            <guid>https://velog.io/@soleil_lucy_75/10%EC%9B%94-%EB%B0%91%EB%B0%94%EB%8B%A5%EB%B6%80%ED%84%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-%EC%9B%B9-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80</guid>
            <pubDate>Sun, 26 Oct 2025 14:06:10 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/099100fb-3dff-4448-bd4b-40b893147f82.jpg" alt=""></p>
<ul>
<li>책 제목: <a href="https://www.hanbit.co.kr/store/books/look.php?p_code=B6818199506">밑바닥부터 시작하는 웹 브라우저</a></li>
<li>저자: 파벨 판체카 , 크리스 해럴슨</li>
<li>번역: 이형욱 , 최병운</li>
<li>출간: 2025-09-15</li>
</ul>
<h1 id="책을-읽고나서">책을 읽고나서</h1>
<p>이 책을 통해 브라우저를 직접 구현해보며, 그동안 이론으로 대충 알고 있던 <strong>브라우저의 동작 원리</strong>를 이해할 수 있었습니다.</p>
<blockquote>
<p>Q. 브라우저 렌더링 원리에 대해 설명해주세요.</p>
</blockquote>
<p>개발자 면접에서 자주 듣게 되는 질문입니다. 저 역시 이 답변을 달달 외우며 공부했던 기억이 납니다.</p>
<p>이 책은 이론을 단순히 설명하기 보다는, 직접 브라우저를 만드는 실습을 통해 이론을 이해하도록 돕습니다.</p>
<p>HTML 문서를 파싱해 트리로 구성하고, 그 트리를 기반으로 width, height, 브라우저 내 위치, 폰트 스타일, 속성 등을 계산하는 일련의 과정을 직접 구현해보게 됩니다. 정말 제목처럼 ‘밑바닥부터’ 브라우저가 화면을 만들어내는 전 과정을 따라갑니다.</p>
<p>각 챕터의 끝에는 연습문제가 수록되어 있어, 이를 직접 풀다 보면 마치 브라우저 엔지니어가 된 듯한 기분이 듭니다.</p>
<p>실습 언어는 파이썬이라 코드 읽기가 어렵지 않았습니다. 실습을 하다 보니 “언젠가 크로미움 같은 브라우저 오픈소스도 읽어보고 싶다”는 생각이 들었습니다. 단, C++로 되어 있어, 언어를 공부한 뒤 도전해볼 계획입니다.</p>
<h1 id="책-한-줄로-표현하기">책 한 줄로 표현하기</h1>
<p>브라우저를 직접 구현하며 브라우저의 원리를 이해시켜 주는 책!</p>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://github.com/browserengineering/book">밑바닥부터 시작하는 웹 브라우저 예제 코드(원서) - browserengineering/book</a></li>
<li><a href="https://github.com/bunhere/Web-Browser-Engineering">밑바닥부터 시작하는 웹 브라우저 예제 코드(번역) - Web-Browser-Engineering</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Protocol] LSP, Language Server Protocol: IDE와 언어 서버가 대화할 때 쓰는 약속]]></title>
            <link>https://velog.io/@soleil_lucy_75/LSP-Language-Server-Protocol-IDE%EC%99%80-%EC%96%B8%EC%96%B4-%EC%84%9C%EB%B2%84%EA%B0%80-%EB%8C%80%ED%99%94%ED%95%A0-%EB%95%8C-%EC%93%B0%EB%8A%94-%EC%95%BD%EC%86%8D</link>
            <guid>https://velog.io/@soleil_lucy_75/LSP-Language-Server-Protocol-IDE%EC%99%80-%EC%96%B8%EC%96%B4-%EC%84%9C%EB%B2%84%EA%B0%80-%EB%8C%80%ED%99%94%ED%95%A0-%EB%95%8C-%EC%93%B0%EB%8A%94-%EC%95%BD%EC%86%8D</guid>
            <pubDate>Sat, 25 Oct 2025 12:35:35 GMT</pubDate>
            <description><![CDATA[<p>VS Code를 사용하여 열심히 개발하던 당신은 이미 입력된 변수가 자동으로 입력되는 기능이 있었으면 좋겠다고 생각하게 됩니다. 함수명을 클릭하면 정의된 위치로 바로 이동하고, 작성 중인 코드에서 타입 오류가 발생하면 IDE가 빨간 밑줄로 표시해 주면 편리하겠다고 느낍니다.</p>
<p>그래서 당신은 이러한 기능들을 직접 IDE에 맞춰 구현하여 사용하기 시작합니다. 하지만 IDE를 WebStorm으로 바꾸자, VS Code에서 잘 쓰던 기능들을 다시 만들어야 하는 상황이 생겼습니다. IDE마다 API가 다르기 때문에 같은 기능을 반복 구현해야 했던 것입니다.</p>
<p>“한 번만 구현해도 여러 IDE에서 쓸 수 있는 방법은 없을까?” 하는 고민 끝에 등장한 것이 바로 <code>LSP(Language Server Protocol)</code>입니다. IDE와 언어 서버가 통신하는 표준 규약을 정의하여, 한 번 구현한 기능을 다양한 IDE에서 재사용할 수 있도록 해줍니다.</p>
<h1 id="what-lsp란-무엇일까">What: LSP란 무엇일까?</h1>
<h2 id="언어-서버">언어 서버</h2>
<p>언어별 스마트 기능을 제공하며, 프로세스 간 통신을 가능하게 하는 프로토콜을 통해 개발 도구와 연결되는 서버입니다. IDE나 다른 도구는 언어 서버와 통신하여 자동 완성, 정의로 이동, 문서 보기 같은 기능을 사용할 수 있습니다.</p>
<h2 id="lsp-language-server-protocol">LSP, Language Server Protocol</h2>
<p>도구와 서버가 통신하는 방식을 표준화한 프로토콜입니다. 이를 통해 단일 언어 서버를 여러 개발 도구에서 재사용할 수 있고, 도구가 최소한의 노력으로 다양한 언어를 지원할 수 있게 합니다.</p>
<h1 id="why-왜-필요했을까">Why: 왜 필요했을까?</h1>
<h2 id="ide-별로-동일한-언어-기능을-여러-번-만들어야-했던-불편함을-줄이기-위해">IDE 별로 동일한 언어 기능을 여러 번 만들어야 했던 불편함을 줄이기 위해</h2>
<p>자동 완성, 정의로 이동, 문서 보기 같은 언어별 기능을 구현하는 일은 많은 노력과 시간이 필요합니다. 과거에는 IDE마다 제공하는 API가 달라, 동일한 기능을 여러 IDE에서 반복해서 만들어야 했습니다.</p>
<p>LSP는 이러한 반복 작업을 줄이기 위해 등장했습니다. 언어 서버를 IDE에서 분리하고, 표준화된 프로토콜에 맞춰 개발하면 한 번 만든 언어 서버를 여러 IDE에서 그대로 재사용할 수 있습니다. 덕분에 개발자는 IDE마다 기능을 다시 구현할 필요가 없고, 다양한 도구에서도 일관된 언어 기능을 쉽게 제공할 수 있게 되었습니다.</p>
<h1 id="how-어떻게-동작하는가">How: 어떻게 동작하는가?</h1>
<h2 id="언어-서버와-ide의-통신-방법-json-rpc">언어 서버와 IDE의 통신 방법: JSON-RPC</h2>
<p>언어 서버는 별도의 프로세스로 실행되며, 개발 도구는 JSON-RPC를 통해 언어 프로토콜을 사용하여 서버와 통신합니다.</p>
<h3 id="예시-일상적인-편집-세션에서의-ide와-언어-서버-통신">예시: 일상적인 편집 세션에서의 IDE와 언어 서버 통신</h3>
<p><img src="https://microsoft.github.io/language-server-protocol/overviews/lsp/img/language-server-sequence.png" alt=""></p>
<ul>
<li>파일 열기(<code>textDocument/didOpen</code>)<ul>
<li>IDE는 문서가 열렸음을 언어 서버에 알립니다.</li>
<li>이제 문서 내용은 파일 시스템이 아닌 IDE 메모리에서 관리되며, IDE와 언어 서버가 동기화합니다.</li>
</ul>
</li>
<li>파일 편집(<code>textDocument/didChange</code>)<ul>
<li>IDE가 변경 사항을 언어 서버에 전달하면, 서버는 문서 내용을 업데이트하고 분석합니다.</li>
<li>분석 결과 오류나 경고가 있으면 IDE에 전달됩니다.</li>
</ul>
</li>
<li>‘Go to Definition’(<code>textDocument/definition</code>)<ul>
<li>IDE는 문서 URI와 커서 위치를 포함한 요청을 서버로 보냅니다.</li>
<li>서버는 해당 변수나 함수가 정의된 위치와 문서 URI를 응답합니다.</li>
</ul>
</li>
<li>파일 닫기(<code>textDocment/didClose</code>)<ul>
<li>IDE에서 알림을 보내면, 언어 서버는 문서가 더 이상 메모리에 없음을 인지합니다.</li>
</ul>
</li>
</ul>
<h2 id="json-rpc-예시">JSON-RPC 예시</h2>
<h3 id="request">Request</h3>
<pre><code class="language-json">{
    &quot;jsonrpc&quot;: &quot;2.0&quot;,
    &quot;id&quot; : 1,
    &quot;method&quot;: &quot;textDocument/definition&quot;,
    &quot;params&quot;: {
        &quot;textDocument&quot;: {
            &quot;uri&quot;: &quot;file:///p%3A/mseng/VSCode/Playgrounds/cpp/use.cpp&quot;
        },
        &quot;position&quot;: {
            &quot;line&quot;: 3,
            &quot;character&quot;: 12
        }
    }
}</code></pre>
<h3 id="response">Response</h3>
<pre><code class="language-json">{
    &quot;jsonrpc&quot;: &quot;2.0&quot;,
    &quot;id&quot;: 1,
    &quot;result&quot;: {
        &quot;uri&quot;: &quot;file:///p%3A/mseng/VSCode/Playgrounds/cpp/provide.cpp&quot;,
        &quot;range&quot;: {
            &quot;start&quot;: {
                &quot;line&quot;: 0,
                &quot;character&quot;: 4
            },
            &quot;end&quot;: {
                &quot;line&quot;: 0,
                &quot;character&quot;: 11
            }
        }
    }
}</code></pre>
<h1 id="where-어디서-쓰이고-있을까">Where: 어디서 쓰이고 있을까?</h1>
<h2 id="다양한-ide와-코드-편집기">다양한 IDE와 코드 편집기</h2>
<p>각 IDE에서 자동 완성, 정의로 이동, 오류 표시 등 언어 기능을 지원할 때 LSP가 활용됩니다.</p>
<ul>
<li>활용 사례: VS Code, IntelliJ IDEs(PyCharm, WebStorm, IntelliJ IDEA 등), Eclipse, Neovim 등</li>
</ul>
<h2 id="온라인-ide">온라인 IDE</h2>
<p>웹 환경에서도 데스크톱 IDE처럼 언어 기능을 제공하기 위해 LSP를 사용합니다.</p>
<ul>
<li>활용 사례: GitHub Codespaces, Replit 등</li>
</ul>
<h3 id="github-codespaces-예시">GitHub Codespaces 예시</h3>
<p><img src="https://docs.github.com/assets/cb-355846/mw-1440/images/help/codespaces/codespace-overview-annotated.webp" alt=""></p>
<h3 id="replit-예시">Replit 예시</h3>
<p><img src="https://velog.velcdn.com/images/soleil_lucy_75/post/a0c0086e-e56b-4eb0-a7c0-138f9e35acef/image.png" alt=""></p>
<h2 id="ai-코드-도구">AI 코드 도구</h2>
<p>코드의 의미와 구조를 분석하고, 토큰 사용량을 줄이며 효율적으로 코드를 처리할 때 LSP 구조를 참고하거나 활용합니다.</p>
<ul>
<li>활용 사례: Serena MCP 등</li>
</ul>
<h1 id="회고-lsp를-알아보고-나서">회고: LSP를 알아보고 나서</h1>
<p>책에서 Serena MCP를 읽다가 LSP 개념을 처음 접했습니다. 글에서 IDE에서 자동 완성, 정의로 이동, 타입 오류 표시 같은 기능이 가능하게 된 배경으로 LSP가 소개된 것을 보고 흥미가 생겨 이번 글을 작성하게 됐습니다.</p>
<p>평소 VS Code를 사용하면서 변수나 함수를 자동 완성하고, 함수 정의로 바로 이동하며, TypeScript 코드에서 타입 오류를 확인하는 기능이 매우 편리하다고 느꼈습니다. 그런데 이런 기능들이 어떻게 구현되는지 궁금했는데, LSP를 공부 하면서 그 궁금증이 해소되었습니다.</p>
<p>마이크로소프트가 만든 LSP는 IDE마다 언어 기능을 반복해서 구현해야 했던 불편함을 해결하기 위해 등장했습니다. 언어 서버에 기능을 구현하고 IDE와 언어 서버가 JSON-RPC로 통신하도록 표준을 정의함으로써, 한 번 구현한 기능을 여러 IDE에서 재사용할 수 있게 한 것입니다. 즉, LSP는 기능 자체를 제공하는 것이 아니라, <strong>기능을 IDE와 독립적으로 구현하고 재사용할 수 있도록 돕는 <code>표준</code></strong>입니다.</p>
<p>LSP를 공부하며 ‘표준’은 중요하다는 생각이 들었습니다. LSP처럼 명확한 규칙과 약속이 있으면 반복 작업을 줄일 수 있고, 개발 생산성이 높아집니다. 마치 팀 단위로 코드 컨벤션을 정해두는 것처럼, 코드 작성 ‘표준’을 정해두면 서로의 코드를 빠르고 편하게 이해할 수 있어 개발 과정에서 불필요한 혼란을 줄일 수 있다고 생각합니다.</p>
<p>‘표준’은 반복 작업의 불편함을 줄이고, 생산성을 높여 준다고 생각이 듭니다.</p>
<h1 id="참고-자료">참고 자료</h1>
<ul>
<li><a href="https://microsoft.github.io/language-server-protocol/">LSP | Microsoft</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docs] ADR(Architecture Decision Record), 팀의 기술 의사결정 과정을 기록하는 문서]]></title>
            <link>https://velog.io/@soleil_lucy_75/ADRArchitecture-Decision-Record-%ED%8C%80%EC%9D%98-%EA%B8%B0%EC%88%A0-%EC%9D%98%EC%82%AC%EA%B2%B0%EC%A0%95-%EA%B3%BC%EC%A0%95%EC%9D%84-%EA%B8%B0%EB%A1%9D%ED%95%98%EB%8A%94-%EB%AC%B8%EC%84%9C</link>
            <guid>https://velog.io/@soleil_lucy_75/ADRArchitecture-Decision-Record-%ED%8C%80%EC%9D%98-%EA%B8%B0%EC%88%A0-%EC%9D%98%EC%82%AC%EA%B2%B0%EC%A0%95-%EA%B3%BC%EC%A0%95%EC%9D%84-%EA%B8%B0%EB%A1%9D%ED%95%98%EB%8A%94-%EB%AC%B8%EC%84%9C</guid>
            <pubDate>Sun, 19 Oct 2025 07:31:33 GMT</pubDate>
            <description><![CDATA[<p>어느 날, 당신은 사이드 프로젝트로 ‘하지 말아야 할 일들을 정리하는 애플리케이션’을 만들고 싶다고 가정해봅시다.</p>
<p>프론트엔드 기술만 사용한다고 했을 때, 자연스레 이런 고민이 떠오릅니다.</p>
<ul>
<li>웹으로 만든다면 Vanilla JS로 시작할까, 아니면 React, Vue, Svelte 중 하나를 쓸까?</li>
<li>스타일링은 CSS로 직접 작성할까, Tailwind나 styled-components를 도입할까?</li>
<li>상태 관리는 또 어떤 라이브러리를 선택해야 할까?</li>
</ul>
<p>이처럼 우리는 프로젝트를 시작하거나, 진행하면서 마주치는 문제를 해결하기 위해 수많은 기술적 의사결정(Decision) 을 내리게 됩니다.</p>
<p>그런데 시간이 지나면 그 당시 어떤 이유로 그 결정을 내렸는지 잘 기억나지 않을 때가 많습니다.</p>
<p>예를 들어, 혼자 하던 프로젝트가 커져서 이제 둘이서 함께하게 되었다고 해봅시다.</p>
<p>새로 합류한 팀원이 묻습니다.</p>
<p>“왜 React만 쓴 건지 궁금해요. TypeScript도 같이 쓰면 좋지 않았을까요? 프로젝트 시작할 때 TypeScript를 사용하지 않은 이유가 있나요?”</p>
<p>그때, 정작 그 이유를 제대로 설명하지 못할 수도 있습니다. 이런 상황이 와도 당황하지 않도록 도와주는 문서가 있다면 어떨까요?</p>
<p>그게 바로 ADR(Architecture Decision Record) 입니다.</p>
<p>ADR은 프로젝트에서 내린 <code>기술적 의사결정의 배경과 이유, 대안, 결과를 기록하는 문서</code>입니다.</p>
<p>예를 들어,</p>
<ul>
<li>기존 기술을 교체하려는 제안이 있을 때, 과거의 선택 배경을 근거로 토론할 수 있고,</li>
<li>새 팀원이 합류했을 때 프로젝트의 기술 선택 이유를 빠르게 이해할 수 있으며,</li>
<li>다른 팀(예: 프론트엔드와 백엔드)이 공통 기술을 선택할 때 결정 과정을 투명하게 공유할 수도 있습니다.</li>
</ul>
<p>이 글은 『한 걸음 앞선 개발자가 지금 꼭 알아야 할 클린 코드』를 읽다가 <code>ADR</code>이라는 용어가 궁금해진 사람이 정리한 글입니다.</p>
<h1 id="what-adrarchitecture-decision-record란">What: ADR(Architecture Decision Record)란?</h1>
<p>ADR은 Architecture Decision Record 혹은 An Architectural Decision Record의 약자로, ‘프로젝트 내 기술·아키텍처 관련 중요한 의사결정을 기록하는 문서’를 의미합니다.</p>
<p>주로 특정 기술이나 설계를 왜, 어떤 근거로 선택했는지를 기록할 때 작성하며, 프로젝트의 방향성이나 기술 스택이 바뀌더라도 결정의 맥락(Context)을 잃지 않도록 돕습니다.</p>
<p>Michael Nygard가 말하는 애자일 환경에서의 ADR:</p>
<blockquote>
<p>“애자일 프로젝트에서는 모든 결정을 한 번에 내리지도, 프로젝트 시작 시 모두 결정되지도 않습니다.
문서화를 반대하는 것이 아니라, 가치 없는 문서를 반대하는 것입니다.
팀이 스스로 활용할 수 있는 문서는 가치가 있으며, 작고 모듈화된 문서라면 업데이트될 가능성이 있습니다.
큰 문서는 절대 최신 상태를 유지하지 못하고, 대부분의 개발자는 읽지조차 않습니다.”</p>
</blockquote>
<p>즉, ADR은 작고 독립적인 기록 단위로, 프로젝트 진행 중 언제든 참고할 수 있는 의사결정 맥락과 배경을 담은 문서라는 점에서 의미가 있습니다.</p>
<p>Nygard가 강조한 ADR 핵심 사항:</p>
<blockquote>
<p>“프로젝트 진행 중 가장 추적하기 어려운 것은 과거 결정의 동기입니다.
새로운 팀원은 이유를 모른 채 그 결정을 맹목적으로 따르거나,
이해 없이 변경할 수밖에 없습니다. 이런 ‘맹목적 수용’과 ‘맹목적 변경’을 피하는 것이 중요합니다.”</p>
</blockquote>
<p>ADR은 바로 이런 결정의 이유와 맥락을 기록하여, 팀이 잘못된 방향으로 움직이는 위험을 줄이고 과거 결정을 이해할 수 있도록 돕는 문서입니다.</p>
<h1 id="why-쓰는-이유">Why: 쓰는 이유?</h1>
<h2 id="기억-보존">기억 보존</h2>
<p>프로젝트에서 기술, 구조, 도구를 채택하거나 변경할 때의 이유를 기록하여, 나중에 왜 그렇게 결정했는지 쉽게 확인할 수 있습니다.
Michael Nygard가 지적했듯, 시간이 지나면 의사결정의 배경을 잊어버리는 것이 가장 큰 문제입니다. 이유를 모른 채 결정을 그대로 따르거나, 이유도 모르고 변경하는 상황을 방지할 수 있습니다.</p>
<h2 id="온보딩-효율화">온보딩 효율화</h2>
<p>새로 합류한 팀원이 과거 결정의 맥락을 빠르게 이해하도록 돕습니다.
기록된 ADR을 통해 팀원은 “왜 이 기술을 선택했는가?”를 바로 파악할 수 있어, 혼란이나 반복 질문을 줄일 수 있습니다.</p>
<h2 id="팀-간-소통">팀 간 소통</h2>
<p>외부 팀이나 다른 개발자에게 결정 이유를 문서로 증명할 수 있습니다.
이는 기술 트레이드오프나 구조적 선택을 설명할 때 설득력을 높입니다.</p>
<h2 id="변경-용이성">변경 용이성</h2>
<p>기술을 교체하거나 구조를 변경할 때, 과거 결정의 배경과 맥락을 참고해 합리적인 판단을 내릴 수 있습니다.
Nygard는 “결정의 이유와 영향을 문서화하면, 무턱대고 받아들이거나 무턱대고 바꾸는 위험을 줄일 수 있다”고 강조합니다.</p>
<h1 id="when-언제-쓰면-좋을까">When: 언제 쓰면 좋을까?</h1>
<p>ADR을 모든 결정에 쓸 필요는 없습니다.</p>
<p>하지만 다음과 같은 순간이라면 ADR을 남겨두는 게 좋습니다:</p>
<ul>
<li>새로운 프레임워크나 라이브러리를 도입할 때 (ex. React vs Vue, Redux vs Zustand)</li>
<li>아키텍처 구조를 변경할 때 (ex. Monolith → Microservice)</li>
<li>기술적 트레이드오프가 존재할 때 (ex. 성능 vs 유지보수성)</li>
<li>팀 내에서 “이 결정, 왜 이렇게 됐지?”라는 논의가 길게 이어졌던 결정이 있을 때</li>
</ul>
<h1 id="how-어떻게-작성하면-좋을까">How: 어떻게 작성하면 좋을까?</h1>
<p>ADR은 복잡할 필요가 없습니다. ‘상황-선택-이유-결과’만 정리해도 충분합니다.</p>
<p>중요한 건 문서 형식보다 “결정의 이유를 남기는 습관” 입니다.</p>
<pre><code class="language-plaintext">// ADR_Template.md
# Title
## Date
## Context (상황)
## Decision (결정)
## Alternatives (대안)
## Status (현재 상태)
## Consequences (결과)</code></pre>
<h1 id="회고-adr-문서에-대해-공부하고나서">회고: ADR 문서에 대해 공부하고나서</h1>
<p>『한 걸음 앞선 개발자가 지금 꼭 알아야 할 클로드 코드』 책을 읽던 중, 프로젝트에 대해 작성하는 문서로 README와 함께 ADR이 소개되어 궁금해져서 개인적으로 알아보고 이렇게 글로 작성하게 되었습니다.</p>
<p>ADR은 새로운 기술을 도입하거나 아키텍처 구조를 변경하는 등, 프로젝트에 큰 변화가 있을 때 그 결정의 과정을 기록하는 문서라고 합니다. 빅테크 기업에서도 많이 사용하는 것으로 보여 흥미로웠습니다.</p>
<p>꼭 팀 프로젝트가 아니더라도 개인 프로젝트에서도 충분히 도움이 될 것 같다는 생각이 들었습니다.</p>
<p>개인적으로 프로젝트를 하다 보면 “내가 이 기술을 왜 선택했지?”라는 질문을 할 때가 종종 있는데, 정작 그 이유를 잘 떠올리지 못할 때가 많았습니다.</p>
<p>이번에 ADR 문서에 대해 알아보면서, 이런 고민을 줄이는 가장 좋은 방법이 ‘기록’이라는 걸 깨달았습니다.</p>
<p>앞으로는 개인 프로젝트라도 중요한 기술 결정을 내릴 때 ADR로 남겨보려 합니다.</p>
<h1 id="appendix">APPENDIX</h1>
<ul>
<li><a href="https://www.cognitect.com/blog/2011/11/15/documenting-architecture-decisions">DOCUMENTING ARCHITECTURE DECISIONS</a></li>
<li><a href="https://adr.github.io/">adr.github.io</a></li>
<li><a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/architectural-decision-records/adr-process.html">ADR process | AWS Docs</a></li>
<li><a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/architectural-decision-records/appendix.html">Appendix: Example ADR | AWS Docs</a></li>
<li><a href="https://learn.microsoft.com/en-us/azure/well-architected/architect-role/architecture-decision-record">Architecture decision record | Microsoft Learn</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA['향로' 와 함께하는 추석 완강 챌린지 회고]]></title>
            <link>https://velog.io/@soleil_lucy_75/%ED%96%A5%EB%A1%9C-%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%98%EB%8A%94-%EC%B6%94%EC%84%9D-%EC%99%84%EA%B0%95-%EC%B1%8C%EB%A6%B0%EC%A7%80-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@soleil_lucy_75/%ED%96%A5%EB%A1%9C-%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%98%EB%8A%94-%EC%B6%94%EC%84%9D-%EC%99%84%EA%B0%95-%EC%B1%8C%EB%A6%B0%EC%A7%80-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sat, 11 Oct 2025 14:54:36 GMT</pubDate>
            <description><![CDATA[<h1 id="챌린지-소개">챌린지 소개</h1>
<p><code>&#39;향로&#39;와 함께하는 추석 완강 챌린지</code>는 긴 추석 연휴동안 인프런 강의를 수강하며 매일 꾸준히 공부하는 습관을 만들고 성취감을 쌓는 학습 이벤트입니다.</p>
<p>연휴를 단순한 휴식이 아닌 성장의 시간으로 보내고 싶은 학습자들이, 인프랩 CTO인 향로님의 응원과 함께 완강 또는 꾸준히 학습하는 습관을 만들기에 도전하는 챌린지입니다.</p>
<h1 id="참여-동기">참여 동기</h1>
<ul>
<li>연휴가 끝나는 주 주말에 보는 기업 코딩테스트를 준비하기 위함</li>
<li>긴 연휴 동안 매일 공부하는 습관을 만들기 위함</li>
<li>챌린지 참여자에게 제공된 강의 40% 할인 쿠폰에 유혹 당함</li>
</ul>
<h1 id="수강한-강의-소개">수강한 강의 소개</h1>
<p>챌린지를 함께할 강의로 「<a href="https://inf.run/AMcxc">눈떠보니 코딩테스트 전날</a>」을 선택했습니다.</p>
<p>선택한 이유는 다음과 같습니다.</p>
<ul>
<li>코딩테스트가 2주도 남지 않은 시점이라, 강의 제목이 마치 내 상황을 그대로 표현한 듯해 끌림</li>
<li>JavaScript 기반 코딩테스트 강의 중에서 가격이 부담스럽지 않음</li>
</ul>
<h1 id="참여-과정과-배운-점">참여 과정과 배운 점</h1>
<ul>
<li>강제로라도 공부하는 환경 마련<ul>
<li>공부하기 싫은 날에도, 챌린지 참여자들과 향로님의 라이브 방송 덕분에 자극을 받아서 최소 1시간이라도 공부를 이어갈 수 있었습니다.</li>
</ul>
</li>
<li>알고리즘과 자료구조 복습<ul>
<li>강의를 통해 코딩테스트에 자주 사용되는 자료구조와 알고리즘을 정리할 수 있었습니다.<ul>
<li>자료구조: Array, Map, Set, Stack, Queue, 트리 등</li>
<li>알고리즘: 재귀, BFS, DFS, 트리 순회(전위, 중위, 후위) 등</li>
</ul>
</li>
</ul>
</li>
<li>기출 문제 풀이 및 생성형 AI 활용 학습<ul>
<li>프로그래머스에서 기출문제를 풀며 감각을 익혔습니다.</li>
<li>클로드의 학습 스타일 답변을 활용해 문제 풀이하는 방법을 배울 수 있었습니다.</li>
<li>클로드의 코딩테스트 문제 풀이 루틴:<ol>
<li>문제 읽고 핵심 파악<ul>
<li>입력/출력</li>
<li>제약 조건</li>
<li>요구 동작</li>
</ul>
</li>
<li>접근법 설계<ul>
<li>풀이 아이디어 도출</li>
<li>시간복잡도 고려</li>
<li>자료구조 선택</li>
</ul>
</li>
<li>구현<ul>
<li>직접 코딩</li>
<li>막히면 힌트 받기</li>
</ul>
</li>
<li>회고<ul>
<li>핵심 내용 정리</li>
<li>배운 점 기록</li>
</ul>
</li>
</ol>
</li>
</ul>
</li>
</ul>
<h1 id="챌린지-후기">챌린지 후기</h1>
<p>이번 챌린지는 사실 큰 기대 없이, 단순히 <strong>공부 습관을 만들고 연휴를 의미 있게 보내고 싶은 마음</strong>으로 참여했습니다. 하지만 예상보다 훨씬 <strong>제 성장과 인생에 도움이 되는 경험</strong>이 되었습니다.</p>
<p>챌린지 도중 진행된 <strong>향로님의 라이브</strong>는 큰 도움이 되었습니다. 포기하지 않고 끝까지 참여하도록 자극을 주었을 뿐만 아니라, 공부를 해도 당장 결과가 나타나지 않을 수 있지만, <strong>그 과정 자체가 언젠가는 의미가 있다는 믿음</strong>을 주어 연휴 동안 공부하길 잘했다는 마음을 갖게 해주었습니다.</p>
<p>라이브에서 추천해주신 책과 책 속 문장, 그리고 해주신 말씀도 인상 깊었습니다.</p>
<blockquote>
<p>나와 말이 안 통하는 사람, 내 말에 토를 다는 사람, 나를 기분 나쁘게 하는 사람을 만나는 건 정말 짜증 나는 일이다.
심지어 그런 사람들과 매일 얼굴을 맞대고 일까지 해야 하다니, 그건 얼마나 큰 고통인가. 하지만 그들이 없다면 내가 어떤 사람인지 알 수 있을까? 인간의 개성은 타인과 내가 부딪치는 경계에서 마찰흔처럼 드러난다.
자기만의 방에 갇힌 채 내 좁은 시야 안에 들어오는 것들만을 세상의 전부로 여기지 않기 위하여, 내 인생만 망했다는 착각에서 헤어나기 위하여, 자기 자신을 있는 그대로 받아들이기 위하여 우리는 오늘도 문을 열고 타인과 지지고 볶는 삶을 향해 한 발을 내딛는 것이다.</p>
</blockquote>
<ul>
<li>‘우리는 나선으로 걷는다’에서<blockquote>
</blockquote>
</li>
</ul>
<p>이 문장을 읽고, 취업 준비 과정에서 겪는 여러 경험과 어려움, 알바를 하면서 느끼는 스트레스 등도 <strong>‘나’라는 사람을 이해할 수 있는 기회</strong>라는 것을 깨달았습니다. 예를 들어, ‘바이브 해커톤’ 참가를 통해 최근에 관심사가 ‘바이브 코딩’이구나를 알 수 있었고, 알바나 대인관계에서 느끼는 불편함을 통해 내가 싫어하는 것과 좋아하는 것을 알 수 있습니다. 이러한 경험들이 <strong>내 성향과 관심사, 공부 습관을 파악하게 해주고, 나를 받아들이는 데 도움</strong>이 된다는 것을 깨달았습니다. 덕분에 이전보다 스트레스를 조금 더 객관적으로 바라보고 조절할 수 있게 되었습니다.</p>
<blockquote>
<p>누군가는 성장했다는 것은 꼭 성공했다는 말은 아니다.
그저 두려움을 추구했음을 의미한다. 작든 크든 성장했다는 것은 어둡고 보이지 않음을 알고도 발을 내딛은 용기에서 출발했다는 것이, 누군가들이 말하던 어떤 성공보다 훨씬 큰 의미가 있다고 나는 생각한다.
뜬금없지만, 두려움을 알고도 터벅터벅 시작하는 용기 있는 모든 분들에게 진심으로 응원과 갈채를 보내고, 몸과 마음의 수고스러움도 세세히 살펴주시기를 혼자 떠올려보는 아침.</p>
</blockquote>
<ul>
<li>‘료의 생각 없는 생각’에서<blockquote>
</blockquote>
</li>
</ul>
<p>향로님은 이번 챌린지 참여자분들 모두가 <strong>보장되지 않는 어두운 곳을 향해 발을 내딛는 용기를 냈다는 것 자체가 굉장히 의미 있고 칭찬 받을 일</strong>이라고 말씀해주셨습니다. 비록 당장 눈에 띄는 성과가 없더라도, 시간을 투자하고 노력한 과정 자체가 의미 있다는 말을 듣고, 내가 하는 공부도 마냥 의미 없는 것만은 아니구나 싶었습니다. 연휴 동안 코딩테스트를 준비한다고 해서 반드시 합격할 수 있는 것은 아니지만, <strong>장기적으로 봤을 때 언젠가는 분명 도움이 될 것</strong>이라는 믿음을 갖게 되었습니다. 어떤 공부든 결코 의미 없는 것은 없다는 깨달음을 얻고, 앞으로도 꾸준히 목표를 향해 노력 해야겠습니다.</p>
<p>챌린지 마지막 날, 실제 기업 코딩테스트를 응시하면서 예전보다 성장한 제 자신을 확인할 수 있었습니다. 예전 같았으면 한 문제만 붙잡고 있었을 텐데, 이번에는 7문제 중 3문제를 풀 수 있었습니다. 이번 연휴 동안 시도한 공부 방법이 <strong>저에게 맞는 코딩테스트 준비 방식</strong>임을 알게 되었고, 기출 문제를 반복적으로 풀어보는 것이 중요하다는 것을 깨달았습니다.</p>
<p>그동안 언제 치를지 모르는 코딩테스트를 대비해 꾸준히 문제를 풀어왔지만, 단순히 푸는 것만으로는 충분하지 않다는 것을 깨달았습니다. 문제를 이해하고, 어떤 자료구조를 선택하며 어떻게 풀어낼지 계획한 뒤 코드를 작성하는 과정이 중요하다는 것을 알게 되었습니다. 이제야 내게 맞는 학습 방법을 찾게 되었습니다. 앞으로도 이번 챌린지에서 알게된 문제 풀이 루틴을 참고하여, 문제를 여러 번 풀어 확실히 익히는 방식으로 공부하려고 합니다.</p>
<p>이번 챌린지를 통해서 많은 위로도 받고 내게 맞는 학습 방법도 찾을 수 있었습니다. 열정이 가득한 챌린지 참여자 분들이 있어서 포기하지 않고 꾸준히 공부할 수 있었습니다. 챌린지를 통해 키운 열정을 계속 유지하며 목표를 향해 조금씨 꾸준히 나아가고자 합니다.</p>
<h1 id="참고">참고</h1>
<ul>
<li><a href="https://jojoldu.tistory.com/842">조직에서 부대끼기 | 향로 (기억보단 기록을)</a></li>
<li><a href="https://inf.run/8ZxHd">&#39;향로&#39; 와 함께하는 추석 완강 챌린지</a></li>
<li><a href="https://product.kyobobook.co.kr/detail/S000211600335">우리는 나선으로 걷는다</a></li>
<li><a href="https://product.kyobobook.co.kr/detail/S000216815984">료의 생각 없는 생각</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>