<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dev_osoli.log</title>
        <link>https://velog.io/</link>
        <description>개발 오소리</description>
        <lastBuildDate>Mon, 09 Feb 2026 11:51:28 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>dev_osoli.log</title>
            <url>https://velog.velcdn.com/images/dev_osoli/profile/032af1e5-c82e-4b10-bd88-6160ba9b9598/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. dev_osoli.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev_osoli" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[TypeScript] 타입 계층도 파악하기 ]]></title>
            <link>https://velog.io/@dev_osoli/TypeScript-%ED%83%80%EC%9E%85-%EA%B3%84%EC%B8%B5%EB%8F%84-%ED%8C%8C%EC%95%85%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_osoli/TypeScript-%ED%83%80%EC%9E%85-%EA%B3%84%EC%B8%B5%EB%8F%84-%ED%8C%8C%EC%95%85%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 09 Feb 2026 11:51:28 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>지난 시간에는 타입스크립트의 기본적인 선언과 타입 시스템의 원리에 대해 알아보았습니다. 이번에는 타입스크립트가 내부적으로 타입을 어떻게 분류하고, 어떤 기준으로 서로 호환되는지 <strong>&#39;타입 계층도&#39;</strong>를 통해 깊이 있게 파고들어 보겠습니다.</p>
</blockquote>
<p align=`center` style=`margin-bottom: -50px;`>
  <img src="https://velog.velcdn.com/images/dev_osoli/post/2da930a5-ee85-4098-a304-9224173b7d89/image.png" alt="변수 이미지" />
  <br />
  <sub style="color: gray;">
    출처: <a href="https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8/dashboard?cid=330452" target="_blank" style="color: gray; text-decoration: underline;">한 입 크기로 잘라먹는 타입스크립트(TypeScript)</a>
  </sub>
</p>

<h2 id="1-타입은-집합이다">1. 타입은 &#39;집합&#39;이다</h2>
<p>타입스크립트를 이해하는 가장 중요한 관점은 <strong>타입은 값들의 집합</strong>이라는 점입니다.</p>
<ul>
<li><strong>슈퍼 타입(부모 타입):</strong> 더 큰 범위를 포함하는 집합</li>
<li><strong>서브 타입(자식 타입):</strong> 더 작은 범위를 포함하는 집합</li>
</ul>
<blockquote>
<ul>
<li><strong>핵심 원칙: 업캐스팅은 허용되고, 다운캐스팅은 금지된다.</strong></li>
</ul>
</blockquote>
<ul>
<li>자식 타입의 값을 부모 타입의 변수에 넣는 것(<strong>업캐스팅</strong>)은 가능합니다. (안전)</li>
<li>부모 타입의 값을 자식 타입의 변수에 넣는 것(<strong>다운캐스팅</strong>)은 불가능합니다. (위험)</li>
</ul>
<pre><code class="language-typescript">let num1: number = 10;     // 슈퍼 타입 (number 전체)
let num2: 10 = 10;         // 서브 타입 (10이라는 특정 리터럴)

num1 = num2; // ✅ 가능 (업캐스팅)
num2 = num1; // ‼️ 에러 (다운캐스팅 불가능)
</code></pre>
<h2 id="2-최상위-계층-unknown과-any">2. 최상위 계층: Unknown과 Any</h2>
<p>타입 계층도의 꼭대기에는 모든 타입을 포함할 수 있는 &#39;전체 집합&#39;이 존재합니다.</p>
<h3 id="unknown-타입-safe-top-type">Unknown 타입 (Safe Top Type)</h3>
<p>모든 값을 저장할 수 있지만, 반대로 모든 타입의 변수에 대입할 수는 없습니다.</p>
<pre><code class="language-typescript">function unknownExam() {
  let unknownVar: unknown;

  unknownVar = &quot;&quot;;
  unknownVar = 1;
  unknownVar = () =&gt; {};

  let num: number = unknownVar; // ‼️ 에러 (다운캐스팅 불가)
  let str: string = unknownVar; // ‼️ 에러 (다운캐스팅 불가)
}
</code></pre>
<h3 id="any-타입-the-exception">Any 타입 (The Exception)</h3>
<p>타입 계층도를 무시하는 치트키 같은 타입입니다. <strong>유일하게 다운캐스팅이 허용</strong>되는 예외적인 존재입니다.</p>
<pre><code class="language-typescript">function anyExam() {
  let unknownVar: unknown;
  let anyVar: any;
  let neverVar: never;

  anyVar = unknownVar; // ✅ 가능 (예외적으로 다운캐스팅 허용)

  let num: number = anyVar; // ✅ 가능 (치트키)

  // 단, 아무리 any라도 never 타입에 대입할 수는 없습니다.
  // neverVar = anyVar; // ‼️ 불가능
}
</code></pre>
<h2 id="3-중간-계층-void와-undefined">3. 중간 계층: Void와 Undefined</h2>
<p>타입 계층도에서 <code>void</code>는 아무것도 반환하지 않는 함수의 타입을 의미하며, <strong><code>undefined</code>를 자식으로 두는 부모(슈퍼) 타입</strong>입니다.</p>
<h3 id="3-1-업캐스팅으로서의-관계">3-1. 업캐스팅으로서의 관계</h3>
<p>함수에서 값을 반환하지 않으면 자바스크립트는 기본적으로 <code>undefined</code>를 반환합니다. 따라서 <code>void</code> 타입에 <code>undefined</code> 값을 넣는 것은 <strong>자연스러운 업캐스팅</strong>이 됩니다.</p>
<pre><code class="language-typescript">function voidExam() {
  function voidFunc(): void {
    console.log(&quot;hi&quot;);
    // 실제로는 return undefined; 가 생략된 것과 같음
  }

  let voidVar: void = undefined; // ✅ 가능 (업캐스팅)
}
</code></pre>
<h3 id="3-2-실전-활용-선택적-프로퍼티-optional-property">3-2. 실전 활용: 선택적 프로퍼티 (Optional Property)</h3>
<p>실무에서 <code>undefined</code>를 가장 많이 마주치는 순간은 객체의 <strong>선택적 프로퍼티(<code>?</code>)</strong>를 사용할 때입니다. 프로퍼티 뒤에 <code>?</code>를 붙이면, 해당 속성은 <strong>&#39;해당 타입 | undefined&#39;</strong>인 유니온 타입으로 정의됩니다.</p>
<pre><code class="language-typescript">type User = {
  name: string;
  age?: number; // 사실상 number | undefined 와 같음
};

let user1: User = { name: &quot;오소리&quot; }; // age는 undefined 상태
let user2: User = { name: &quot;오소리&quot;, age: 25 };
</code></pre>
<h3 id="3-3-void와-선택적-프로퍼티의-차이점">3-3. Void와 선택적 프로퍼티의 차이점</h3>
<p>&quot;값이 없다&quot;는 측면에서 <code>void</code>와 <code>undefined</code>는 비슷해 보이지만, 용도가 명확히 다릅니다.</p>
<ul>
<li><strong><code>void</code></strong>: 주로 <strong>함수의 반환값</strong>에서 &quot;이 함수의 결과물에 신경 쓰지 마세요&quot;라는 의도로 사용됩니다.</li>
<li><strong><code>undefined</code></strong>: 주로 <strong>변수나 객체의 프로퍼티</strong>에서 &quot;현재 이 값이 할당되지 않았음&quot;을 명시적으로 나타낼 때 사용합니다.</li>
</ul>
<p><code>never</code> 타입은 실무에서 직접 변수에 지정할 일은 드물지만, 타입 시스템의 안정성을 지탱하는 아주 중요한 <strong>&#39;안전장치&#39;</strong> 역할을 합니다. 실전 사용처를 포함하여 내용을 재구성해 드릴게요.</p>
<hr>
<h2 id="4-최하위-계층-never-bottom-type">4. 최하위 계층: Never (Bottom Type)</h2>
<p><code>never</code>는 <strong>공집합</strong>을 의미합니다. 수학적으로 집합에 아무런 원소가 없는 상태처럼, 어떤 값도 가질 수 없는 <strong>불가능한 상태</strong>를 나타내는 타입입니다.</p>
<ul>
<li><strong>모든 타입의 서브 타입:</strong> 계층도의 가장 바닥에 있어 어디든 대입될 수 있습니다. (업캐스팅 가능)</li>
<li><strong>다운캐스팅 절대 불가:</strong> 그 어떤 타입도 <code>never</code> 타입 변수에 대입될 수 없습니다. (심지어 <code>any</code>조차 불가능합니다.)</li>
</ul>
<pre><code class="language-typescript">function neverExam() {
  function neverFunc(): never {
    while (true) {}
  }

  let num: number = neverFunc(); // ✅ 가능 (어디든 들어갈 수 있음)
  let str: string = neverFunc(); // ✅ 가능

  let neverVar: never;
  // neverVar = 1;      // ‼️ 에러
  // neverVar = &quot;str&quot;;  // ‼️ 에러
  // neverVar = anyVar; // ‼️ 에러 (any조차 감당 안 됨)
}
</code></pre>
<h3 id="💡-실전에서는-언제-사용할까">💡 실전에서는 언제 사용할까?</h3>
<p>&quot;직접 쓸 일이 있나?&quot; 싶지만, <code>never</code>는 다음과 같은 상황에서 우리 코드를 지켜줍니다.</p>
<h3 id="4-1-철저한-조건부-검사-exhaustive-check">4-1. 철저한 조건부 검사 (Exhaustive Check)</h3>
<p>모든 케이스를 다 처리했는지 검사할 때 사용합니다. 만약 새로운 타입이 추가되었는데 처리를 깜빡했다면 <code>never</code>가 에러를 띄워줍니다.</p>
<pre><code class="language-typescript">type Drink = &quot;Coffee&quot; | &quot;Juice&quot; | &quot;Tea&quot;;

function order(drink: Drink) {
  switch (drink) {
    case &quot;Coffee&quot;: break;
    case &quot;Juice&quot;: break;
    // ‼️ 만약 &quot;Tea&quot; 케이스를 빼먹었다면?
    default:
      // Tea가 처리되지 않아 이 코드로 넘어오게 되고, 
      // never 타입 변수에 Tea가 할당되려 하므로 컴파일 에러가 발생합니다!
      const check: never = drink; 
  }
}
</code></pre>
<h3 id="4-2-의도적인-에러-발생-및-무한-루프">4-2. 의도적인 에러 발생 및 무한 루프</h3>
<p>함수가 정상적으로 끝나지 않는 경우(중간에 에러를 던지거나 무한 반복될 때) 반환 타입으로 지정합니다.</p>
<pre><code class="language-typescript">function throwError(message: string): never {
  throw new Error(message);
}
</code></pre>
<h3 id="4-3-타입-조작-및-필터링">4-3. 타입 조작 및 필터링</h3>
<p>나중에 다룰 고급 타입에서, 특정 조건을 만족하지 않는 타입을 <strong>제거</strong>하고 싶을 때 <code>never</code>를 사용합니다.</p>
<pre><code class="language-typescript">// T가 number면 제외(never)하고, 아니면 그대로 유지해라!
type ExcludeNumber&lt;T&gt; = T extends number ? never : T;

type Result = ExcludeNumber&lt;string | number&gt;; // 결과는 string만 남음
</code></pre>
<h2 id="5-객체-타입의-호환성">5. 객체 타입의 호환성</h2>
<p>객체 타입 간의 호환성은 &#39;이름&#39;이 아니라 <strong>&#39;구조&#39;</strong>를 기준으로 결정됩니다. (구조적 타입 시스템)</p>
<ul>
<li><strong>조건(프로퍼티)이 적을수록 더 넓은 범위의 슈퍼 타입</strong>이 됩니다.</li>
</ul>
<pre><code class="language-typescript">type Animal = {
  name: string;
  color: string;
};

type Dog = {
  name: string;
  color: string;
  breed: string; // 조건이 하나 더 많음 (서브 타입)
};

let animal: Animal = { name: &quot;기린&quot;, color: &quot;yellow&quot; };
let dog: Dog = { name: &quot;돌돌이&quot;, color: &quot;brown&quot;, breed: &quot;진도&quot; };

// ✅ 가능: 부모 타입 변수에 자식 객체를 넣는 것 (업캐스팅)
animal = dog; 

// ‼️ 에러: 자식 타입 변수에 부모 객체를 넣는 것 (다운캐스팅)
// dog = animal; 
</code></pre>
<h3 id="💡-주의-초과-프로퍼티-검사">💡 주의: 초과 프로퍼티 검사</h3>
<p>객체 리터럴을 <strong>직접</strong> 대입할 때는 정의되지 않은 속성이 있으면 에러가 발생합니다.</p>
<pre><code class="language-typescript">let animal2: Animal = {
  name: &quot;진도&quot;,
  color: &quot;white&quot;,
  // breed: &quot;진도&quot; // ‼️ 에러 (초과 프로퍼티 검사)
};
</code></pre>
<p>변수에 담아서 대입할 때는 통과되지만, 리터럴로 직접 줄 때는 타입스크립트가 엄격하게 검사한다는 점을 유의해야 합니다.</p>
<h2 id="마치며-타입-계층도는-어떻게-타입을-관리하는가">마치며: 타입 계층도는 어떻게 타입을 관리하는가?</h2>
<p>타입스크립트의 타입 시스템은 단순히 타입을 나열하는 것이 아니라, 거대한 <strong>계층도(Hierarchy)</strong>를 통해 전체 구조를 체계적으로 관리합니다.</p>
<ul>
<li><strong>범위의 관리:</strong> 최상위의 <strong><code>unknown</code></strong>부터 최하위의 <strong><code>never</code></strong>까지, 각 타입이 가지는 집합의 크기를 기준으로 상하 관계를 설정하여 데이터의 흐름을 통제합니다.</li>
<li><strong>호환성의 관리:</strong> &quot;이 변수에 이 값을 넣어도 안전한가?&quot;라는 질문에 대해, 계층도상의 <strong>&#39;업캐스팅&#39;</strong> 여부를 판단 근거로 삼아 런타임 에러를 사전에 차단합니다.</li>
<li><strong>확장의 관리:</strong> 객체 타입에 속성이 추가될수록 계층도의 아래쪽(자식)으로 배치되는 원리를 통해, 타입이 구체화될수록 더 엄격한 관리가 이루어지도록 설계되어 있습니다.</li>
</ul>
<blockquote>
<p>결국 타입 계층도를 이해한다는 것은 <strong>타입스크립트가 우리 코드를 어떤 질서로 관리하고 보호하는지 그 설계 원리를 파악하는 것</strong>과 같습니다. 이 질서를 이해하면 복잡한 타입 에러 앞에서도 당황하지 않고 정확한 해결책을 찾을 수 있습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TypeScript] 다양한 타입 선언과 시스템]]></title>
            <link>https://velog.io/@dev_osoli/TypeScript-%ED%83%80%EC%9E%85-%EC%84%A0%EC%96%B8%EA%B3%BC-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@dev_osoli/TypeScript-%ED%83%80%EC%9E%85-%EC%84%A0%EC%96%B8%EA%B3%BC-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Sun, 08 Feb 2026 12:19:47 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>TypeScript는 단순히 타입을 붙이는 것을 넘어, 코드의 안정성을 높이고 개발자의 실수를 미연에 방지하는 강력한 도구입니다. 기본적인 선언 방법부터 실무 활용 팁까지 정리해 보겠습니다.</p>
</blockquote>
<h2 id="1-기본적인-타입-선언-방법">1. 기본적인 타입 선언 방법</h2>
<h3 id="타입-별칭-type-alias">타입 별칭 (Type Alias)</h3>
<p>가장 많이 사용되는 방식입니다. 복잡한 객체 구조에 이름을 붙여 재사용할 수 있게 해줍니다.</p>
<pre><code class="language-typescript">type User = {
  id: number;
  name: string;
  nickname: string;
  birth: string;
};

const user: User = { 
  id: 1, 
  name: &quot;홍길동&quot;,
  nickname: &quot;dev_osoli&quot;,
  birth: &quot;1998.11.12&quot;,
};
</code></pre>
<h3 id="인덱스-시그니처-index-signature">인덱스 시그니처 (Index Signature)</h3>
<p>객체의 속성 이름은 동적이지만, <strong>키와 값의 타입 규칙</strong>이 일정한 경우에 유용합니다.</p>
<pre><code class="language-typescript">type CountryCodes = {
  [key: string]: string; 
};

let countryCodes: CountryCodes = { 
  Korea: &#39;ko&#39;,
  UnitedState: &#39;us&#39;,
};
</code></pre>
<blockquote>
<p><strong>💡 활용 팁:</strong> 수많은 국가 코드나 설정값처럼 키값이 계속 늘어날 수 있는 구조에서 코드의 중복을 획기적으로 줄여줍니다.</p>
</blockquote>
<h2 id="2-유연함과-확신-와-">2. 유연함과 확신: <code>?</code>와 <code>!</code></h2>
<p>TypeScript에서 이 두 기호는 <strong>데이터의 존재 여부</strong>를 다룰 때 핵심적인 역할을 합니다.</p>
<table>
<thead>
<tr>
<th>기호</th>
<th>이름</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><strong><code>?</code></strong></td>
<td><strong>옵셔널 (Optional)</strong></td>
<td>&quot;이 값은 <strong>있을 수도 있고 없을 수도 있음</strong>&quot; (선택)</td>
</tr>
<tr>
<td><strong><code>!</code></strong></td>
<td><strong>단언 (Assertion)</strong></td>
<td>&quot;이 값은 <strong>절대 null/undefined가 아님</strong>을 내가 보장함&quot; (확신)</td>
</tr>
</tbody></table>
<pre><code class="language-typescript">interface Person {
  name: string;
  tall?: number; 
}

function introduce(person: Person) {
  // 1. 단언 (!) 사용 시
  // 개발자가 &quot;난 이 함수 호출할 때 무조건 tall을 넣을 거야&quot;라고 확신할 때
  console.log(person.tall!.toFixed()); 

  // 2. 옵셔널 체이닝 (?.) 사용 시
  // 값이 없으면 에러 대신 undefined를 반환 (toFixed는 호출되지 않음)
  console.log(person.tall?.toFixed());

  // 3. 조건문 (Safe Guard) 사용 시 - 가장 권장되는 방법
  // 타입 가드를 통해 블록 내부에서 tall을 &#39;number&#39;로 확정시킴
  if (person.tall) {
    console.log(person.tall.toFixed()); // 여기선 !나 ?가 없어도 안전함
  }
}</code></pre>
<hr>
<h2 id="3-대수-타입-algebraic-data-types">3. 대수 타입 (Algebraic Data Types)</h2>
<p>여러 타입을 합성하여 새로운 타입을 만드는 방법입니다.</p>
<ul>
<li><strong>합집합 (Union, <code>|</code>)</strong>: 여러 타입 중 하나일 수 있음을 의미합니다.</li>
<li><strong>교집합 (Intersection, <code>&amp;</code>)</strong>: 모든 타입의 속성을 전부 가져야 함을 의미합니다.</li>
</ul>
<pre><code class="language-typescript">type Dog = { name: string; color: string; };
type Person = { name: string; language: string; };

type Union = Dog | Person; // Dog이거나 Person이거나
type Intersection = Dog &amp; Person; // Dog이면서 동시에 Person (name, color, language 모두 필요)</code></pre>
<h2 id="4-서로소-유니온-타입-discriminated-union">4. 서로소 유니온 타입 (Discriminated Union)</h2>
<p>객체 타입들에 <strong>공통된 리터럴 속성(<code>tag</code>, <code>state</code> 등)</strong>을 부여하여 타입을 확실하게 구분하는 기법입니다.</p>
<h3 id="왜-사용할까">왜 사용할까?</h3>
<p>비동기 작업의 상태를 관리할 때, <code>?</code>를 남발하면 <code>if</code>문 안에서 타입 추론이 제대로 되지 않아 에러가 발생할 수 있습니다. 하지만 서로소 유니온을 사용하면 안전하게 좁히기(Narrowing)가 가능합니다.</p>
<pre><code class="language-typescript">type LoadingTask = { state: &#39;LOADING&#39; };
type FailedTask = { state: &#39;FAILED&#39;; error: { message: string } };
type SuccessTask = { state: &#39;SUCCESS&#39;; response: { data: string } };

type AsyncTask = LoadingTask | FailedTask | SuccessTask;

function processResult(task: AsyncTask) {
  switch (task.state) {
    case &#39;LOADING&#39;:
      console.log(&#39;로딩 중&#39;);
      break;
    case &#39;FAILED&#39;:
      // task가 FailedTask임이 확실해져서 error 속성에 안전하게 접근 가능
      console.log(`에러 : ${task.error.message}`);
      break;
    case &#39;SUCCESS&#39;:
      console.log(`성공 : ${task.response.data}`);
      break;
  }
}
</code></pre>
<hr>
<h2 id="5-인터페이스와-클래스">5. 인터페이스와 클래스</h2>
<h3 id="인터페이스-interface">인터페이스 (Interface)</h3>
<p>객체의 구조를 정의하는 &#39;약속&#39;입니다. 메서드 오버로딩이나 선언 합치기가 가능합니다.</p>
<pre><code class="language-typescript">interface Person {
  readonly name: string;
  age?: number;
  sayHi(): void;
}</code></pre>
<h3 id="클래스-class--상속">클래스 (Class) &amp; 상속</h3>
<p>객체를 생성하는 틀이며, <code>extends</code>를 통해 기능을 확장할 수 있습니다.</p>
<pre><code class="language-typescript">class Student {
  constructor(public name: string, public grade: string) {}
}

class StudentDeveloper extends Student {
  constructor(name: string, grade: string, public skill: string) {
    super(name, grade); // 부모 생성자 호출
  }
}</code></pre>
<h2 id="6-타입-조작-keyof--typeof">6. 타입 조작: Keyof &amp; Typeof</h2>
<ul>
<li><strong><code>typeof</code></strong>: 실제 값에서 타입을 추출합니다.</li>
<li><strong><code>keyof</code></strong>: 객체 타입의 모든 키(key)들을 문자열 유니온 타입으로 가져옵니다.</li>
</ul>
<pre><code class="language-typescript">const person = { name: &quot;홍길동&quot;, age: 27 };

// person 객체의 타입을 가져와서 그 키값들만 유니온으로 추출
type PersonKey = keyof typeof person; // &quot;name&quot; | &quot;age&quot;
</code></pre>
<h3 id="마치며">마치며</h3>
<p>TypeScript의 선언 방법은 다양하지만, 핵심은 <strong>&quot;어떻게 하면 타입 에러를 줄이고 의도를 명확히 전달할 것인가&quot;</strong>에 있습니다. 특히 <strong>서로소 유니온</strong>은 복잡한 상태 관리에서 빛을 발하는 기법이니 꼭 익혀두시길 권장합니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TypeScript] 기본 타입(Basic Types) 완벽 정리]]></title>
            <link>https://velog.io/@dev_osoli/TypeScript-%EA%B8%B0%EB%B3%B8-%ED%83%80%EC%9E%85Basic-Types-%EC%99%84%EB%B2%BD-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@dev_osoli/TypeScript-%EA%B8%B0%EB%B3%B8-%ED%83%80%EC%9E%85Basic-Types-%EC%99%84%EB%B2%BD-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 03 Feb 2026 12:30:33 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>타입스크립트를 배우는 가장 첫 번째 단계는 변수에 &quot;타입의 이름표&quot;를 붙이는 것입니다. 자바스크립트의 유연함에 타입스크립트의 단단함을 더해주는 핵심 기본 타입들을 예제와 함께 알아보겠습니다.</p>
</blockquote>
<p align=`center` style=`margin-bottom: -50px;`>
  <img src="https://velog.velcdn.com/images/dev_osoli/post/2da930a5-ee85-4098-a304-9224173b7d89/image.png" alt="변수 이미지" />
  <br />
  <sub style="color: gray;">
    출처: <a href="https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8/dashboard?cid=330452" target="_blank" style="color: gray; text-decoration: underline;">한 입 크기로 잘라먹는 타입스크립트(TypeScript)</a>
  </sub>
</p>

<h2 id="1-변수-선언과-타입-어노테이션annotation">1. 변수 선언과 타입 어노테이션(Annotation)</h2>
<p>타입스크립트에서는 변수명 뒤에 콜론(<code>:</code>)을 붙여 타입을 명시합니다. 이를 <strong>타입 어노테이션</strong>이라고 부릅니다.</p>
<pre><code class="language-tsx">// 기본 변수 선언
const userId: string = &quot;user123&quot;;      // 문자열
let messageCount: number = 0;          // 숫자
const isLoading: boolean = true;       // 불리언</code></pre>
<hr>
<h2 id="2-원시-타입-primitive-types">2. 원시 타입 (Primitive Types)</h2>
<p>하나의 값만 저장하는 가장 기본적인 타입들입니다.</p>
<h3 id="숫자-문자열-불리언">숫자, 문자열, 불리언</h3>
<pre><code class="language-tsx">let num: number = 123;      // 정수, 실수, Infinity, NaN 모두 number!
let str: string = &quot;hello&quot;;  // 백틱(`)을 이용한 템플릿 리터럴도 가능
let bool: boolean = true;   // true 또는 false</code></pre>
<h3 id="null과-undefined">null과 undefined</h3>
<p>기본적으로 <code>null</code>과 <code>undefined</code>는 각각 자기 자신의 타입만 가질 수 있습니다.</p>
<pre><code class="language-tsx">let nullVal: null = null;
let undeVal: undefined = undefined;

// [꿀팁] 설정을 통해 엄격한 검사를 끌 수도 있지만, 
// 실무에서는 strictNullChecks: true를 유지하는 것이 안전합니다.</code></pre>
<h3 id="리터럴-타입-literal-types">리터럴 타입 (Literal Types)</h3>
<p>특정 &quot;값&quot; 자체를 타입으로 지정할 수 있습니다. 해당 값 외에는 넣을 수 없게 엄격히 제한합니다.</p>
<pre><code class="language-tsx">let exactNum: 10 = 10;
// exactNum = 12; // ‼️ 에러 발생! 10만 넣을 수 있습니다.</code></pre>
<hr>
<h2 id="3-배열array과-튜플tuple">3. 배열(Array)과 튜플(Tuple)</h2>
<p>데이터 묶음을 다룰 때 사용하는 타입들입니다.</p>
<h3 id="배열">배열</h3>
<pre><code class="language-tsx">let numArr: number[] = [1, 2, 3];
let strArr: Array&lt;string&gt; = [&quot;a&quot;, &quot;b&quot;, &quot;c&quot;]; // 제네릭 방식

// 여러 타입이 섞인 배열 (유니온 타입 활용)
let multiArr: (string | number)[] = [1, &quot;hello&quot;];

// 다차원 배열
let doubleArr: number[][] = [[1, 2], [3, 4]];</code></pre>
<blockquote>
<p>왜 Array<string>이 제네릭인가요? 제네릭(Generic)은 타입을 마치 파라미터처럼 넘겨주는 방식을 말합니다. Array라는 틀은 이미 만들어져 있고, 그 안에 어떤 타입의 알맹이를 담을지 &#39;&lt;&gt;&#39; 사이에 써주는 것이죠. &quot;배열(Array)인데, 알맹이는 문자열(string)로 채울 거야!&quot;라고 선언하는 방식이라 제네릭 방식이라고 부릅니다.</p>
</blockquote>
<h3 id="튜플">튜플</h3>
<p><strong>길이와 타입 순서가 고정된 배열</strong>입니다. 자바스크립트에는 없는 타입스크립트만의 특징입니다.</p>
<pre><code class="language-tsx">let user: [string, number] = [&quot;홍길동&quot;, 1];
// user = [1, &quot;홍길동&quot;]; // ‼️ 에러! 순서가 맞지 않습니다.

// [주의] 튜플은 컴파일 후 일반 배열이 되므로 push, pop은 허용되지만, 
// 초기 정의된 구조를 강제하여 실수를 방지하는 데 매우 효과적입니다.</code></pre>
<hr>
<h2 id="4-실무-데이터-구조화-interface--optional">4. 실무 데이터 구조화: Interface &amp; Optional</h2>
<p>실제 개발에서는 객체 형태의 데이터를 많이 다룹니다. <code>interface</code>를 통해 구조를 정의합니다.</p>
<pre><code class="language-tsx">interface UserSettings {
  theme: &quot;light&quot; | &quot;dark&quot;;    // 리터럴과 유니온(|)의 조합
  notificationsEnabled?: boolean; // ?는 &#39;없어도 됨&#39;을 의미 (Optional)
}

const settings: UserSettings = {
  theme: &quot;dark&quot; // notificationsEnabled는 생략 가능
};</code></pre>
<h2 id="💡-마치며">💡 마치며</h2>
<p>타입스크립트의 기본 타입은 단순히 오류를 방지하는 기능을 넘어, 코드가 어떤 데이터를 다루는지 명확하게 보여주는 살아있는 문서 역할을 합니다. 처음에는 조금 번거로워 보여도, 타입을 꼼꼼히 작성하는 습관을 들이면 나중에 훨씬 더 편하고 안정적인 개발이 가능합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TypeScript] 컴파일러 옵션 정리]]></title>
            <link>https://velog.io/@dev_osoli/TypeScript-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC-%EC%98%B5%EC%85%98-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@dev_osoli/TypeScript-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC-%EC%98%B5%EC%85%98-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 03 Feb 2026 11:53:45 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>타입스크립트 프로젝트의 핵심 설정 파일인 <strong><code>tsconfig.json</code></strong> 을 제대로 활용하면 개발 효율과 코드의 안정성을 동시에 잡을 수 있습니다. 실무에서 가장 자주 쓰이고 중요한 옵션들을 예제와 함께 정리했습니다.</p>
</blockquote>
<h2 id="🚀-한눈에-보는-tsconfigjson-예시">🚀 한눈에 보는 tsconfig.json 예시</h2>
<p>아래는 실무에서 흔히 쓰이는 주요 옵션들을 모은 예시 설정입니다.</p>
<pre><code class="language-json">{
  &quot;compilerOptions&quot;: {
    &quot;target&quot;: &quot;ESNext&quot;,         // 최신 자바스크립트 버전으로 컴파일
    &quot;lib&quot;: [&quot;dom&quot;, &quot;dom.iterable&quot;, &quot;esnext&quot;], // 사용 가능한 내장 API 목록 (브라우저 환경 등)
    &quot;module&quot;: &quot;CommonJS&quot;,       // 생성될 자바스크립트의 모듈 시스템 (CommonJS, NodeNext 등)
    &quot;outDir&quot;: &quot;dist&quot;,           // 컴파일된 파일들이 저장될 위치
    &quot;strict&quot;: true,             // 모든 엄격한 타입 검사 옵션 활성화
    &quot;noEmit&quot;: true,             // 변환된 JS 파일을 생성하지 않음 (타입 체크 전용)
    &quot;jsx&quot;: &quot;react-jsx&quot;,         // JSX 문법 처리 방식 (React 최신 버전용)
    &quot;strictNullChecks&quot;: true,   // null과 undefined 엄격하게 구분
    &quot;moduleDetection&quot;: &quot;force&quot;, // 모든 파일을 격리된 모듈로 취급 (전역 오염 방지)
    &quot;resolveJsonModule&quot;: true,  // JSON 파일을 직접 import 가능하게 허용
    &quot;isolatedModules&quot;: true,    // 각 파일을 독립된 모듈로 컴파일 (빌드 도구 호환성)
    &quot;skipLibCheck&quot;: true,       // 라이브러리 파일(*.d.ts)의 타입 검사 건너뛰기
    &quot;esModuleInterop&quot;: true,    // CommonJS 모듈을 ES6 문법으로 자연스럽게 import
    &quot;incremental&quot;: true,        // 변경된 부분만 컴파일하여 빌드 속도 향상
    &quot;plugins&quot;: [{ &quot;name&quot;: &quot;next&quot; }], // 프레임워크(Next.js 등) 전용 플러그인
    &quot;paths&quot;: {                  // 경로 별칭 설정
      &quot;@/*&quot;: [&quot;./src/*&quot;]
    }
  },
  &quot;ts-node&quot;: {
    &quot;esm&quot;: true                   // ts-node에서 ESM 모듈 형식 사용 설정
  },
  &quot;include&quot;: [&quot;src&quot;]              // 컴파일 대상 폴더 지정
  &quot;exclude&quot;: [&quot;node_modules&quot;]     // 컴파일에서 제외할 폴더
}</code></pre>
<table>
<thead>
<tr>
<th>옵션</th>
<th>기본값 (Default)</th>
<th>참고 사항</th>
</tr>
</thead>
<tbody><tr>
<td>target</td>
<td>ES3</td>
<td>아주 보수적이죠? 그래서 보통 ES6나 ESNext로 명시합니다.</td>
</tr>
<tr>
<td>module</td>
<td>target에 따라 다름</td>
<td>target이 ES3~5면 CommonJS, ES6 이상이면 ESNext가 기본인 경우가 많습니다.</td>
</tr>
<tr>
<td>outDir</td>
<td>undefined</td>
<td>명시하지 않으면 .ts 파일이 있는 위치에 .js가 생성됩니다.</td>
</tr>
<tr>
<td>strict</td>
<td>false</td>
<td>가장 중요한 옵션입니다. 안정성을 위해 반드시 true를 권장합니다.</td>
</tr>
<tr>
<td>strictNullChecks</td>
<td>false</td>
<td>strict: true를 설정하면 자동으로 true가 됩니다.</td>
</tr>
<tr>
<td>moduleDetection</td>
<td>auto</td>
<td>기본은 auto지만, 현대적인 프로젝트에서는 force가 안전합니다.</td>
</tr>
<tr>
<td>skipLibCheck</td>
<td>false</td>
<td>외부 라이브러리 에러까지 잡으려다 빌드가 느려지곤 해서 보통 true로 씁니다.</td>
</tr>
<tr>
<td>esModuleInterop</td>
<td>false</td>
<td><code>import React from &#39;react&#39;</code> 같은 문법을 쓰려면 true가 필수입니다.</td>
</tr>
<tr>
<td>incremental</td>
<td>false</td>
<td>빌드 속도를 위해 true로 두는 것이 유리합니다.</td>
</tr>
<tr>
<td>lib</td>
<td>target에 따라 다름</td>
<td>브라우저 API(dom) 등을 쓸지 결정합니다.</td>
</tr>
<tr>
<td>noEmit</td>
<td>false</td>
<td>결과물을 생성하지 않고 타입 체크만 할 때 true로 둡니다.</td>
</tr>
<tr>
<td>resolveJsonModule</td>
<td>false</td>
<td>JSON 파일을 직접 import 하려면 true로 켜주세요.</td>
</tr>
<tr>
<td>isolatedModules</td>
<td>false</td>
<td>Vite 등 최신 빌드 도구를 쓴다면 안전을 위해 true를 권장합니다.</td>
</tr>
<tr>
<td>jsx</td>
<td>undefined</td>
<td>React 등 JSX 문법 처리 방식을 결정합니다 (예: react-jsx).</td>
</tr>
<tr>
<td>exclude</td>
<td>[node_modules]</td>
<td>컴파일에서 제외할 폴더를 지정해 빌드 성능을 높입니다.</td>
</tr>
</tbody></table>
<h2 id="📘-핵심-옵션-상세-설명">📘 핵심 옵션 상세 설명</h2>
<h3 id="1-기본-설정-project-structure">1. <strong>기본 설정 (Project Structure)</strong></h3>
<ul>
<li><strong><code>target</code></strong> : 타입스크립트 코드를 어떤 버전의 자바스크립트로 변환할지 정합니다. ESNext는 현재 환경에서 지원하는 가장 최신의 사양을 의미합니다.</li>
<li><strong><code>module</code></strong> : 자바스크립트 파일 간에 코드를 주고받는 방식을 정합니다. Node.js 백엔드 환경에서는 주로 CommonJS를, 최신 웹 번들러 환경에서는 ESNext를 사용합니다.</li>
<li><strong><code>outDir</code></strong> : tsc 명령어로 컴파일했을 때 자바스크립트 결과물들이 저장될 폴더를 지정합니다. 주로 dist나 build 폴더를 사용합니다.</li>
<li><strong><code>include</code></strong> : 타입스크립트가 컴파일할 파일의 범위를 정합니다. [&quot;src&quot;]로 설정하면 src 폴더 안의 파일들만 관리하게 되어 훨씬 깔끔합니다.</li>
<li><strong><code>exclude</code></strong>: 컴파일 과정에서 명시적으로 제외할 폴더를 지정합니다. 보통 <code>node_modules</code>나 <code>dist</code> 폴더를 제외하여 컴파일 성능을 최적화합니다.</li>
</ul>
<h3 id="2-타입-검사-checking--emit">2. <strong>타입 검사 (Checking &amp; Emit)</strong></h3>
<ul>
<li><strong><code>strict</code></strong> : 타입스크립트의 정수입니다. <code>true</code>로 설정하면 모든 엄격한 타입 체크 기능을 켭니다. 새로 시작하는 프로젝트라면 반드시 <code>true</code>로 설정하는 것을 권장합니다.</li>
<li><strong><code>strictNullChecks</code></strong> : <code>null</code>이나 <code>undefined</code> 값을 다룰 때 발생할 수 있는 런타임 에러를 방지합니다.</li>
<li><strong><code>noEmit</code></strong>: 타입스크립트가 자바스크립트 결과물을 직접 생성(Emit)하지 않도록 합니다.<blockquote>
<p>최근에는 Vite나 Next.js(SWC)처럼 다른 도구가 빌드를 전담하므로, 타입스크립트는 &quot;오직 타입 체크&quot;만 수행하도록 이 옵션을 켜두는 경우가 많습니다.</p>
</blockquote>
</li>
<li><strong><code>skipLibCheck</code></strong> : <code>node_modules</code>에 있는 라이브러리 타입 정의 파일들의 검사를 생략합니다.
TIP</li>
</ul>
<blockquote>
<p>버전이 다른 여러 라이브러리가 같은 타입 이름을 다르게 정의해서 발생하는 충돌(특히 20버전 이상 대응 시)을 해결할 때 매우 유용합니다.</p>
</blockquote>
<h3 id="3-모듈-및-라이브러리-설정-modules--libs">3. 모듈 및 라이브러리 설정 (Modules &amp; Libs)</h3>
<ul>
<li><strong><code>lib</code></strong>: 타입스크립트가 어떤 환경의 내장 함수(API)들을 알고 있는지 정합니다. 브라우저용 앱이라면 <code>[&quot;dom&quot;, &quot;dom.iterable&quot;, &quot;esnext&quot;]</code> 처럼 설정하여 <code>window</code>, <code>document</code> 같은 인터페이스를 사용할 수 있게 합니다.</li>
<li><strong><code>moduleDetection: &quot;force&quot;</code></strong> : 파일 내부에 <code>import</code>나 <code>export</code>가 없어도 타입스크립트가 이를 독립된 모듈로 인식하게 만듭니다. 전역 공간이 더러워지는 것을 방지하는 아주 깔끔한 옵션입니다.</li>
<li><strong><code>resolveJsonModule</code></strong> : <code>tsconfig.json</code>파일을 한 컴파일 단위 내에서 직접 <code>import</code> 할 수 있게 해줍니다. 설정 정보나 상수 데이터를 다룰 때 매우 유용합니다.</li>
<li><strong><code>isolatedModules</code></strong> : 모든 파일을 각각의 독립된 모듈로 강제합니다. 현대적인 빌드 도구(SWC, esbuild)는 파일을 하나씩 따로 변환하기 때문에, 이 옵션을 켜두어야 도구들이 안전하게 코드를 처리할 수 있습니다.</li>
</ul>
<h3 id="4-개발-환경-유틸리티">4. 개발 환경 유틸리티</h3>
<ul>
<li><strong><code>jsx</code></strong>: JSX 문법을 최종 결과물에서 어떻게 표현할지 결정합니다. 최신 <code>React</code> 프로젝트라면 <code>&quot;react-jsx&quot;</code>를 사용하여 별도의 <code>import React</code> 없이도 JSX를 사용할 수 있게 합니다.</li>
<li><strong><code>plugins</code></strong>: <code>Next.js</code>와 같은 프레임워크 전용 플러그인을 설정합니다. 예를 들어 <code>Next.js</code> 플러그인은 컴포넌트 내에서 타입 체크를 더 정확하게 도와줍니다.</li>
<li><strong><code>ts-node</code></strong> : 타입스크립트 파일을 컴파일 없이 바로 실행할 때 필요한 설정입니다. &quot;esm&quot;: true 옵션은 최신 모듈 시스템을 원활하게 사용하게 해줍니다.</li>
<li><strong><code>paths</code></strong> : <code>../../shared/ui/Button</code> 같은 지저분한 경로를 <code>@/shared/ui/Button</code>으로 바꿔줍니다. 코드 가독성이 획기적으로 좋아집니다.</li>
<li><strong><code>esModuleInterop</code></strong> : <strong><code>import * as React from &#39;react&#39;</code></strong> 대신 <strong><code>import React from &#39;react&#39;</code></strong> 처럼 더 현대적인 방식으로 라이브러리를 가져올 수 있게 도와줍니다.</li>
</ul>
<h3 id="🔍-잠깐-번들러bundler란-무엇인가요">🔍 잠깐! &quot;번들러(Bundler)&quot;란 무엇인가요?</h3>
<blockquote>
<p>컴파일 옵션을 설명할 때 자주 등장하는 번들러는 수많은 자바스크립트 파일, CSS, 이미지 등 프로젝트의 자원들을 하나의 파일(또는 소수의 파일)로 묶어주는 도구입니다.</p>
</blockquote>
<ul>
<li>대표 도구: Vite, Webpack, esbuild, Rollup</li>
<li>필요한 이유: 브라우저가 수많은 파일을 개별적으로 요청하면 로딩 속도가 느려지기 때문에, 이를 최적화해서 하나로 합치고 코드를 압축하는 역할을 합니다. 최근 타입스크립트 설정(<code>moduleResolution: &quot;bundler&quot;</code>)은 바로 이 도구들과의 호환성을 맞추기 위한 것입니다.</li>
</ul>
<h2 id="🔥-2026년-선호하는-최신-트렌드">🔥 2026년 선호하는 최신 트렌드</h2>
<p>최근 Vite, Next.js, Bun과 같은 현대적인 도구들을 사용하는 프로젝트에서는 다음과 같은 설정들이 표준처럼 자리 잡고 있습니다.</p>
<ol>
<li><p><strong><code>moduleResolution: &quot;bundler&quot;</code></strong>
예전에는 <code>node</code>를 주로 썼지만, 이제는 <code>Vite</code>나 <code>Webpack</code> 같은 번들러 환경에 최적화된 <code>&quot;bundler&quot;</code> 옵션을 사용합니다. 이는 현대적인 패키지들이 제공하는 <code>exports</code> 필드를 정확하게 해석해 줍니다.</p>
</li>
<li><p><strong><code>verbatimModuleSyntax: true</code></strong>
기존의 <code>importsNotUsedAsValues</code>를 대체하는 최신 옵션입니다. <code>import type</code>을 명확히 구분하여 컴파일된 자바스크립트에 불필요한 코드가 남지 않도록 엄격하게 관리하며, 번들러와의 호환성이 매우 좋습니다.</p>
</li>
<li><p><strong><code>isolatedModules: true</code></strong>
각 파일을 독립적으로 컴파일할 수 있음을 보장합니다. 이는 <code>SWC</code>나 <code>esbuild</code>처럼 타입 정보를 사용하지 않고 빠르게 트랜스파일하는 도구들을 쓸 때 필수적인 설정입니다.</p>
</li>
<li><p><strong><code>noUnusedLocals &amp; noUnusedParameters</code></strong>
사용하지 않는 변수나 매개변수가 있을 때 에러를 발생시킵니다. 코드의 청결도를 유지하는 데 매우 효과적이라 많은 팀에서 기본적으로 활성화하는 추세입니다.</p>
</li>
</ol>
<h2 id="💡-마치며">💡 마치며</h2>
<p><strong><code>tsconfig.json</code></strong>은 단순히 설정을 나열하는 곳이 아니라, 우리 프로젝트의 품질 가이드라인을 정하는 곳입니다. 특히 <code>strictNullChecks</code>나 <code>moduleDetection</code> 같은 옵션을 적극적으로 활용하여 타입 안정성과 모듈 일관성을 확보하면, 팀 전체의 개발 경험이 훨씬 견고해지고 예측 가능한 코드베이스를 유지할 수 있습니다.</p>
<p>📚 공식 출처 및 참고 자료</p>
<p>이 글은 다음의 공식 자료와 최신 생태계 권장 사항을 바탕으로 작성되었습니다.</p>
<ul>
<li><a href="https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/">TypeScript 5.0 Announcement (Microsoft)</a></li>
<li><a href="https://www.typescriptlang.org/docs/handbook/modules/reference.html#the-bundler-strategy">TypeScript Official Docs: Module Resolution (Bundler)</a></li>
<li><a href="https://www.totaltypescript.com/tsconfig-cheat-sheet">Total TypeScript: TSConfig Cheat Sheet</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TypeScript] 타입 시스템 소개]]></title>
            <link>https://velog.io/@dev_osoli/TypeScript-%ED%83%80%EC%9E%85-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%86%8C%EA%B0%9C</link>
            <guid>https://velog.io/@dev_osoli/TypeScript-%ED%83%80%EC%9E%85-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%86%8C%EA%B0%9C</guid>
            <pubDate>Mon, 02 Feb 2026 12:24:22 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>기존의 JavaScript는 자유롭다는 장점이 있지만 동적 타입이라는 이유로 타입 관련 버그가 굉장히 많이 발생했습니다. 실행(런타임)을 해야지 타입이 결정되기 때문에 VS Code 같은 에디터에서 자동완성, 리펙토링을 지원하기 어려웠습니다. 버그를 잡기 어려울 뿐만 아니라 코드 이해가 어려워 파라미터·반환값의 문서화가 필요했고, 팀원 간의 설명이 필요했습니다. 이러한 문제를 해결하고 JavaScript의 유연함을 지키기 위해 TypeScript가 나왔습니다. 이 글에서 타입스크립트에 대해서 간략히 소개해보겠습니다. </p>
</blockquote>
<h2 id="1-정적-타입-vs-동적-타입-그리고-타입스크립트">1. 정적 타입 vs 동적 타입, 그리고 타입스크립트</h2>
<p>📋 정적 타입 시스템 (Static Type System)</p>
<ul>
<li>특징: 코드 실행 전, 컴파일 단계에서 모든 변수의 타입을 고정합니다.</li>
<li>대표 언어: C, Java 등</li>
<li>장점: 실행 전 오류를 발견할 수 있어 매우 안정적입니다.</li>
<li>단점: 변수 선언 시마다 타입을 지정해야 하므로 타이핑 양이 많고 유연성이 떨어집니다.</li>
</ul>
<p>📋 동적 타입 시스템 (Dynamic Type System)</p>
<ul>
<li>특징: 코드를 실행하면서 변수의 타입을 유동적으로 결정합니다.</li>
<li>대표 언어: Python, JavaScript</li>
<li>장점: 타입 지정 없이 자유롭게 값을 담을 수 있어 코드 작성이 빠르고 유연합니다.</li>
<li>단점: 실행 중에 예상치 못한 타입 오류로 인해 프로그램이 비정상 종료될 위험이 큽니다. </li>
</ul>
<blockquote>
<p><strong>💡 TypeScript: 점진적 타입 시스템 (Gradual Type System)</strong></p>
</blockquote>
<ul>
<li>두 시스템의 장점을 결합하여 <strong>타입 추론(Type Inference)</strong>을 통해 모든 변수에 타입을 일일이 선언하지 않아도 안전성을 확보합니다. </li>
<li>변수에 담기는 초기값을 기준으로 타입을 추론하고 지정하여 에러를 미리 찾아서 실행 전에 사용자에게 경고를 줍니다.  </li>
</ul>
<h2 id="2-동작-원리와-컴파일-과정">2. 동작 원리와 컴파일 과정</h2>
<p>타입스크립트는 자바스크립트로 변환되는 <code>트랜스파일(Transpile)</code> 과정을 거칩니다.</p>
<ul>
<li><strong>일반적인 JS</strong>: JavaScript → AST(추상 문법 트리) → 바이트 코드</li>
<li>TypeScript의 과정: <ol>
<li>TypeScript 코드를 읽어 AST를 생성합니다. (공백, 주석 제거 후 트리 구조화)</li>
<li><strong>타입 검사(Type Checking)</strong>를 수행합니다. 이 단계에서 실패하면 컴파일이 종료됩니다.</li>
<li>검사가 통과되면 JavaScript 코드로 변환됩니다.</li>
<li>변환된 JS가 다시 AST를 거쳐 컴퓨터가 이해하는 바이트 코드가 됩니다.</li>
</ol>
</li>
</ul>
<blockquote>
<ul>
<li>AST : 소스코드를 해석해 추상 문법 트리(AST)를 만듭니다. (주석, 공백 등 제거)</li>
</ul>
</blockquote>
<ul>
<li>타입 검사 (Type Checking): AST를 바탕으로 타입 오류가 없는지 확인합니다. 에러 발생 시 여기서 중단됩니다.</li>
<li>JavaScript 변환: 타입 관련 코드가 모두 제거된 순수 자바스크립트 코드가 생성됩니다.</li>
<li>실행: 생성된 JS가 엔진(V8 등)에 의해 바이트 코드로 변환되어 실행됩니다.</li>
</ul>
<h2 id="3-개발-생산성-혁신">3. 개발 생산성 혁신</h2>
<p>TypeScript는 IDE통합으로 개발 경험을 완전히 바꿔났습니다.</p>
<p>✅ 자동완성(IntelliSense): 함수 파라미터·반환 타입 실시간 표시
✅ 리팩토링 안전: 변수명 변경 → 모든 사용처 자동 업데이트<br>✅ 네비게이션: Ctrl+클릭으로 정의부 즉시 이동, 호버로 타입 힌트
✅ Quick Fix: 에디터가 타입 에러 자동 수정 제안</p>
<pre><code class="language-tsx">// ❌ JS: IDE가 모름(실행 전까지 에러를 못 찾음)
function getUser(id) {
  return fetch(`/api/users/${id}`).then(r =&gt; r.json())
}

// ✅ TS: 완벽 지원
function getUser(id: number): Promise&lt;User&gt; {
  return fetch(`/api/users/${id}`).then(r =&gt; r.json())
}</code></pre>
<blockquote>
<p>JavaScript에서는 <code>id</code> 타입을 컴파일러가 알 수 없기에 <code>id.toUpperCase()</code> 같은 String 메서드를 사용한다면 에러가 안 나옵니다. 실행 후, <code>id</code>에 숫자가 들어오는 즉시 에러가 발생합니다. 그에 반해 TypeScript는 입력 즉시 빨간줄로 수정 제안이 나옵니다. </p>
</blockquote>
<h2 id="4-ts-vs-tsx-차이점">4. TS vs TSX 차이점</h2>
<p>두 확장자의 차이는 <code>.tsx</code>는 JSX 문법을 지원하고, <code>.ts</code>는 순수 TypeScript만 가능합니다. 
<code>.tsx</code>는 JavaScript 요소를 넣으면 JavaScript로 변환하고 실행까지 진행하지만, <code>.ts</code>는 이러한 과정이 없습니다. </p>
<pre><code class="language-ts">❌ components/Button.ts    // JSX 넣으면 컴파일 에러!
const Button = () =&gt; {
  return &lt;div&gt;Click me&lt;/div&gt;  // 빨간줄! ❌
}

✅ components/Button.tsx   // 문제없이 동작
const Button = () =&gt; {
  return &lt;div&gt;Click me&lt;/div&gt;  // 초록색! ✅
}

✅ utils/api.ts           // 순수 로직은 .ts
export function getUser(id: number): Promise&lt;User&gt; {
  return fetch(`/api/users/${id}`).then(r =&gt; r.json())
}</code></pre>
<pre><code class="language-text">.ts 파일
├── TypeScript → 타입 체크
└── JavaScript 출력

.tsx 파일  
├── TypeScript → 타입 체크  
├── JSX → React.createElement 변환
└── JavaScript 출력</code></pre>
<p>이러한 이유에는 성능과 역할을 명확히 하기 위해서 입니다. </p>
<h3 id="장점1-빌드-속도-최적화">장점1. 빌드 속도 최적화</h3>
<pre><code class="language-ts">❌ utils/api.tsx    // 불필요한 JSX 변환 과정 발생 → 빌드 20% 느려짐
✅ utils/api.ts     // 순수 TS만 → 변환 과정 0개, 빠른 컴파일</code></pre>
<blockquote>
<p>tsx 컴파일러는 모든 파일에 대해 JSX -&gt; React.createElement 변환을 시도합니다. JSX 없는 파일에서 이 과정은 낭비입니다. 그래서 tsx 파일은 React 컴포넌트를, ts 파일은 API, 타입, 훅 등을 정의해둡니다. </p>
</blockquote>
<h3 id="장점2-코드-역할-즉시-파악">장점2. 코드 역할 즉시 파악</h3>
<pre><code>파일명만 봐도 역할 파악 가능:
├── api.ts          # API 로직 (순수 TS)
├── types/user.ts   # 타입 정의 (순수 TS)  
├── hooks/useUser.ts # 커스텀 훅 (순수 TS)
└── UserCard.tsx    # React 컴포넌트 (JSX)</code></pre><blockquote>
<p>ts, tsx로 나누는 것은 파일 확장자로 컴포넌트와 로직인지 구분할 수 있다는 장점이 있습니다. </p>
</blockquote>
<h2 id="5-ai-시대-typescript가-필수인-이유">5. AI 시대, TypeScript가 필수인 이유</h2>
<p>생성형AI 코드 생성 도구들은 <strong>TypeScript</strong>를 강력히 선호합니다. </p>
<p><a href="https://github.blog/developer-skills/programming-languages-and-frameworks/typescripts-rise-in-the-ai-era-insights-from-lead-architect-anders-hejlsberg/">GitHub 2025 Octoverse 공식 통계</a>에 따르면 </p>
<p>📈 TypeScript 성장률: <strong>66%</strong> (JavaScript 추월 1위)
🤖 GitHub Copilot 사용률: TS 프로젝트 <strong>2배</strong> 높음</p>
<blockquote>
<p>Anders Hejlsberg (TypeScript 설계자): &quot;AI는 타입 있는 언어에서 훨씬 정확하다&quot;</p>
</blockquote>
<h3 id="왜-ai가-typescript를-좋아하나">왜 AI가 TypeScript를 좋아하나?</h3>
<ol>
<li>명확한 의도 전달<pre><code class="language-ts">❌ JS: function(id) {}  // id 타입? 반환값?
✅ TS: function(id: number): Promise&lt;User&gt; {}  // AI가 100% 이해!</code></pre>
AI → &quot;이 함수는 숫자 ID로 User를 반환한다&quot; 즉시 파악    </li>
<li>압도적인 정확도 차이</li>
</ol>
<p>실제로 Copilot과 같은 도구를 사용할 때의 실험 결과에 따르면, 타입의 유무에 따라 제안의 질이 달라집니다.</p>
<ul>
<li>정확도: TS(약 92%) vs JS(약 78%)</li>
<li>효과:<ul>
<li>Props 타입 자동완성 정확도 95% 이상</li>
<li>리팩토링 및 코드 수정 제안의 안전성 90% 이상 확보</li>
<li>반면, JS에서 any 타입을 남발할 경우 부정확한 제안이 약 <strong>40%</strong>까지 증가할 수 있습니다.</li>
</ul>
</li>
</ul>
<h2 id="마무리하며">마무리하며</h2>
<blockquote>
<p>타입스크립트는 이제 단순한 &#39;에러 방지용&#39; 도구를 넘어, AI와 협업하며 생산성을 극대화하기 위한 필수 언어가 되었습니다. 명확한 타입 정의는 나뿐만 아니라 동료 개발자, 그리고 우리가 사용하는 AI 도구에게도 가장 강력한 힌트가 됩니다.</p>
</blockquote>
<p>지금 바로 tsconfig.json을 설정하고, AI와 함께 더 정확하고 안전한 코딩을 시작해 보세요! 다음 시간에는 타입스크립트의 핵심 설정 파일인 <code>tsconfig.json</code>에 대해 다루겠습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI 파이프라인 스크립트 ]]></title>
            <link>https://velog.io/@dev_osoli/FastAPI-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8</link>
            <guid>https://velog.io/@dev_osoli/FastAPI-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8</guid>
            <pubDate>Sun, 25 Jan 2026 12:12:40 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이전 포스트(<a href="https://velog.io/@dev_osoli/Git-%EC%97%B0%EB%8F%99-Webhook-%EB%B8%8C%EB%9E%9C%EC%B9%98PR-%ED%8A%B8%EB%A6%AC%EA%B1%B0-%EC%84%A4%EA%B3%84"><strong>Git 연동 &amp; Webhook, 브랜치/PR 트리거 설계</strong></a>)에서 Jenkins와 GitHub Webhook 연동으로 <code>develop</code> 브랜치 PR merge 시 자동 트리거 설정을 완료했습니다. 이번 글에서는 <strong>FastAPI</strong> 프로젝트를 <strong>Docker</strong> 이미지로 빌드 → tar 전송 → 원격 배포하는 파이프라인을 설명합니다.</p>
</blockquote>
<h2 id="파이프라인-전체-구조">파이프라인 전체 구조</h2>
<p>파이프라인은 5단계로 구성되어 있습니다.</p>
<ol>
<li>Pre-cleaning: Docker 디스크 정리</li>
<li>자동 버전 태깅: Git 태그 추출 및 빌드명 설정</li>
<li>SSH tar 전송: 이미지 tar 원격 전송</li>
<li>원격 배포: docker-compose 재실행</li>
</ol>
<h2 id="주요-특징">주요 특징</h2>
<ul>
<li><strong>Docker 용량 자동 정리</strong> (Pre-cleaning)</li>
<li><strong>Git 태그 듀얼 태깅</strong> (<code>example:latest</code> + <code>example:v1.2.3</code>)</li>
<li><strong>대용량 tar 전송</strong> (이미지 빌드 생략)</li>
<li><strong>docker-compose 통합 배포</strong></li>
</ul>
<h2 id="전체-jenkinsfile-스크립트">전체 Jenkinsfile 스크립트</h2>
<pre><code class="language-groovy">pipeline {
  agent any

  options {
    timestamps()
    timeout(time: 40, unit: &#39;MINUTES&#39;)
  }

  environment {
    // 원격 서버 정보
    REMOTE_HOST = &quot;xx.xx.xxx&quot;
    // 원격 서버에서 Docker 컨테이너 실행 유저
    REMOTE_USER = &quot;ubuntu&quot;
    // Jenkins에 등록된 SSH Key ID
    SSH_CRED_ID = &quot;web-server-example&quot;
    DEPLOY_ROOT = &quot;/home/ubuntu/app/jenkins/example&quot;

    // 이미지 및 파일 이름 정의
    LOCAL_FASTAPI_DIR = &quot;.&quot; 
    IMG_FASTAPI = &quot;example:latest&quot;
    TAR_FASTAPI = &quot;example-fastapi.tar&quot;
    COMPOSE_FILE = &quot;docker-compose.deploy.yml&quot;
  }

  tools {
  }

   stages {
   // 1️⃣ 용량 확보 (Pre-cleaning)
    stage(&#39;🧹 Pre-Cleanup &amp; Build&#39;) {
      steps {
        script {
          echo &quot;🧹 Pre-cleaning Docker space...&quot;
          sh &quot;docker image prune -f&quot;
          sh &quot;docker images example:* | grep -v &#39;latest&#39; | awk &#39;{print \$3}&#39; | xargs -r docker rmi -f || true&quot;
          sh &quot;docker builder prune -f --filter &#39;until=24h&#39;&quot;
          // 용량 확인 
          sh &quot;df -h .&quot;

          // 2️⃣ Git 태그 &amp; 빌드명 설정
          def gitVer = sh(script: &quot;git describe --tags --always&quot;, returnStdout: true).trim()
          currentBuild.displayName = &quot;#${BUILD_NUMBER}-${gitVer}&quot;
          def imgTagged = &quot;chatbot-ai:${gitVer}&quot;

          // 3. 이미지 빌드 &amp; tar 저장
          echo &quot;🐳 Building ${imgTagged}...&quot;
          sh &quot;docker build -t ${imgTagged} -t ${IMG_FASTAPI} ${LOCAL_FASTAPI_DIR}&quot;
          sh &quot;docker save -o ${TAR_FASTAPI} ${imgTagged} ${IMG_FASTAPI}&quot;
        }
      }
    }

    // 3️⃣ 원격 서버로 tar 파일 전송
    stage(&#39;Transfer to Remote&#39;) {
      steps {
        withCredentials([sshUserPrivateKey(credentialsId: env.SSH_CRED_ID, keyFileVariable: &#39;SSH_KEY&#39;)]) {
          script {
            echo &quot;🚀 Transferring artifacts to $REMOTE_HOST...&quot;

            // 1. 원격 디렉토리 생성
            sh &#39;&#39;&#39;
              ssh -i &quot;$SSH_KEY&quot; -o StrictHostKeyChecking=no &quot;$REMOTE_USER@$REMOTE_HOST&quot; &quot;mkdir -p $DEPLOY_ROOT&quot;
            &#39;&#39;&#39;

            // 2. FASTAPI Tar 파일 전송
            sh &#39;&#39;&#39;
              scp -i &quot;$SSH_KEY&quot; -o StrictHostKeyChecking=no &quot;$TAR_FASTAPI&quot; &quot;$REMOTE_USER@$REMOTE_HOST:$DEPLOY_ROOT/&quot;
            &#39;&#39;&#39;

            // 3. 전송 후 로컬 캐시 삭제
            sh &#39;&#39;&#39;
              rm &quot;$TAR_FASTAPI&quot;
            &#39;&#39;&#39;
          }
        }
      }
    }

    // 4️⃣ 원격 배포 실행 (All Services Restart)
    stage(&#39;Remote Deploy&#39;) {
      steps {
        withCredentials([sshUserPrivateKey(credentialsId: env.SSH_CRED_ID, keyFileVariable: &#39;SSH_KEY&#39;)]) {
          script {
            echo &quot;🚀 Executing remote deployment on $REMOTE_HOST...&quot;

            // 원격 서버 접속 후 이미지 로드 및 컨테이너 재기동
            sh &#39;&#39;&#39;
              ssh -i &quot;$SSH_KEY&quot; -o StrictHostKeyChecking=no &quot;$REMOTE_USER@$REMOTE_HOST&quot; &quot;
                cd $DEPLOY_ROOT

                # 1. 전체 서비스 종료
                docker compose -f $COMPOSE_FILE down

                # 2. 로그 디렉토리 생성 및 권한 설정 (755)
                mkdir -p logs/fastapi
                chmod -R 755 logs 2&gt;/dev/null || echo &#39;ℹ️  Log directory exists (skipping permission change)&#39;

                # 3. 새로운 FASTAPI 이미지 로드
                docker load -i $TAR_FASTAPI

                # 4. 전체 서비스 실행
                docker compose -f $COMPOSE_FILE up -d --no-build

                # 5. 서버 용량 확보를 위해 tar 파일 삭제
                rm $TAR_FASTAPI

                # 6. 미사용 이미지 정리
                docker image prune -f
              &quot;
            &#39;&#39;&#39;
          }
        }
      }
    }
  }

  post {
    success { echo &quot;✅ FASTAPI Deployment Successful&quot; }
    failure { echo &quot;❌ Failed&quot; }
    cleanup { sh &quot;rm -f *.tar&quot; }
  }
}</code></pre>
<h2 id="단계별-상세-설명">단계별 상세 설명</h2>
<h3 id="1-디스크-용량-최적화-pre-cleaning">1. 디스크 용량 최적화 (Pre-cleaning)</h3>
<pre><code class="language-bash">docker image prune -f                    # Dangling 이미지 삭제 (~100MB)
docker images example:* | grep -v latest | awk &#39;{print $3}&#39; | xargs docker rmi -f  # 구버전 선택 삭제
docker builder prune -f --filter &#39;until=24h&#39;  # 24시간 경과 빌드캐시 삭제
df -h .  # 현재 디스크 상태 출력</code></pre>
<h3 id="2-자동-버전-태깅">2. 자동 버전 태깅</h3>
<ul>
<li>git describe --tags --always → v1.2.3 또는 HEAD 추출</li>
<li>듀얼 태깅: chatbot-ai:v1.2.3 + chatbot-ai:latest</li>
<li>docker save로 tar 압축 (~500MB)</li>
</ul>
<pre><code class="language-text">🧹 Pre-cleaning Docker space...
Deleted Images: sha256:xxx (500MB)
Building example:v1.2.3...
Successfully tagged example:latest</code></pre>
<h3 id="3-ssh로-산출물tar-전송">3. SSH로 산출물(tar) 전송</h3>
<p>SSH/SCP 동작</p>
<pre><code>ssh -i $SSH_KEY ... &quot;mkdir -p /home/ubuntu/app/jenkins/example&quot;
scp -i $SSH_KEY example-fastapi.tar ubuntu@xx.xx.xxx:/home/ubuntu/app/jenkins/example/
rm example-fastapi  # 로컬 즉시 삭제</code></pre><p>특징: tar 파일(~500MB)만 전송, 소스코드 제외</p>
<h3 id="4-원격-배포-자동화">4. 원격 배포 자동화</h3>
<p>원격 서버 실행 순서</p>
<pre><code class="language-bash">cd /home/ubuntu/app/jenkins/example
docker compose down               # 1. 서비스 중단 (0 downtime 아님)
mkdir -p logs/fastapi             # 2. 로그 디렉토리
docker load -i example-fastapi    # 3. 이미지 로드 (~30초)
docker compose up -d --no-build   # 4. 재시작 (FastAPI 새 이미지 사용)
rm example-fastapi                # 5. tar 삭제
docker image prune -f             # 6. 정리</code></pre>
<p>핵심 포인트</p>
<ul>
<li>--no-build: 로컬 이미지 사용으로 빌드 생략</li>
<li>logs 디렉토리 755 권한: 컨테이너 appuser 쓰기 보장</li>
</ul>
<h2 id="배포-서버-필수-설정">배포 서버 필수 설정</h2>
<h3 id="1-docker-composeyml">1. docker-compose.yml</h3>
<p>원격 서버의 docker-compose.yml 예시:</p>
<pre><code>version: &#39;3.8&#39;
services:
  example:
    image: example:latest
    ports:
      - &quot;8080:8080&quot;
    volumes:
      - ./logs:/app/logs
    restart: unless-stopped</code></pre><p>위치: </p>
<pre><code>/home/ubuntu/app/jenkins/example/docker-compose.yml</code></pre><h3 id="2-환경-변수-정리">2. 환경 변수 정리</h3>
<table>
<thead>
<tr>
<th>변수</th>
<th>값</th>
<th>용도</th>
</tr>
</thead>
<tbody><tr>
<td><code>REMOTE_HOST</code></td>
<td><code>xx.xx.xxx</code></td>
<td>배포 대상 서버</td>
</tr>
<tr>
<td><code>SSH_CRED_ID</code></td>
<td><code>web-server-example</code></td>
<td>Jenkins SSH 자격증명</td>
</tr>
<tr>
<td><code>DEPLOY_ROOT</code></td>
<td><code>/home/ubuntu/app/jenkins/example</code></td>
<td>원격 배포 경로</td>
</tr>
<tr>
<td><code>IMG_FASTAPI</code></td>
<td><code>example:latest</code></td>
<td>Docker 이미지명</td>
</tr>
<tr>
<td><code>TAR_FASTAPI</code></td>
<td><code>example-fastapi.tar</code></td>
<td>전송용 tar 파일</td>
</tr>
</tbody></table>
<h3 id="3-배포-시나리오">3. 배포 시나리오</h3>
<pre><code>1. GitHub: develop PR → merge (Webhook 발생)
2. Jenkins: Generic Webhook Trigger → 파이프라인 실행
3. 로컬: Docker 빌드 → tar 생성 (example:v1.2.3, latest)
4. SSH: tar 전송 → 원격 docker load
5. 원격: docker-compose 재시작 → FastAPI 배포</code></pre><h2 id="장단점">장단점</h2>
<h3 id="장점">장점:</h3>
<ul>
<li>완전 자동화: PR merge → 배포까지 1클릭 불필요</li>
<li>안정성: Pre-cleaning으로 용량 문제 해결</li>
<li>추적성: Git 태그로 이미지 버전 관리</li>
</ul>
<h3 id="단점">단점:</h3>
<ul>
<li>tar 전송 시간: 이미지 크기에 비례 (최적화 권장)</li>
<li>SSH 의존: 키 관리 및 네트워크 안정성 필요</li>
<li>단일 실패점: Jenkins 노드 장애 시 배포 중단</li>
</ul>
<h2 id="도입-체크리스트">도입 체크리스트</h2>
<pre><code>✅ Jenkins + Generic Webhook Trigger 플러그인 설치
✅ GitHub Webhook: /generic-webhook-trigger/invoke?token=XXX
✅ docker-compose.deploy.yml (FastAPI 정의)
✅ SSH 자격증명 등록 (web-server-example)
✅ 원격 서버: docker-compose, logs 디렉토리 권한 확인</code></pre><blockquote>
<p>필자는 Webhook + Docker tar 배포 조합이 보안성이 엄격한 곳이거나 소규모 팀에서 가장 실용적이라고 봅니다. Docker 레지스트리를 받을 수 없기에 SSH/SCP로 배포를 어쩔 수 없이 하는 경우와 규모가 작아 SSH/SCP를 전송할 때 용량이 크게 고려할 점이 아니라면 좋은 방식이라 생각합니다. </p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot 파이프라인 스크립트]]></title>
            <link>https://velog.io/@dev_osoli/Spring-Boot-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8</link>
            <guid>https://velog.io/@dev_osoli/Spring-Boot-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8</guid>
            <pubDate>Wed, 21 Jan 2026 12:51:15 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이전 포스트(<a href="https://velog.io/@dev_osoli/Git-%EC%97%B0%EB%8F%99-Webhook-%EB%B8%8C%EB%9E%9C%EC%B9%98PR-%ED%8A%B8%EB%A6%AC%EA%B1%B0-%EC%84%A4%EA%B3%84"><strong>Git 연동 &amp; Webhook, 브랜치/PR 트리거 설계</strong></a>)에서 Jenkins와 GitHub Webhook 연동으로 <code>develop</code> 브랜치 PR merge 시 자동 트리거 설정을 완료했습니다. 이번 글에서는 <strong>Spring Boot</strong> 프로젝트를 <strong>Docker</strong> 이미지로 빌드 → tar 전송 → 원격 배포하는 파이프라인을 설명합니다.</p>
</blockquote>
<h2 id="파이프라인-전체-구조">파이프라인 전체 구조</h2>
<p>파이프라인은 5단계로 구성되어 있습니다.</p>
<ol>
<li>Pre-cleaning: Docker 디스크 정리</li>
<li>자동 버전 태깅: Git 태그 추출 및 빌드명 설정</li>
<li>JAR 빌드: Gradle bootJar 생성</li>
<li>SSH tar 전송: 이미지 tar 원격 전송</li>
<li>원격 배포: docker-compose 재실행</li>
</ol>
<h2 id="주요-특징">주요 특징</h2>
<ul>
<li><strong>Docker 용량 자동 정리</strong> (Pre-cleaning)</li>
<li><strong>Git 태그 듀얼 태깅</strong> (<code>example:latest</code> + <code>example:v1.2.3</code>)</li>
<li><strong>대용량 tar 전송</strong> (이미지 빌드 생략)</li>
<li><strong>docker-compose 통합 배포</strong></li>
</ul>
<h2 id="전체-jenkinsfile-스크립트">전체 Jenkinsfile 스크립트</h2>
<pre><code class="language-groovy">pipeline {
  agent any
  options {
    timestamps()
    timeout(time: 40, unit: &#39;MINUTES&#39;)
  }
  environment {
    // [필수 설정] 원격 서버 접속 정보
    REMOTE_HOST = &quot;xx.xx.xxx&quot;
    // 원격 서버에서 Docker 컨테이너 실행 유저
    REMOTE_USER = &quot;ubuntu&quot;
    // Jenkins에 등록된 SSH Key ID
    SSH_CRED_ID = &quot;web-server-example&quot;
    DEPLOY_ROOT = &quot;/home/ubuntu/app/jenkins/example&quot;

    // 이미지 및 파일 이름 정의
    IMG_SPRING = &quot;example:latest&quot;
    TAR_SPRING = &quot;example-spring.tar&quot;
  }
  tools {
    jdk &quot;JDK 21&quot;
    gradle &quot;Gradle 8.13&quot;
  }
  stages {
    stage(&#39;Build JAR&#39;) {
      steps {
        script {
          // gradlew로 clean -&gt; bootJar(JAR생성) -&gt; 테스트 실행 생략
          sh &quot;chmod +x gradlew&quot;
          sh &quot;./gradlew clean bootJar -x test&quot;
        }
      }
    }
    stage(&#39;Build &amp; Save Image&#39;) {
      steps {
        script {
          // 🚨 디스크 용량 확보 (Pre-cleaning)
          echo &quot;🧹 Docker space optimization...&quot;
          sh &quot;docker image prune -f&quot;
          sh &quot;docker builder prune -f --filter &#39;until=24h&#39;&quot;

          // Git Short Hash 추출
          def gitVer = sh(script: &quot;git describe --tags --always&quot;, returnStdout: true).trim()

          // 이미지 태그 설정
          def baseImageName = IMAGE_NAME.replace(&quot;:latest&quot;, &quot;&quot;)
          def imgTagged = &quot;${baseImageName}:${gitVer}&quot;


          echo &quot;🐳 Building Docker Image (Tags: ${gitVer}, latest)...&quot;
          sh &quot;&quot;&quot;
            docker build --no-cache \
              -t $IMG_SPRING \
              -t ${imgTagged} .
          &quot;&quot;&quot;
          echo &quot;📦 Archiving Image to Tar...&quot;
          sh &quot;docker save -o $TAR_SPRING $IMG_SPRING ${imgTagged}&quot;
        }
      }
    }
    // 2️⃣ 원격 서버로 tar 파일 전송
    stage(&#39;Transfer to Remote&#39;) {
      steps {
        withCredentials([sshUserPrivateKey(credentialsId: env.SSH_CRED_ID, keyFileVariable: &#39;SSH_KEY&#39;)]) {
          script {
            echo &quot;🚀 Transferring artifacts to $REMOTE_HOST...&quot;

            // 원격 배포 폴더 생성 (이미 있으면 무시)
            sh &#39;ssh -i &quot;$SSH_KEY&quot; -o StrictHostKeyChecking=no &quot;$REMOTE_USER@$REMOTE_HOST&quot; &quot;mkdir -p $DEPLOY_ROOT&quot;&#39;

            // Docker 이미지 tar 파일 전송
            sh &#39;scp -i &quot;$SSH_KEY&quot; -o StrictHostKeyChecking=no &quot;$TAR_SPRING&quot; &quot;$REMOTE_USER@$REMOTE_HOST:$DEPLOY_ROOT/&quot;&#39;
            sh &#39;rm &quot;$TAR_SPRING&quot;&#39;
          }
        }
      }
    }

    // 3️⃣ 원격 배포 및 서비스 재시작
    stage(&#39;Remote Deploy&#39;) {
      steps {
        withCredentials([sshUserPrivateKey(credentialsId: env.SSH_CRED_ID, keyFileVariable: &#39;SSH_KEY&#39;)]) {
          script {
            echo &quot;🚀 Executing remote deployment on $REMOTE_HOST...&quot;

            // 원격 서버 접속 후 이미지 로드 및 컨테이너 재기동
            sh &#39;&#39;&#39;
              ssh -i &quot;$SSH_KEY&quot; -o StrictHostKeyChecking=no &quot;$REMOTE_USER@$REMOTE_HOST&quot; &quot;
                cd $DEPLOY_ROOT

                docker compose down

                mkdir -p logs
                chmod -R 755 logs 2&gt;/dev/null || true

                docker load -i $TAR_SPRING

                docker compose up -d --no-build

                rm $TAR_SPRING
                docker image prune -f
              &quot;
            &#39;&#39;&#39;
          }
        }
      }
    }
  }

  // ✅ 파이프라인 종료 후 후처리
  post {
    success { echo &quot;✅ Spring Boot Deployment Successful!&quot; }
    failure { echo &quot;❌ Deployment Failed&quot; }
    cleanup { 
      sh &quot;rm -f *.tar&quot; 
      sh &quot;docker image prune -f&quot;
    }
  }
}</code></pre>
<h2 id="단계별-상세-설명">단계별 상세 설명</h2>
<h3 id="1-디스크-용량-최적화-pre-cleaning">1. 디스크 용량 최적화 (Pre-cleaning)</h3>
<pre><code>sh &quot;docker image prune -f&quot;                    // Dangling 이미지 삭제
sh &quot;docker builder prune -f --filter &#39;until=24h&#39;&quot; // 24시간 경과 캐시 삭제</code></pre><p><strong>로컬/원격 모두 적용</strong>으로 <code>No space left on device</code> 오류 방지합니다.</p>
<h3 id="2-자동-버전-태깅">2. 자동 버전 태깅</h3>
<pre><code class="language-groovy">def gitVer = sh(script: &quot;git describe --tags --always&quot;, returnStdout: true).trim()
currentBuild.displayName = &quot;#${BUILD_NUMBER}-${gitVer}&quot;
env.GIT_VER = gitVer  // 전역 변수 저장</code></pre>
<p>결과: <code>#13-v1.0.0</code> 또는 <code>#13-v1.0.0-3-gabc123</code></p>
<h3 id="3-jar-빌드">3. JAR 빌드</h3>
<pre><code class="language-groovy">sh &quot;./gradlew clean bootJar -x test&quot;</code></pre>
<ul>
<li><code>clean</code> : 이전 빌드 삭제</li>
<li><code>bootJar</code> : 실행가능 fat JAR 생성</li>
<li><code>-x test</code> : 배포용 테스트 생략</li>
</ul>
<h3 id="4-ssh로-산출물tar-전송">4. SSH로 산출물(tar) 전송</h3>
<pre><code class="language-groovy">sh &#39;ssh -i &quot;$SSH_KEY&quot; -o StrictHostKeyChecking=no &quot;$REMOTE_USER@$REMOTE_HOST&quot; &quot;mkdir -p $DEPLOY_ROOT&quot;&#39;
sh &#39;scp -i &quot;$SSH_KEY&quot; -o StrictHostKeyChecking=no &quot;$TAR_SPRING&quot; &quot;$REMOTE_USER@$REMOTE_HOST:$DEPLOY_ROOT/&quot;&#39;</code></pre>
<p>특징: tar 파일(~500MB)만 전송, 소스코드 제외</p>
<h3 id="5-원격-배포-자동화">5. 원격 배포 자동화</h3>
<pre><code class="language-groovy">sh &#39;&#39;&#39;
  ssh -i &quot;$SSH_KEY&quot; -o StrictHostKeyChecking=no &quot;$REMOTE_USER@$REMOTE_HOST&quot; &quot;
    cd $DEPLOY_ROOT
    docker compose down
    docker load -i $TAR_SPRING
    docker compose up -d --no-build
    rm $TAR_SPRING
    docker image prune -f
  &quot;
&#39;&#39;&#39;</code></pre>
<p>핵심: <code>--no-build</code>로 tar 로드 이미지만 사용</p>
<h2 id="배포-서버-필수-설정">배포 서버 필수 설정</h2>
<h3 id="1-docker-composeyml">1. docker-compose.yml</h3>
<p>원격 서버의 docker-compose.yml 예시:</p>
<pre><code class="language-yaml">version: &#39;3.8&#39;
services:
  example:
    image: example:latest
    ports:
      - &quot;8080:8080&quot;
    volumes:
      - ./logs:/app/logs
    restart: unless-stopped</code></pre>
<p><strong>위치</strong>: </p>
<pre><code class="language-yaml">/home/ubuntu/app/jenkins/example/docker-compose.yml</code></pre>
<h3 id="2-jenkins-credential-등록">2. Jenkins Credential 등록</h3>
<pre><code>ID: web-server-credentails
Kind: SSH Username with private key
Username: ubuntu
Private Key: ~/.ssh/id_rsa (서버 SSH 키)</code></pre><blockquote>
<p>Jenkins UI에서 GitHub Credentails 생성하는 곳에서 만들어 줍니다. Private Key 같은 경우 Enter directly 체크하여 .pem을 text 편집기로 열어 나온 내용 전부를 넣어줍니다.</p>
</blockquote>
<h3 id="3-jenkins-서버-tools-플러그인">3. Jenkins 서버 Tools 플러그인</h3>
<p>여기서는 <a href="https://velog.io/@dev_osoli/React-Vite-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8">이전 React + Vite 파이프라인</a>과 다르게 <code>tools</code>를 사용합니다. 이는 Jenkins에서 지원하는 플러그인을 해당 script에서 사용하겠다는 의미입니다. </p>
<p>Jenkins UI &gt; Setting &gt; Tools 에서 JDK와 Gradle을 찾아 설정해 줍니다. </p>
<ul>
<li>JDK<ul>
<li>name : JDK 21</li>
<li>Install automatically 체크 </li>
<li>Version : 21버전 중에 선택 </li>
</ul>
</li>
<li>Gradle<ul>
<li>name : Gradle 8.13</li>
<li>Install automatically 체크 </li>
<li>Version : Gradle 8.13 선택</li>
</ul>
</li>
</ul>
<blockquote>
<p>참고로 해당 <code>name</code>에 작성한 이름은 script의 tools에 명시된 이름과 동일해야 합니다. </p>
</blockquote>
<h2 id="트러블슈팅">트러블슈팅</h2>
<table>
<thead>
<tr>
<th><strong>문제</strong></th>
<th><strong>원인</strong></th>
<th><strong>해결법</strong></th>
</tr>
</thead>
<tbody><tr>
<td><code>No space left</code></td>
<td>디스크 부족</td>
<td>Pre-cleaning 단계 확인</td>
</tr>
<tr>
<td><code>Permission denied</code></td>
<td>SSH 권한</td>
<td>Credential Private Key 재입력</td>
</tr>
<tr>
<td><code>docker compose not found</code></td>
<td>플러그인 미설치</td>
<td><code>apt install docker-compose-plugin</code></td>
</tr>
<tr>
<td><code>Image load 실패</code></td>
<td>tar 손상</td>
<td><code>docker load -i</code> 로그 확인</td>
</tr>
</tbody></table>
<h2 id="장단점">장단점</h2>
<p><strong>장점</strong>:</p>
<ul>
<li><strong>Pre-cleaning</strong>으로 안정적 디스크 관리</li>
<li>Git 태그 듀얼 태깅으로 롤백 용이</li>
<li><code>-no-build</code>로 배포 속도 최적화</li>
<li>post cleanup으로 Jenkins 에이전트 정리</li>
</ul>
<p><strong>단점</strong>:</p>
<ul>
<li>tar 전송 시간 (이미지 크기 의존)</li>
<li>SSH 네트워크 의존성</li>
<li>단일 서버 구조 확장성 제한</li>
</ul>
<h2 id="도입-시-고려사항">도입 시 고려사항</h2>
<ol>
<li><strong>Slack 알림</strong>: <code>post</code> 블록에 Slack Notifier Plugin 추가</li>
<li><strong>백업</strong>: <code>docker compose pull</code>로 이미지 사전 백업</li>
<li><strong>보안</strong>: SSH 키 로테이션 및 IP 화이트리스트</li>
</ol>
<h2 id="마무리">마무리</h2>
<p>이 파이프라인은 <strong>Spring Boot → Docker tar → 원격 배포</strong>를 완전 자동화된 Spring Boot 배포 플로우를 제공합니다. 
젠킨스 서버의 스펙 및 프로젝트 규모에 따라 다르겠지만 필자는 이 구조로 <strong>PR → 4분 배포</strong>를 안정적으로 운영했습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React + Vite 파이프라인 스크립트 ]]></title>
            <link>https://velog.io/@dev_osoli/React-Vite-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8</link>
            <guid>https://velog.io/@dev_osoli/React-Vite-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8</guid>
            <pubDate>Wed, 21 Jan 2026 12:08:24 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이전 포스트(<a href="https://velog.io/@dev_osoli/Git-%EC%97%B0%EB%8F%99-Webhook-%EB%B8%8C%EB%9E%9C%EC%B9%98PR-%ED%8A%B8%EB%A6%AC%EA%B1%B0-%EC%84%A4%EA%B3%84">Git 연동 &amp; Webhook, 브랜치/PR 트리거 설계</a>)에서 Jenkins와 GitHub Webhook을 연동해 <code>develop</code> 브랜치로의 PR merge 시 파이프라인이 자동으로 트리거되도록 설정했습니다.<br>이번 글에서는 해당 트리거를 기반으로, <strong>React + Vite</strong> 환경의 클라이언트 애플리케이션을 <strong>Docker 기반으로 빌드하고 원격 서버에 배포</strong>하는 파이프라인 Jenkinsfile 스크립트를 구성해 보겠습니다.</p>
</blockquote>
<h2 id="파이프라인-전체-구조">파이프라인 전체 구조</h2>
<p>아래 Jenkinsfile은 크게 3단계로 구성되어 있습니다.</p>
<ol>
<li><strong>Build &amp; Save Image</strong>: 로컬에서 Docker 이미지를 빌드하고 <code>.tar</code> 파일로 저장  </li>
<li><strong>Transfer to Remote</strong>: SCP를 이용해 빌드된 이미지를 원격 서버로 전송  </li>
<li><strong>Remote Deploy</strong>: 원격 서버에서 컨테이너 재시작 및 이미지 정리  </li>
</ol>
<p>각 단계는 Git 커밋 hash를 기반으로 버전 태깅을 자동화하여, 빌드 추적과 롤백이 용이하도록 설계했습니다.</p>
<h3 id="주요-특징">주요 특징</h3>
<ul>
<li><strong>Git 커밋 hash 자동 태깅</strong> (롤백 용이)</li>
<li><strong>듀얼 태그</strong> (<code>latest</code> + <code>v1a2b3c</code>)</li>
<li><strong>원격 배포</strong> (SSH + docker-compose)</li>
<li><strong>자동 정리</strong> (디스크 용량 문제 방지)</li>
</ul>
<h2 id="전체-스크립트-및-주석">전체 스크립트 및 주석</h2>
<pre><code class="language-groovy">pipeline {
    agent any
    options {
        timestamps() // 콘솔 로그에 타임스탬프 표시
    }

    environment {
        // [필수 설정] 원격 서버 접속 정보
        REMOTE_HOST = &quot;xx.xxx.xxx&quot;
        // 원격 서버에서 Docker 컨테이너 실행 유저
        REMOTE_USER = &quot;ubuntu&quot;
        SSH_CRED_ID = &quot;web-server-credentails&quot; // Jenkins에 등록된 SSH Key ID
        DEPLOY_PATH = &quot;/home/ubuntu/app/jenkins/client&quot;

        // 이미지 및 파일 이름 정의
        IMAGE_NAME = &quot;example-client:latest&quot;
        TAR_NAME = &quot;example-client.tar&quot;

        NODE_OPTIONS = &quot;--max-old-space-size=4096&quot;
    }

     stages {
        stage(&#39;Build &amp; Save Image&#39;) {
          steps {
            script {
              // 🚨 디스크 용량 확보 (Pre-cleaning)
              echo &quot;🧹 Pre-cleaning Docker space...&quot;
              sh &quot;docker image prune -f&quot;
              sh &quot;docker builder prune -f --filter &#39;until=24h&#39;&quot;

              // Git Short Hash 추출
              def gitVer = sh(returnStdout: true, script: &quot;git rev-parse --short HEAD&quot;).trim()

              // 빌드 이름을 &#39;#빌드번호-태그&#39; 형식으로 변경 (예: #13-a1b2c3d)
              currentBuild.displayName = &quot;#${env.BUILD_NUMBER}-${gitVer}&quot;
              echo &quot;🏷️ Building version: ${currentBuild.displayName}&quot;

              // 이미지 태그 설정
              def baseImageName = IMAGE_NAME.replace(&quot;:latest&quot;, &quot;&quot;)
              def imgTagged = &quot;${baseImageName}:${gitVer}&quot;

              echo &quot;🐳 Building Docker Image (Tags: ${gitVer}, latest)...&quot;
              sh &quot;docker build -t ${imgTagged} -t ${IMAGE_NAME} .&quot;

              echo &quot;📦 Archiving Image to Tar...&quot;
              sh &quot;docker save -o ${TAR_NAME} ${imgTagged} ${IMAGE_NAME}&quot;
            }
          }
        }

        // 2️⃣ 원격 서버로 tar 파일 전송
        stage(&#39;Transfer to Remote&#39;) {
            steps {
                withCredentials([sshUserPrivateKey(credentialsId: env.SSH_CRED_ID, keyFileVariable: &#39;SSH_KEY&#39;)]) {
                    script {
                        echo &quot;🚀 Transferring image tar to $REMOTE_HOST...&quot;

                        // 원격 배포 폴더 생성 (이미 있으면 무시)
                        sh &quot;ssh -i \&quot;$SSH_KEY\&quot; -o StrictHostKeyChecking=no \&quot;$REMOTE_USER@$REMOTE_HOST\&quot; \&quot;mkdir -p $DEPLOY_PATH\&quot;&quot;

                        // Docker 이미지 tar 파일 전송
                        sh &quot;scp -i \&quot;$SSH_KEY\&quot; -o StrictHostKeyChecking=no ${TAR_NAME} \&quot;$REMOTE_USER@$REMOTE_HOST:$DEPLOY_PATH/\&quot;&quot;
                    }
                }
            }
        }

        // 3️⃣ 원격 배포 및 서비스 재시작
        stage(&#39;Remote Deploy&#39;) {
            steps {
                withCredentials([sshUserPrivateKey(credentialsId: env.SSH_CRED_ID, keyFileVariable: &#39;SSH_KEY&#39;)]) {
                    script {
                        echo &quot;🚀 Executing remote deployment on $REMOTE_HOST...&quot;

                        // 원격 서버 접속 후 이미지 로드 및 컨테이너 재기동
                        sh &quot;&quot;&quot;
                        ssh -i \&quot;$SSH_KEY\&quot; -o StrictHostKeyChecking=no \&quot;$REMOTE_USER@$REMOTE_HOST\&quot; &quot;
                            cd $DEPLOY_PATH
                            echo &#39;📥 Loading Docker Image...&#39;
                            docker load -i ${TAR_NAME}

                            echo &#39;🔄 Restarting Services...&#39;
                            docker compose down
                            docker compose up -d

                            echo &#39;🧹 Cleaning up...&#39;
                            rm ${TAR_NAME}
                            docker image prune -f
                        &quot;
                        &quot;&quot;&quot;
                    }
                }
            }
        }
    }

    // ✅ 파이프라인 종료 후 후처리
    post {
        success { echo &quot;✅ Client Deployment Successful&quot; }
        failure { echo &quot;❌ Failed&quot; }
        cleanup { 
          sh &quot;rm -f *.tar&quot; 
          sh &quot;docker image prune -f&quot;
      }
   }
}</code></pre>
<h2 id="단계별-상세-설명">단계별 상세 설명</h2>
<h3 id="1-디스크-용량-최적화-pre-cleaning">1. 디스크 용량 최적화 (Pre-cleaning)</h3>
<pre><code class="language-groovy">sh &quot;docker image prune -f&quot;                    // Dangling 이미지 삭제
sh &quot;docker builder prune -f --filter &#39;until=24h&#39;&quot; // 24시간 경과 캐시 삭제</code></pre>
<h3 id="2-자동-버전-태깅">2. 자동 버전 태깅</h3>
<pre><code class="language-groovy">def gitVer = sh(returnStdout: true, script: &quot;git rev-parse --short HEAD&quot;).trim()
// 결과: &quot;a1b2c3d&quot; (7자리 커밋 hash)

def imgTagged = &quot;example-client:a1b2c3d&quot;
sh &quot;docker build -t ${imgTagged} -t example-client:latest .&quot;</code></pre>
<p>빌드 결과: <code>example-client:latest</code> + <code>example-client:a1b2c3d</code></p>
<h3 id="3-ssh로-산출물tar-전송">3. SSH로 산출물(tar) 전송</h3>
<pre><code class="language-groovy">// 원격 배포 폴더 생성 (이미 있으면 무시)
sh &quot;&quot;&quot;
  ssh -i &quot;\$SSH_KEY&quot; \
    -o StrictHostKeyChecking=no \
    &quot;\$REMOTE_USER@\$REMOTE_HOST&quot; \
    &quot;mkdir -p \$DEPLOY_PATH&quot;
&quot;&quot;&quot;

// Docker 이미지 tar 파일 전송
sh &quot;&quot;&quot;
  scp -i &quot;\$SSH_KEY&quot; \
    -o StrictHostKeyChecking=no \
    ${TAR_NAME} \
    &quot;\$REMOTE_USER@\$REMOTE_HOST:\$DEPLOY_PATH/&quot;
&quot;&quot;&quot;</code></pre>
<h3 id="4-원격-배포-자동화">4. 원격 배포 자동화</h3>
<p>원격 서버의 docker-compose.yml 예시:</p>
<pre><code class="language-yaml">version: &#39;3.8&#39;
services:
  example-client:
    image: example-client:latest
    ports:
      - &quot;3000:3000&quot;
    restart: unless-stopped</code></pre>
<h3 id="5-원격-실행">5. 원격 실행</h3>
<p>SSH로 원격 서버를 접속하여 이미지를 로드하고 컨테이너를 재실행 시킵니다. </p>
<pre><code class="language-groovy">// 원격 서버 접속 후 이미지 로드 및 컨테이너 재기동
sh &quot;&quot;&quot;
  ssh -i &quot;\$SSH_KEY&quot; \
    -o StrictHostKeyChecking=no \
    &quot;\$REMOTE_USER@\$REMOTE_HOST&quot; &quot;

    cd \$DEPLOY_PATH

    echo &#39;📥 Loading Docker Image...&#39;
    docker load -i ${TAR_NAME}

    echo &#39;🔄 Restarting Services...&#39;
    docker compose down
    docker compose up -d

    echo &#39;🧹 Cleaning up...&#39;
    rm ${TAR_NAME}
    docker image prune -f
  &quot;
&quot;&quot;&quot;
</code></pre>
<h2 id="배포-서버-필수-설정">배포 서버 필수 설정</h2>
<h3 id="1-docker-composeyml-위치">1. docker-compose.yml 위치</h3>
<pre><code class="language-text">/home/ubuntu/app/jenkins/client/docker-compose.yml</code></pre>
<h3 id="2-jenkins-credential-등록">2. Jenkins Credential 등록</h3>
<pre><code class="language-text">ID: web-server-credentails
Kind: SSH Username with private key
Username: ubuntu
Private Key: ~/.ssh/id_rsa (서버 SSH 키)</code></pre>
<blockquote>
<p>Jenkins UI에서 GitHub Credentails 생성하는 곳에서 만들어 줍니다. Private Key 같은 경우 Enter directly 체크하여 <code>.pem</code>을 text 편집기로 열어 나온 내용 전부를 넣어줍니다. </p>
</blockquote>
<h2 id="트러블슈팅">트러블슈팅</h2>
<table>
<thead>
<tr>
<th><strong>문제</strong></th>
<th><strong>해결법</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong><code>No space left on device</code></strong></td>
<td>Pre-cleaning 추가 (<strong><code>docker image prune -f</code></strong>)</td>
</tr>
<tr>
<td><strong><code>Permission denied</code></strong></td>
<td><strong><code>SSH_CRED_ID</code></strong> credential 재확인</td>
</tr>
<tr>
<td><strong><code>docker compose not found</code></strong></td>
<td>서버에 <strong><code>docker-compose-plugin</code></strong> 설치</td>
</tr>
<tr>
<td><strong><code>Image not found</code></strong></td>
<td><strong><code>docker load</code></strong> 성공 여부 로그 확인</td>
</tr>
</tbody></table>
<blockquote>
<p>실무에서는 Slack 알림 연동과 배포 승인 게이트를 추가하면 더욱 완벽합니다.
React SPA뿐 아니라 Vue, Next.js 등에도 동일하게 적용 가능합니다.</p>
</blockquote>
<h2 id="마무리">마무리</h2>
<p>이 파이프라인은 Jenkins → Docker Build → 원격 서버 배포까지 완전 자동화된 클라이언트 배포 플로우를 제공합니다.
React + Vite 프로젝트뿐 아니라, Node.js 기반 SPA에서도 동일한 방식으로 확장할 수 있습니다.</p>
<blockquote>
<p>필자의 경험상, 초기에는 한 서버 내에서 모든 CI/CD를 구성해 운영하는 것이 가장 안정적입니다.
이후 트래픽 증가나 서비스 분리 필요성이 생기면, 빌드 서버와 배포 서버를 분리하는 구조로 확장하는 것을 권장합니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Jenkins 파이프라인 : Pipeline Script와 SCM 방식 비교]]></title>
            <link>https://velog.io/@dev_osoli/Jenkins-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-Pipeline-Script%EC%99%80-SCM-%EB%B0%A9%EC%8B%9D-%EB%B9%84%EA%B5%90</link>
            <guid>https://velog.io/@dev_osoli/Jenkins-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-Pipeline-Script%EC%99%80-SCM-%EB%B0%A9%EC%8B%9D-%EB%B9%84%EA%B5%90</guid>
            <pubDate>Tue, 20 Jan 2026 12:30:45 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>젠킨스 파이프라인은 CI/CD 프로세스를 코드로 정의하는 기능입니다. <a href="https://velog.io/@dev_osoli/Git-%EC%97%B0%EB%8F%99-Webhook-%EB%B8%8C%EB%9E%9C%EC%B9%98PR-%ED%8A%B8%EB%A6%AC%EA%B1%B0-%EC%84%A4%EA%B3%84">이전글</a>에서 설명한 것 처럼 GitHub Webhook 등의 이벤트가 발생하면 젠킨스가 자동으로 해당 파이프라인 코드를 실행합니다. 
Jenkins에서 파이프라인을 생성할 때 보면 두 가지 옵션이 있습니다. 바로 Pipeline Script from SCM과 Pipeline Script입니다. 이번 글에서는 이 두 방식의 차이와 사용 방법을 살펴보겠습니다. </p>
</blockquote>
<h2 id="pipeline-script-방식">Pipeline Script 방식</h2>
<p><strong><code>Pipeline Script</code></strong> 방식은 Jenkins 서버 내부에서 직접 스크립트를 작성해 실행하는 방식입니다. Jenkins UI에서 <strong><code>Pipeline Script</code></strong>를 선택하면 스크립트를 작성할 수 있는 편집기가 나타납니다.</p>
<p><img src="https://velog.velcdn.com/images/dev_osoli/post/a2f27c9a-ed76-4ffc-8d96-5adf78433cc6/image.png" alt=""></p>
<p>이 방식은 설정이 간단해 빠르게 테스트할 수 있다는 장점이 있지만, 실제 운영 환경에서는 여러 단점 때문에 잘 사용되지 않습니다. 따라서 소규모 테스트나 프로토타입 환경에서 주로 활용됩니다.</p>
<h3 id="장점">장점</h3>
<ul>
<li>설정이 간단하고 빠르게 작성할 수 있습니다.</li>
<li>간단한 테스트용 파이프라인을 구성하기 좋습니다. </li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li>버전 관리가 불가능하여 스크립트를 수동으로 백업해야 합니다.</li>
<li>협업 중 UI에서 충돌이 발생할 수 있습니다.</li>
<li>스크립트가 Jenkins 서버 내부에 노출되어 보안 위험이 있습니다.</li>
<li>프로젝트 코드를 빌드하려면 스크립트에서 직접 <code>git pull</code> 등을 수행해야 합니다.</li>
</ul>
<h2 id="pipeline-script-from-scm">Pipeline Script from SCM</h2>
<p><code>Pipeline Script</code> 방식의 단점을 보완할 수 있는 것이 바로 <code>SCM</code>(Source Control Management) 방식입니다.</p>
<h3 id="scmsource-control-management-개념">SCM(Source Control Management) 개념</h3>
<p>SCM은 소스 코드 버전 관리 시스템을 의미합니다. Git과 같은 저장소에서 소스 코드를 가져와(<strong><code>checkout</code></strong>) 그 안에 정의된 <strong><code>Jenkinsfile</code></strong>을 실행합니다. 다음과 같은 기능을 제공합니다.</p>
<ul>
<li>변경 이력 추적 : 누가 언제 어떤 파일을 수정했는지 기록합니다. </li>
<li>동시 작업 관리 : 저장소의 파일을 가져오기 때문에 중복 파일 수정 시에도 충돌을 해결할 수 있습니다. </li>
<li>브랜치/분기 관리 : 기능 개발, 버그 수정, 릴리스별 브랜치를 두어 독립 작업이 가능합니다. </li>
<li>롤백 및 복구 : 문제가 생기면 이전 버전으로 되돌릴 수 있습니다. </li>
</ul>
<blockquote>
<p>저장소 기반의 관리이기 때문에 파이프라인 운영이나 협업 환경에서 높은 확장성을 가질 수 있습니다.</p>
</blockquote>
<h3 id="장점-1">장점</h3>
<ul>
<li>형상 관리 &amp; 이력 추적 :<br>Jenkinsfile이 Git에 있으므로 commit/PR 단위로 변경 이력을 추적할 수 있고, 문제가 생기면 이전 버전으로 롤백하기 쉽습니다. </li>
<li>브랜치/PR별 파이프라인 :<br><code>feature/*</code>, <code>develop</code>, <code>main</code> 등 브랜치마다 다른 Jenkinsfile을 두거나 조건 분기를 넣어 브랜치 전략에 맞는 빌드·배포 흐름을 구성할 수 있습니다. </li>
<li>협업과 코드 리뷰 :<br>파이프라인 변경도 Git 저장소의 <code>Rules</code>를 따르기에 PR 리뷰, 리뷰 승인 등의 분기 조건을 걸 수 있으므로 실수로 인한 배포 사고를 줄일 수 있습니다. </li>
<li>Jenkins 서버와의 결합도 감소 :<br>스크립트가 Jenkins UI나 디스크에 종속되지 않아, 서버를 교체하거나 재설치하더라도 저장소만 연결하면 기존 설정을 그대로 복구할 수 있습니다.</li>
</ul>
<h3 id="단점-1">단점</h3>
<ul>
<li>초기 설정 복잡도 :<br>위의 장점을 다 갖추기 위해서 Git 연동, Credentials, 브랜치 규칙 설정 등 초기 세팅이 다소 복잡합니다.</li>
<li>SCM 가용성 의존 :<br>Git 서버 장애나 네트워크 문제로 저장소를 체크아웃하지 못하면 파이프라인 전체가 실패합니다. </li>
</ul>
<blockquote>
<p>따라서 초기에 한 번 제대로 설정하는 것이 중요하며, 실패 시 전체 중단에 대비한 Fallback 전략이 필요합니다. </p>
</blockquote>
<h3 id="scm-세팅-방법">SCM 세팅 방법</h3>
<p>Jenkins UI에서 Pipeline에서 Definition을 <code>Pipeline Script from SCM</code>으로 설정하면 다음과 같은 설정창이 나옵니다. </p>
<p><img src="https://velog.velcdn.com/images/dev_osoli/post/c37e6695-ac86-4d1f-9d6d-d89a18fae0ed/image.png" alt=""></p>
<ul>
<li>Repository URL : Git 저장소 주소를 작성하시면 됩니다. </li>
<li>Credentials : Git 저장소를 인증하기 위한 토큰이라 생각하시면 됩니다.<br>(ADD 버튼을 통해 새 Credentials를 생성할 수 있습니다.)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_osoli/post/f7fb160b-2dcd-4d94-8653-205bb881567c/image.png" alt=""></p>
<p>GitHub Credentails을 만들기 위해서는 아래와 같이 작성합니다. </p>
<ul>
<li>kind : <strong><code>Username with password</code></strong>를 선택</li>
<li>Scope : &quot;Global&quot;은 모든 프로젝트, &quot;System&quot;은 Jenkins 시스템 전용으로 사용 가능</li>
<li>Username : GitHub 사용자명 </li>
<li>Password : GitHub Access Token 입력 </li>
<li>ID : Pipeline 스크립트 내부에서 Credentials가 필요한 경우 <code>credentialsId: &#39;github-creds&#39;</code> 처럼 자격 증명이 가능합니다. </li>
<li>Description : 해당 Credentails 에 대한 설명 작성 </li>
</ul>
<blockquote>
<p>위 과정을 마치면 Jenkins 모든 파이프라인이 작성되었습니다. 이제 Jenkinsfile로 Script만 작성하면 CI/CD를 구축할 수 있습니다. 다음에 React부터 Spring Boot, FastAPI 프로젝트를 젠킨스 자체 빌드하여 SSH로 전송 후 원격 실행하는 방식으로 순차적으로 알아보겠습니다. </p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Git 연동 & Webhook, 브랜치/PR 트리거 설계 ]]></title>
            <link>https://velog.io/@dev_osoli/Git-%EC%97%B0%EB%8F%99-Webhook-%EB%B8%8C%EB%9E%9C%EC%B9%98PR-%ED%8A%B8%EB%A6%AC%EA%B1%B0-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@dev_osoli/Git-%EC%97%B0%EB%8F%99-Webhook-%EB%B8%8C%EB%9E%9C%EC%B9%98PR-%ED%8A%B8%EB%A6%AC%EA%B1%B0-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Mon, 19 Jan 2026 12:56:15 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p> Git 저장소에서 발생하는 이벤트를 Jenkins로 전달하려면, 먼저 Git 저장소에 Webhook을 등록해 두어야 합니다. 
Webhook은 push, pull_request, PR closed 등의 이벤트가 발생했을 때 미리 지정한 URL로 HTTP 요청을 보내주는 기능입니다. Jenkins에서는 이 요청을 받아 조건에 맞는 이벤트일 시 제작해둔 파이프라인을 수행합니다. 이 글에서는 Git과 Jenkins를 Webhook으로 연결하고, &quot;특정 브랜치에 대한 PR이 merged되어 closed 되었을 때만&quot; Jenkins 파이프라인을 트리거하는 방법을 정리해 보겠습니다.</p>
</blockquote>
<h2 id="webhook과-trigger트리거">Webhook과 Trigger(트리거)</h2>
<p>Webhook은 Git의 이벤트인 <code>push</code>나 <code>pull_request</code> 등이 발생하면 지정된 URL로 HTTP POST 요청을 보내주는 기능입니다. 그리고  Jenkins에서는 Git이 보낸 내용을 보고 조건 검사를 거쳐 지정해둔 빌드를 실행하는 것이 Trigger(트리거)라고 합니다. </p>
<h3 id="webhook의-주요-jsonpath-컬럼">Webhook의 주요 JSONPath 컬럼</h3>
<ul>
<li><strong><code>$.action</code></strong> : Git 저장소의 상태 변화 값입니다. <ul>
<li><strong>opened</strong> : PR이 생성되었을 때 발생</li>
<li><strong>closed</strong> : PR이 닫혔을 때 발생. Merge 성공(true) 또는 수동(false) 두 상황 모두 Webhook이 발생합니다. </li>
<li><strong>synchronize</strong> : PR이 open된 상태에서 head에 새 커밋을 추가하거나 강제 push 시 발생하는 Webhook. 테스트를 재실행할 때 많이 사용합니다.   </li>
</ul>
</li>
<li><strong><code>$.pull_request.merged</code></strong> : PR이 실제로 성공했는지 확인하는 플래그 값 <ul>
<li>PR이 실제 merge(병합)된 경우만 true로, 빌드 트리거 핵심 조건입니다. </li>
<li>PR이 발생하고, closed <code>$.action</code>이 발생해야 이 값으로 배포 빌드 트리거 여부를 판단합니다. </li>
</ul>
</li>
<li><strong><code>$.pull_request.base.ref</code></strong> : PR의 대상 브랜치. 즉, merge될 곳을 나타내는 값입니다.</li>
<li><strong><code>$.ref</code></strong> : push 이벤트가 일어날 때 나오는 값이며, push 이벤트를 직접 트리거할 때 사용합니다. </li>
</ul>
<h3 id="주요-트리거-유형">주요 트리거 유형</h3>
<p>트리거에는 Webhook을 처리하는 폴링 방식과 이벤트 기반으로 나누어져 있습니다.</p>
<ul>
<li><p>폴링 방식(Github Hook Trigger for GITScm Polling)은 Jenkins에서 지원하는 GitHub 전용 플러그인으로 간단한 push 이벤트에 적합합니다. </p>
<ul>
<li>엔드포인트 : 자동 <code>/github-webhook/</code> (토큰 불필요)</li>
<li>push/PR을 자동 처리하고, Jenkins에서 SCM Branches에 push 했을 때 빌드하고 싶은 브랜치를 작성하면 됩니다. </li>
<li><code>merged=true</code> 같은 분기를 설정할 수 없으며, 사용자가 push를 조심해야 하는 단점이 있습니다. </li>
</ul>
</li>
<li><p>이벤트 기반(Generic Webhook Trigger)는 Jenkins에서 범용 플러그인을 사용하며 Webhook에서 전송하는 JSONPath를 좀 더 세밀하게 필터링하여 트리거 설정을 할 수 있습니다. </p>
<ul>
<li>엔드포인트: <code>/generic-webhook-trigger/invoke?token=YOUR_TOKEN</code></li>
<li><code>$.action=&quot;closed&quot;</code>(PR이 닫히고) 일 때, <code>$.pull_request.merged=true</code>(merge가 성공적이면 실행) 같은 Webhook 요청에 대해서만 빌드할 수 있도록 처리할 수 있습니다. </li>
</ul>
</li>
</ul>
<h2 id="webhook-설정-방법">Webhook 설정 방법</h2>
<blockquote>
<p>본글에서는 이벤트 기반(Generic Webhook Trigger)로 진행하겠습니다. </p>
</blockquote>
<h3 id="1-jenkins-trigger-플러그인-설치">1. Jenkins Trigger 플러그인 설치</h3>
<p>먼저 Setting &gt; Plugins &gt; Available plugins 클릭하여 <code>Generic Webhook Trigger Plugin</code>을 검색하여 설치해줍니다. 
<img src="https://velog.velcdn.com/images/dev_osoli/post/5e7b9d0b-a95d-4977-8afd-08297276e4f9/image.png" alt=""></p>
<blockquote>
<p>설치하면 사진과 같이 <code>Installed plugins</code>에서 조회할 수 있습니다. </p>
</blockquote>
<h3 id="2-jenkins-파이프라인-설정">2. Jenkins 파이프라인 설정</h3>
<ol>
<li>New Item을 클릭하여 파이프라인 이름을 설정해준 뒤 Pipeline을 클릭하고 OK를 눌러줍니다.
<img src="https://velog.velcdn.com/images/dev_osoli/post/53d70ec7-deed-427a-a0c9-44287708c93d/image.png" alt=""></li>
</ol>
<ol start="2">
<li><p>Trigger에서 <code>Generic Webhook Trigger</code>를 체크해줍니다. 
<img src="https://velog.velcdn.com/images/dev_osoli/post/a991aec7-e6cb-4fa0-b158-e49b42602705/image.png" alt=""></p>
<blockquote>
<p>만약, 해당 항목이 없다면 플러그인 설치가 제대로 안 된겁니다. </p>
</blockquote>
</li>
<li><p>체크했다면 다음과 같은 메뉴가 나올 것인데 Post content parameters에서 +추가 버튼을 클릭하여 다음과 같이 3개의 항목을 설정해줍니다. </p>
</li>
</ol>
<ul>
<li>Git 저장소 상태 변화값 설정 <ul>
<li>Variable : <code>ACTION</code></li>
<li>Expression : <code>$.action</code></li>
<li>JSONPath 체크</li>
</ul>
</li>
<li>Git 저장소 상태 변화값 설정 <ul>
<li>Variable : <code>MERGED</code></li>
<li>Expression : <code>$.pull_request.merged</code></li>
<li>JSONPath 체크</li>
</ul>
</li>
<li>Git 저장소 상태 변화값 설정 <ul>
<li>Variable : <code>BRANCH</code></li>
<li>Expression : <code>$.pull_request.base.ref</code></li>
<li>JSONPath 체크</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_osoli/post/b1828f67-cc9e-40f2-9005-6b258987c539/image.png" alt=""></p>
<ol start="4">
<li><p>Git Webhook 엔드포인트에 사용할 Token 값을 지정해 줍니다. </p>
</li>
<li><p>마지막으로 Optional filter에 표현식을 작성해줍니다. </p>
</li>
</ol>
<ul>
<li>Expression : <code>(?=.*closed)(?=.*true)(?=.*develop).*</code><ul>
<li>action==&quot;closed&quot; : PR closed 상태</li>
<li>merged==true : 실제 merge 성공</li>
<li><code>branch=.develop.*</code> : 브랜치명이 develop으로 시작하는 경우 (정규식)</li>
</ul>
</li>
<li>Text : <code>$ACTION $MERGED $BRANCH</code><ul>
<li>Expression의 RegEX 정규식과 매칭되는지 확인하기 위한 테스트 문자열 생성하는 역할</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_osoli/post/3447eb8e-0abc-40a0-a6ab-59ff3feba748/image.png" alt=""></p>
<ol start="6">
<li>아래 Pipeline의 스크립트를 작성하고, 저장해주면 Jenkins 설정 끝입니다. </li>
</ol>
<h3 id="3-github-webhook-설정">3. GitHub Webhook 설정</h3>
<p>배포 Repository &gt; Settings &gt; Webhooks &gt; Add webhook 클릭하여 다음과 같이 작성 </p>
<ul>
<li>Payload URL : <code>http://&lt;배포서버 주소&gt;:9090/generic-webhook-trigger/invoke?token=&lt;Jenkins에서 작성한 토큰&gt;</code></li>
<li>Content type : application/json </li>
<li>Let me select individual events. 체크 후 <code>Pull requests</code> 체크 및 모든 이벤트 체크 해제</li>
</ul>
<h3 id="4-테스트">4. 테스트</h3>
<p>테스트 방법은 간단합니다. 테스트 PR을 생성해 develop 브랜치로 merge 하세요. 그리고 Jenkins에서 빌드되는지 확인하세요. 만약 아무런 반응이 없다면 GitHub Webhook 설정했던 곳으로 들어가 Recent Deliveries를 확인해 보세요. </p>
<ul>
<li>Response 200응답이 아닌 경우 : Webhook과 Jenkins Generic Webhook Trigger 설정이 잘못된 경우입니다. 토큰값이 파이프라인 작성과 Webhook이 동일한지 확인하고, IP주소와 Jenkins 포트가 맞는지 재확인하세요. </li>
<li>Response 200응답인 경우 : Body에서 <code>action</code>, <code>pull_request.merged</code>, <code>pull_request.base.ref</code>이 작성해둔 값이 closed, true, develop인지 확인하세요.<blockquote>
<p>저는 빈 문자열 하나 잘못 설정해서 1시간 동안 찾았습니다...</p>
</blockquote>
</li>
</ul>
<p>이제 GitHub Webhook과 Jenkins Generic Webhook Trigger를 연동해 특정 브랜치(develop)로의 PR merged 시에만 정확히 파이프라인을 트리거하는 설정이 완료되었습니다. 위 내용들은 트리거 분기의 기초 작업이라 보시면 됩니다. 여기서 이제 Git의 태그를 활용하는 방식이라던가, 모니터링, 보안이 들어가면 CI/CD 파이프라인이 더욱 정교해집니다. 또한 스크립트에서도 깃 태그 방식을 이용한 Docker 이미지 태그 설정 버전 관리, Docker 서버 용량 관리 등을 신경써야 하기 때문에 한 번 설계 시 전체 워크플로우를 고려해야 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Jenkins로 CI/CD 설계하기: 빌드 3가지 패턴 정리 ]]></title>
            <link>https://velog.io/@dev_osoli/Jenkins%EB%A1%9C-CICD-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0-%EB%B9%8C%EB%93%9C-3%EA%B0%80%EC%A7%80-%ED%8C%A8%ED%84%B4-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@dev_osoli/Jenkins%EB%A1%9C-CICD-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0-%EB%B9%8C%EB%93%9C-3%EA%B0%80%EC%A7%80-%ED%8C%A8%ED%84%B4-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 13 Jan 2026 13:53:04 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Jenkins로 CI/CD를 설계할 때는 “빌드를 어디서 하고, 어떤 형태의 결과물을 어디서 어떻게 실행할 것인가”를 먼저 정리하는 것이 좋습니다. 본글에서 실무에서 자주 쓰이는 세 가지 패턴을 중심으로 정리하려고 합니다. 해당 패턴들은 배포 서버 구조, 보안(아웃바운드 규칙 등), 팀 규모에 따라 선택이 달라질 수 있습니다.</p>
</blockquote>
<h2 id="패턴-1-배포-서버에서-git-pull--docker-compose-스크립트">패턴 1: 배포 서버에서 git pull + docker compose 스크립트</h2>
<p>이 패턴은 가장 원초적인 방법으로, 가장 쉬운 방법입니다. Jenkins가 원격 서버에 접속해 <code>git pull</code> 과 <code>docker compose</code> 또는 <code>Dockerfile</code> 를 실행하게 하는 방식입니다. 즉, 코드 체크아웃과 컨테이너 빌드·실행을 모두 “배포 대상 서버”에서 처리합니다.</p>
<h3 id="jenkins-역할">Jenkins 역할</h3>
<ul>
<li>웹훅 또는 스케줄로 파이프라인 트리거</li>
<li>SSH로 배포 서버 접속 후 아래와 같은 명령어 스크립트 실행</li>
</ul>
<pre><code class="language-bash"># 해당 디렉토리 이동 
cd /home/ubuntu/app

# git pull 
git pull origin main 

# docker compose 내림. 볼륨은 선택 
docker compose -f docker-compose.yml down -v 

# docker compose 실행 
docker compose -f docker-compose.yml up --build -d 

# 기존 도커 이미지 삭제 
docker image prune -f</code></pre>
<h3 id="장점">장점</h3>
<ul>
<li>Jenkins 서버에 Docker를 설치하지 않아도 됨</li>
<li>배포 서버 기준으로 환경이 곧 실행 환경이라 가장 쉽다.</li>
</ul>
<h3 id="단점-및-고려사항">단점 및 고려사항</h3>
<ul>
<li>배포 서버에 Git, Docker, Compose 가 모두 설치되어 있어야 함.</li>
<li>그럼으로 CI/CD를 위한 배포 환경 관리가 분산되어 있어 관리가 어려움.</li>
<li>빌드 작업이 배포 서버 리소스를 점유하므로, 부담이 있을 수 있음.</li>
</ul>
<blockquote>
<p>이 패턴은 “단일/소규모 서버 + 간단한 서비스” 환경에서 시작하기 좋습니다. 초기 세팅이 간단하고, 서버 한 대로 사이드 프로젝트 또는 PoC용 환경에 적합합니다.</p>
</blockquote>
<hr>
<h2 id="패턴-2-docker-바이너리이미지-빌드-패턴">패턴 2: Docker 바이너리(이미지) 빌드 패턴</h2>
<p>해당 패턴은 Jenkins가 애플리케이션을 빌드하고 Docker 이미지를 생성한 뒤, 레지스트리(Docker Hub, ECR 등)에 저장(push) 후에 배포 서버에서는 이미지만 pull 해서 실행하는 방식입니다. </p>
<p>패턴 1방식에서 깃 저장소가 아닌 Docker 이미지를 저장하는 곳으로 변경했다고 이해하시면 됩니다. </p>
<h3 id="jenkins-역할-1">Jenkins 역할</h3>
<ul>
<li>소스 코드 체크아웃 후 애플리케이션 빌드(Gradle, Node 등)</li>
<li>Dockerfile 기반 이미지 빌드 및 레지스트리 push</li>
</ul>
<pre><code class="language-groovy">stage(&quot;Build &amp; Push Image&quot;){
    steps{
        sh &quot;&quot;
            docker build -t my-registry/app:${BUILD_NUMBER} .
            docker push my-registry/app:${BUILD_NUMBER} 
        &quot;&quot;
    }
}</code></pre>
<h3 id="배포-서버-역할">배포 서버 역할</h3>
<ul>
<li>Jenkins 에서 SSH로 접속하거나, 별도 스크립트로 이미지를 가져온(pull) 후 컨테이너를 재시작하는 방법입니다.</li>
</ul>
<pre><code class="language-bash"># docker 이미지 가져오기 
docker pull my-registry/app:${BUILD_NUMBER}

# 이미지 실행 
docker compose up -d </code></pre>
<h3 id="장점-1">장점</h3>
<ul>
<li><code>Docker 이미지</code> 라는 표준 아티팩트가 남아 롤백과 추적이 쉽다.</li>
<li>빌드 환경을 Jenkins 쪽에서 통제하므로, 배포 서버는 런타임만 신경쓰면 된다.</li>
<li><code>Kubernetes</code> 등 다른 오케스트레이션으로 확장하기 좋다.</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li>Jenkins에서 Docker를 사용할 수 있도록 Docker-in-Docker, docker socket 마운트 등 추가 구성이 필요</li>
<li>레지스트리 권한 관리, 태그 전략, 이미지 정리 정책 등을 함께 설계해야 한다.</li>
<li>이미지 정리 정책을 제대로 설정 안 할시 Jenkins 서버 용량 부족 문제가 발생한다.</li>
</ul>
<hr>
<h2 id="패턴-3-jenkins-자체-빌드-→-ssh-전송-및-원격-실행">패턴 3: Jenkins 자체 빌드 → SSH 전송 및 원격 실행</h2>
<p>이 패턴은 Jenkins가 바이너리(JAR, dist, tar, 도커 이미지 등)를 직접 빌드한 후, SSH를 통해 배포 서버로 해당 내용을 전송하고, 이를 원격에서 스크립트를 실행하는 방식입니다. </p>
<p>젠킨스 서버가 별도로 있고, 배포 서버에서 아웃바운드가 모두 막혀있는 경우 배포 서버에서는 빌드 자체가 불가능하기 때문에 사용한 적이 있습니다. </p>
<blockquote>
<p>여기서는 Spring Boot 를 JAR로 빌드하는 것이 아닌 Docker 이미지 빌드하며 <code>.tar</code> 파일을 저장하고 전송하는 것으로 예를 들겠습니다.</p>
</blockquote>
<h3 id="jenkins-역할-2">Jenkins 역할</h3>
<ol>
<li>코드 체크아웃 및 빌드(JAR, dist, rar, 도커 이미지 등을 생성)</li>
<li><code>Publish Over SSH</code> 또는 <code>ssh/scp</code> 로 빌드 결과물 전송 </li>
<li>전송 완료 후 원격 서버에서 스크립트 실행 </li>
<li>원격 서버에서 기존 컨테이너 종료 → 새 이미지 <code>load</code> → <code>docker compose</code> 로 서비스 재기동 </li>
</ol>
<h3 id="1단계-spring-boot-jar-빌드build-jar">1단계: Spring Boot Jar 빌드(<code>Build JAR</code>)</h3>
<p>첫 번째 스테이지에서는 로컬(젠킨스 서버)에서 Spring Boot JAR를 빌드합니다. </p>
<pre><code class="language-groovy">stage(&#39;Build JAR&#39;) {
  steps {
    script {
      dir(&quot;${env.LOCAL_SPRING_DIR}&quot;) {
        // Gradle Wrapper 실행 권한 부여
        sh &quot;chmod +x gradlew&quot;

        // 테스트는 제외하고 bootJar 빌드
        sh &quot;./gradlew clean bootJar -x test&quot;
      }
    }
  }
}</code></pre>
<h3 id="2단계-docker-이미지-빌드--tar-저장build--save-image">2단계: Docker 이미지 빌드 &amp; Tar 저장(<code>Build &amp; Save Image</code>)</h3>
<p>두 번쨰 스테이지에서는 방금 빌드된 JAR를 기반으로 Docker 이미지를 만들고, 이를 <code>.tar</code> 파일로 저장합니다. </p>
<pre><code class="language-groovy">stage(&#39;Build &amp; Save Image&#39;) {
  steps {
    script {
      // 2-1. Spring Boot 이미지 빌드 (Dual Tags: latest, git-tag)
      sh &quot;&quot;&quot;
        docker build --no-cache \
          -t $IMG_SPRING \
          -t ${imgTagged} \
          -f $LOCAL_SPRING_DIR/$DOCKERFILE $LOCAL_SPRING_DIR
      &quot;&quot;&quot;

      // 2-2. 도커 이미지를 tar 파일로 저장
      sh &quot;docker save -o $TAR_SPRING $IMG_SPRING ${imgTagged}&quot;
    }
  }
}</code></pre>
<blockquote>
<p><code>--no-cache</code> 옵션은 캐시를 사용하지 않고 항상 깨끗한 빌드를 수행하는 옵션입니다. 이미지 레이어에서 변동된 코드 반영이 안 될 위험을 줄이는 대신, 빌드 시간이 조금 더 길어집니다.</p>
</blockquote>
<h3 id="3단계-tar-파일-ssh-전송transfer-to-remote">3단계: Tar 파일 SSH 전송(<code>Transfer to Remote</code>)</h3>
<p>세 번째 스테이지에서는 SSH 키를 이용해 원격 서버에 접속하고, <code>docker save</code> 로 만든 <code>.tar</code> 파일을 전송합니다. </p>
<pre><code class="language-groovy">stage(&#39;Transfer to Remote&#39;) {
  steps {
    withCredentials([sshUserPrivateKey(credentialsId: env.SSH_CRED_ID, keyFileVariable: &#39;SSH_KEY&#39;)]) {
      script {
        // 1. 원격 디렉토리 생성
        sh &#39;&#39;&#39;
        ssh -i &quot;$SSH_KEY&quot; -o StrictHostKeyChecking=no &quot;$REMOTE_USER@$REMOTE_HOST&quot; &quot;mkdir -p $DEPLOY_ROOT&quot;
        &#39;&#39;&#39;

        // 2. 대용량 Tar 파일 전송
        sh &#39;&#39;&#39;
        scp -i &quot;$SSH_KEY&quot; -o StrictHostKeyChecking=no &quot;$TAR_SPRING&quot; &quot;$REMOTE_USER@$REMOTE_HOST:$DEPLOY_ROOT/&quot;
        &#39;&#39;&#39;

        // 3. 전송 후 로컬 캐시 삭제
        sh &#39;&#39;&#39;
        rm &quot;$TAR_SPRING&quot;
        &#39;&#39;&#39;
      }
    }
  }
}</code></pre>
<ul>
<li><strong><code>withCredentials([sshUserPrivateKey(...])</code></strong><ul>
<li>Jenkins 크리덴셜 스토어에 저장된 SSH 개인키를 빌드 시점에만 환경 변수(<strong><code>SSH_KEY</code></strong>)로 꺼내서 사용합니다.</li>
</ul>
</li>
<li><strong><code>mkdir -p $DEPLOY_ROOT</code></strong><ul>
<li>배포 루트 디렉터리가 없더라도 에러 없이 생성되도록 <strong><code>p</code></strong> 옵션을 사용합니다.</li>
</ul>
</li>
<li><strong><code>scp</code></strong> 전송 후 <strong><code>rm</code></strong><ul>
<li>Jenkins 노드의 디스크 사용량을 줄이기 위해, 전송이 끝난 <strong><code>.tar</code></strong> 파일을 바로 삭제합니다. 장기 운영 시 불필요한 아티팩트가 쌓이지 않도록 하는 실무적인 처리입니다.</li>
</ul>
</li>
</ul>
<h3 id="4단계-원격-서버에서-docker-compose-배포-remote-deploy">4단계: 원격 서버에서 Docker Compose 배포 (<strong><code>Remote Deploy</code></strong>)</h3>
<p>마지막 스테이지에서는 다시 SSH로 원격 서버에 접속하여, 기존 컨테이너를 내리고 새 이미지를 <strong><code>load</code></strong>한 뒤 <strong><code>docker compose</code></strong>로 재기동합니다.</p>
<pre><code class="language-groovy">stage(&#39;Remote Deploy&#39;) {
  steps {
    withCredentials([sshUserPrivateKey(credentialsId: env.SSH_CRED_ID, keyFileVariable: &#39;SSH_KEY&#39;)]) {
      script {
        sh &#39;&#39;&#39;
        ssh -i &quot;$SSH_KEY&quot; -o StrictHostKeyChecking=no &quot;$REMOTE_USER@$REMOTE_HOST&quot; &quot;
          cd $DEPLOY_ROOT

          # 1. 기존 컨테이너 종료
          docker compose -f $COMPOSE_FILE down

          # 2. 로그 디렉토리 생성 및 권한 설정
          mkdir -p logs/fastapi logs/spring
          chmod -R 755 logs 2&gt;/dev/null || echo &#39;Log directory exists&#39;

          # 3. 새로운 Spring Boot 이미지 로드
          docker load -i $TAR_SPRING

          # 4. 서비스 실행 (이미지 기반)
          docker compose -f $COMPOSE_FILE up -d --no-build

          # 5. tar 파일 삭제
          rm $TAR_SPRING

          # 6. 미사용 이미지 정리
          docker image prune -f
        &quot;
        &#39;&#39;&#39;
      }
    }
  }
}</code></pre>
<ul>
<li><strong><code>docker compose -f $COMPOSE_FILE down</code></strong><ul>
<li>기존 컨테이너를 깨끗하게 내려 준 뒤 새 이미지를 올리므로, 포트 충돌이나 오래된 컨테이너가 남는 문제를 방지합니다.</li>
</ul>
</li>
<li>로그 디렉터리 선 생성<ul>
<li><strong><code>logs/fastapi</code></strong>, <strong><code>logs/spring</code></strong>를 미리 만들고 권한을 맞춰줌으로써, 컨테이너 내부에서 호스트 볼륨을 마운트할 때 권한 문제를 최소화합니다.</li>
</ul>
</li>
<li><strong><code>docker load -i $TAR_SPRING</code></strong><ul>
<li>앞서 전송한 <strong><code>.tar</code></strong> 파일에서 이미지를 로드합니다. 이 시점 이후에 <strong><code>docker compose</code></strong>는 새 이미지를 사용할 수 있습니다.</li>
</ul>
</li>
<li><strong><code>docker compose up -d --no-build</code></strong><ul>
<li>원격 서버에 소스 코드는 없고 이미지만 존재하므로, <strong><code>-no-build</code></strong> 옵션으로 불필요한 빌드를 방지합니다.</li>
</ul>
</li>
<li>배포 이후 정리 작업<ul>
<li><strong><code>rm $TAR_SPRING</code></strong>, <strong><code>docker image prune -f</code></strong>로 디스크를 정리하여, 장기적으로 서버 용량 이슈를 예방합니다.</li>
</ul>
</li>
</ul>
<h3 id="장점-2">장점</h3>
<ul>
<li>Docker를 쓰지 않는 레거시 환경에 적합합니다.</li>
<li>보안 정책 등이 신경 쓰여 젠킨스 서버에서 모든 것을 할 때 유리합니다.</li>
<li>어떤 형식의 아티팩트든 빌드 후 그대로 전송하여, 환경(OS 등)에 관계없이 실행할 수 있습니다.</li>
</ul>
<h3 id="단점-1">단점</h3>
<ul>
<li>젠킨스 서버에 JDK, Gradle, NodeJS 등 플러그인 버전 관리가 필요.</li>
<li>아티팩트 버전 관리와 롤백 스크립트를 별도로 설계해야 합니다.</li>
<li>SSH 키/계정 관리, 네트워크 보안 규칙 등을 신경 써야 합니다.</li>
</ul>
<h2 id="세-패턴-비교-요약">세 패턴 비교 요약</h2>
<table>
<thead>
<tr>
<th><strong>항목</strong></th>
<th><strong>git pull + compose (패턴 1)</strong></th>
<th><strong>Docker 이미지(패턴 2)</strong></th>
<th><strong>빌드 후 SSH 전송(패턴 3)</strong></th>
</tr>
</thead>
<tbody><tr>
<td>빌드 위치</td>
<td>배포 서버</td>
<td>Jenkins (또는 빌드 전용 노드)</td>
<td>Jenkins</td>
</tr>
<tr>
<td>배포 아티팩트</td>
<td>Git 워킹 디렉터리 + 컨테이너</td>
<td>Docker 이미지</td>
<td>JAR/ZIP/바이너리 파일</td>
</tr>
<tr>
<td>Docker 필요 위치</td>
<td>배포 서버</td>
<td>Jenkins + 배포 서버</td>
<td>선택 (필수 아님)</td>
</tr>
<tr>
<td>롤백 난이도</td>
<td>브랜치/태그 기반 수동 롤백</td>
<td>태그된 이미지로 비교적 쉬운 롤백</td>
<td>아티팩트 보관/스크립트에 따라 상이</td>
</tr>
<tr>
<td>초기 도입 난이도</td>
<td>낮음</td>
<td>중간(레지스트리, Docker 구성 필요)</td>
<td>중간(SSH/키 관리 필요)</td>
</tr>
<tr>
<td>확장성(K8s 등)</td>
<td>낮음</td>
<td>높음</td>
<td>중간</td>
</tr>
<tr>
<td>적합한 상황</td>
<td>소규모 서버, 빠른 PoC</td>
<td>멀티 서버/쿠버네티스, 표준화된 배포 필요</td>
<td>레거시 서버, 단일 JAR/바이너리 배포 환경, 보안 정책</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[Jenkins란? + 설치(Docker 기준, 초기 설정)]]></title>
            <link>https://velog.io/@dev_osoli/Jenkins%EB%9E%80-%EC%84%A4%EC%B9%98Docker-%EA%B8%B0%EC%A4%80-%EC%B4%88%EA%B8%B0-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@dev_osoli/Jenkins%EB%9E%80-%EC%84%A4%EC%B9%98Docker-%EA%B8%B0%EC%A4%80-%EC%B4%88%EA%B8%B0-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Mon, 12 Jan 2026 12:50:32 GMT</pubDate>
            <description><![CDATA[<h2 id="jenkins란-무엇인가">Jenkins란 무엇인가</h2>
<blockquote>
<p>Jenkins는 빌드, 테스트, 배포까지 소프트웨어 개발 과정을 자동화해 CI/CD 파이프라인을 구성할 수 있게 해주는 오픈 소스 자동화 서버입니다. Java 기반으로 동작하며, Git과 같은 버전 관리 시스템, 빌드 도구, 배포 도구와 연동해 코드 변경 시점마다 자동으로 작업을 실행할 수 있습니다.</p>
</blockquote>
<p>주로 다음과 같은 목적에 사용합니다.</p>
<ul>
<li>코드 변경마다 자동 빌드·테스트를 수행하는 지속적 통합(Continuous Integration) 구현</li>
<li>승인/조건에 따라 점진적으로 배포를 진행하는 지속적 배포(Continuous Deployment/Delivery) 구성</li>
<li>Git, Docker, Slack 등 다양한 플러그인을 통해 팀에서 사용하는 DevOps 도구 체인과 손쉽게 연동</li>
</ul>
<blockquote>
<p>Jenkins는 JVM만 있으면 Linux, Windows, macOS, 컨테이너 환경 등 거의 모든 플랫폼에서 동작할 수 있습니다. 수백 개의 플러그인을 통해 소스 관리, 빌드, 테스트, 배포, 모니터링 등 대부분의 DevOps 도구와 연동이 가능합니다.</p>
</blockquote>
<h2 id="docker로-jenkins-설치-개요">Docker로 Jenkins 설치 개요</h2>
<blockquote>
<p>여기서는 Jenkins를 호스트 서버에 직접 설치하는 대신, docker-compose.yml과 Dockerfile을 이용해 컨테이너 형태로 구성합니다. 이렇게 하면 Jenkins 업그레이드나 설정 백업·복원이 단순해지고, 다른 서버로의 마이그레이션도 비교적 수월해집니다.
또한 Jenkins 컨테이너 안에서 Docker를 사용하도록 설정해, Jenkins Job이나 Pipeline에서 docker build, docker compose 명령을 실행해 애플리케이션 빌드와 컨테이너 기동까지 자동화할 수 있습니다.</p>
</blockquote>
<h3 id="docker-composeyml-작성">docker-compose.yml 작성</h3>
<p>먼저 서버에서 <code>/home/ubuntu/app/jenkins</code> 폴더를 만들고 해당 경로에 <code>docker-compose.yml</code> 파일을 생성합니다.</p>
<pre><code class="language-bash">cd /home/ubuntu/app

# jenkins 폴더 생성 
mkdir jenkins

cd /jenkins 

# docker-compose.yml 파일 생성 
touch docker-compose.yml 

# 편집기 사용 
vim docker-compose.yml </code></pre>
<pre><code class="language-yaml"> services:
  jenkins:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: jenkins
    user: root
    ports:
      - &quot;9090:8080&quot;
      - &quot;50000:50000&quot;
    volumes:
      - jenkins_home:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock
      - /home/ubuntu/app:/home/ubuntu/app
      - /home/ubuntu/.ssh:/root/.ssh
    restart: always

volumes:
  jenkins_home: </code></pre>
<p>각 항목은 다음 의미를 가집니다.</p>
<ul>
<li><strong><code>build.context</code></strong>, <strong><code>dockerfile</code></strong><ul>
<li>현재 디렉터리의 <strong><code>Dockerfile</code></strong>을 사용해 커스텀 Jenkins 이미지를 빌드합니다.</li>
</ul>
</li>
<li><strong><code>user: root</code></strong><ul>
<li>컨테이너 내부에서 root 계정으로 실행해 Docker 설치 및 호스트 리소스 접근을 단순화합니다.</li>
</ul>
</li>
<li><strong><code>ports</code></strong><ul>
<li><strong><code>9090:8080</code></strong>: 호스트 9090 포트를 컨테이너 8080 포트(Jenkins UI)와 매핑</li>
<li><strong><code>50000:50000</code></strong>: JNLP 에이전트 연결용 포트로, 별도 에이전트 노드를 구성할 때 사용</li>
</ul>
</li>
<li><strong><code>volumes</code></strong><ul>
<li><strong><code>jenkins_home:/var/jenkins_home</code></strong>: Jenkins 설정, Job, 플러그인 데이터를 영구 보관하는 볼륨</li>
<li><strong><code>/var/run/docker.sock:/var/run/docker.sock</code></strong>: Jenkins 컨테이너가 호스트의 Docker 데몬을 그대로 사용하도록 연결</li>
<li><strong><code>/home/ubuntu/app:/home/ubuntu/app</code></strong>: 실제 프로젝트 코드가 있는 디렉터리를 Jenkins에서 그대로 참조할 수 있도록 마운트</li>
<li><strong><code>/home/ubuntu/.ssh:/root/.ssh</code></strong>: 호스트의 SSH 키를 컨테이너 root 계정에 마운트해 GitHub, GitLab, 사내 Git 등 SSH 리포지토리에 접근 가능하게 설정</li>
</ul>
</li>
</ul>
<h3 id="dockerfile-작성">Dockerfile 작성</h3>
<p>같은 경로에 <code>docker-compose.yml</code>에서 사용할 <code>Dockerfile</code>을 생성합니다.</p>
<pre><code class="language-bash"># Dockerfile 생성 
touch Dockerfile

# 편집기 
vim Dockerfile</code></pre>
<pre><code class="language-Dockerfile">FROM jenkins/jenkins:lts-jdk21

USER root

# 1. 기본 필수 패키지 설치 (Docker Client + curl)
RUN apt-get update &amp;&amp; \
    apt-get install -y docker.io curl &amp;&amp; \
    rm -rf /var/lib/apt/lists/*

# 2. Docker Compose V2 수동 설치
# 공식 GitHub 릴리즈에서 바이너리를 다운로드하여 CLI 플러그인 경로에 설치합니다.
RUN mkdir -p /usr/lib/docker/cli-plugins &amp;&amp; \
    curl -SL https://github.com/docker/compose/releases/download/v2.24.5/docker-compose-linux-x86_64 \
      -o /usr/lib/docker/cli-plugins/docker-compose &amp;&amp; \
    chmod +x /usr/lib/docker/cli-plugins/docker-compose

# 3. Git Safe Directory 설정 (영구 적용)
RUN git config --system --add safe.directory &#39;*&#39;

USER root</code></pre>
<p>각 단계는 다음 역할을 합니다.</p>
<ul>
<li><strong><code>FROM jenkins/jenkins:lts-jdk21</code></strong><ul>
<li>Jenkins LTS 및 JDK 21이 포함된 공식 이미지를 기반으로 사용합니다.</li>
<li>JDK 21 외 다른 버전 이용 시 일부 플러그인 설치가 불가합니다. </li>
<li>프로젝트의 JDK 버전과 꼭 맞출 필요 없습니다. 해당 JDK는 플러그인으로 JDK를 설정할 수 있습니다. </li>
</ul>
</li>
<li><strong><code>apt-get install -y docker.io curl</code></strong><ul>
<li>컨테이너 내부에서 <strong><code>docker</code></strong> CLI를 사용할 수 있도록 Docker 클라이언트를 설치합니다.</li>
<li><strong><code>curl</code></strong>은 Docker Compose 바이너리를 다운로드할 때 사용합니다.</li>
</ul>
</li>
<li>Docker Compose v2 설치<ul>
<li><strong><code>/usr/lib/docker/cli-plugins/docker-compose</code></strong> 경로에 바이너리를 두면 <strong><code>docker compose</code></strong> 서브커맨드로 인식됩니다.</li>
<li>이후 Jenkins Job에서 <strong><code>docker compose up -d</code></strong>와 같은 명령을 그대로 사용할 수 있습니다.</li>
</ul>
</li>
<li><strong><code>git config --system --add safe.directory &#39;*&#39;</code></strong><ul>
<li>Jenkins가 Git 리포지토리를 다룰 때 “dubious ownership” 경고를 피하기 위해 모든 디렉터리를 Git safe directory로 처리합니다.</li>
<li>호스트 디렉터리를 마운트했는데 권한이 Jenkins 사용자와 다를 때 유용합니다.</li>
</ul>
</li>
</ul>
<p>실무에서는 <strong><code>docker.io</code></strong> 대신 Docker 공식 리포지토리(docker-ce)에서 CLI를 설치하는 예제도 자주 사용합니다. 보안상으로는 Dockerfile 마지막에 <strong><code>USER jenkins</code></strong>로 다시 내려가는 구성이 더 안전하지만, 여기서는 권한 문제를 피하기 위해 root 계정을 유지했습니다.</p>
<blockquote>
<p>본글에서는 권한 문제를 겪고 싶지 않기에 그냥 root로 하겠습니다. </p>
</blockquote>
<h3 id="jenkins-컨테이너-실행-및-초기-설정">Jenkins 컨테이너 실행 및 초기 설정</h3>
<p>이제 컨테이너를 빌드하고 실행한 뒤, <code>http://&lt;서버IP&gt;:9090</code> 으로 접속합니다. </p>
<pre><code class="language-bash"># build 
docker compose build 

# 컨테이너 띄우기 
docker compose up -d 

# 빌드와 실행을 한 번에
docker compose up --build -d </code></pre>
<p>컨테이너가 올라가면 브라우저에서 <code>http://&lt;서버IP&gt;:9090</code>으로 접속합니다. 처음 접속 시 Jenkins는 초기 관리자 비밀번호 입력 화면을 보여주는데, 이 값은 컨테이너 내부의 <code>/var/jenkins_home/secrets/initialAdminPassword</code> 파일에서 확인할 수 있습니다.</p>
<pre><code class="language-bash">docker exec -it jenkins cat /var/jenkins_home/secrets/initialAdminPassword</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_osoli/post/511bcb14-5a51-4209-b8d7-b1c6f520fb00/image.png" alt=""></p>
<p>위 명령으로 얻은 비밀번호를 초기 화면에 입력하면 다음 단계로 진행됩니다. 이후 플러그인 설치 화면이 나오면, 처음 사용하는 경우라면 <code>“Install suggested plugins”</code>를 선택해 Git, Pipeline 등 기본 플러그인을 자동 설치하는 것이 편리합니다. 특정 조직 표준이 있거나 최소 설치를 선호한다면 최소 플러그인만 설치 후 필요한 플러그인만 추가하는 방식도 가능합니다.</p>
<p><img src="https://velog.velcdn.com/images/dev_osoli/post/9cb01743-4457-44bf-b70f-946931eb5591/image.png" alt=""></p>
<p>플러그인 설치가 끝나면 관리자 계정 생성 화면이 표시되며, 일반적인 회원가입처럼 사용자명·비밀번호·이메일 등을 입력해 계정을 만들면 됩니다. 여기까지가 서버에 Docker 기반으로 Jenkins를 설치하고 초기 설정을 완료하는 전체 흐름입니다. 이후 글에서는 Jenkins를 활용하기 전에 알고 있으면 좋은 기본 개념과 주요 용어, Pipeline 구성 방법 등을 정리해 보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker + Spring Boot: 외부 JAR를 컴파일·빌드·런타임에 나눠 넣는 방법 ]]></title>
            <link>https://velog.io/@dev_osoli/Docker-Spring-Boot-%EC%99%B8%EB%B6%80-JAR%EB%A5%BC-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%B9%8C%EB%93%9C%EB%9F%B0%ED%83%80%EC%9E%84%EC%97%90-%EB%82%98%EB%88%A0-%EB%84%A3%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@dev_osoli/Docker-Spring-Boot-%EC%99%B8%EB%B6%80-JAR%EB%A5%BC-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%B9%8C%EB%93%9C%EB%9F%B0%ED%83%80%EC%9E%84%EC%97%90-%EB%82%98%EB%88%A0-%EB%84%A3%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Sun, 28 Dec 2025 11:27:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_osoli/post/0f838753-e475-4862-9e75-edebbd878d08/image.png" alt=""></p>
<blockquote>
<p>외부 JAR를 Spring Boot에서 사용한다는 것은, <strong>어느 시점의 classpath에 그 JAR를 올릴지 선택하는 문제</strong>입니다. 시점마다 목적과 설계 포인트가 달라지며, 특히 Docker 환경에서는 “언제, 어디에 복사해서 classpath에 태울지”를 명확히 하는 것이 중요합니다. 아래에서는 <strong><code>libs/external-logging-1.0.0.jar</code></strong>를 예시로 시점별로 정리해 보겠습니다.</p>
</blockquote>
<hr>
<h2 id="1-컴파일-시점에-넣는-경우">1. 컴파일 시점에 넣는 경우</h2>
<p>컴파일 시점에는 소스 코드가 외부 JAR 안의 클래스와 메서드를 <strong><code>import</code></strong> 할 수 있어야 합니다. Gradle에서는 일반 라이브러리처럼 <strong><code>implementation files(&quot;libs/external-logging-1.0.0.jar&quot;)</code></strong> 형태로 의존성을 추가합니다.</p>
<ul>
<li><p>특징</p>
<ul>
<li><p>컴파일 에러 없이 해당 JAR의 타입·메서드를 직접 사용합니다.</p>
</li>
<li><p>Spring Boot Gradle/Maven 플러그인을 사용할 경우, 보통 패키징 시점에 fat JAR(<strong><code>BOOT-INF/lib</code></strong>) 안에 함께 포함됩니다.</p>
</li>
<li><p>버전이 자주 바뀌지 않는 공통 라이브러리에 적합합니다.</p>
<blockquote>
<p>fat jar는 “내 코드랑 필요한 라이브러리를 전부 한 덩어리 JAR 하나에 다 넣어 둔 실행용 최종 JAR 형태”라고 보시면 됩니다.</p>
</blockquote>
</li>
</ul>
</li>
<li><p>예시 (build.gradle)</p>
<pre><code class="language-groovy">  dependencies {
      implementation files(&#39;libs/external-logging-1.0.0.jar&#39;)
  }</code></pre>
</li>
<li><p>Docker 예시</p>
<pre><code class="language-docker">  # 1단계: 빌드 (컴파일+패키징)
  FROM gradle:8-jdk AS builder
  WORKDIR /workspace
  COPY . .
  RUN gradle clean bootJar

  # 2단계: 런타임
  FROM eclipse-temurin:17-jre
  WORKDIR /app

  # 빌드 결과물만 복사 (외부 JAR는 fat JAR 안에 이미 포함)
  COPY --from=builder /workspace/build/libs/my-app-0.0.1-SNAPSHOT.jar app.jar

  ENTRYPOINT [&quot;java&quot;,&quot;-jar&quot;,&quot;/app/app.jar&quot;]</code></pre>
</li>
</ul>
<p>이 패턴에서는 외부 JAR가 개발 환경과 빌드 컨테이너의 파일 시스템에 존재하고, 최종 <strong><code>app.jar</code></strong> 내부 <strong><code>BOOT-INF/lib</code></strong>에도 포함된다는 점이 핵심입니다.</p>
<hr>
<h2 id="2-빌드패키징-시점에-넣는-경우">2. 빌드(패키징) 시점에 넣는 경우</h2>
<p>여기서 빌드 시점은 <strong><code>gradle build</code></strong> 또는 <strong><code>mvn package</code></strong>로 실행 가능한 하나의 JAR를 만드는 패키징 단계입니다. 이때 “어떤 JAR를 실행 JAR 내부에 넣을지” 혹은 “외부 폴더로 뺄지”를 결정합니다.</p>
<h2 id="2-1-실행-jar-내부에-포함하는-경우">2-1. 실행 JAR 내부에 포함하는 경우</h2>
<p>컴파일 의존성에 올려둔 외부 JAR는 Spring Boot 플러그인이 기본 설정이라면 자동으로 <strong><code>BOOT-INF/lib</code></strong>에 패키징합니다.</p>
<ul>
<li><p>장점</p>
<ul>
<li><strong><code>java -jar app.jar</code></strong> 한 줄로 실행이 가능합니다.</li>
<li>Docker 이미지에도 <strong><code>app.jar</code></strong>만 복사하면 됩니다.</li>
</ul>
</li>
<li><p>디렉터리 예시</p>
<pre><code>  /opt/projects/my-app/
    build.gradle
    src/...
    libs/
      external-logging-1.0.0.jar
  /opt/projects/my-app-docker/
    Dockerfile</code></pre></li>
<li><p>Dockerfile</p>
<pre><code class="language-docker">  FROM eclipse-temurin:17-jre
  WORKDIR /app

  COPY build/libs/my-app-0.0.1-SNAPSHOT.jar app.jar

  ENTRYPOINT [&quot;java&quot;,&quot;-jar&quot;,&quot;/app/app.jar&quot;]</code></pre>
</li>
</ul>
<h2 id="2-2-실행-jar-밖에-두고-유지하는-경우">2-2. 실행 JAR 밖에 두고 유지하는 경우</h2>
<p>실행 JAR에는 기본 의존성만 담고, 일부 JAR는 파일 시스템 상의 별도 디렉터리에 두고 <strong><code>loader.path</code></strong> 등으로 런타임 classpath에 추가하는 패턴입니다.</p>
<ul>
<li><p>특징</p>
<ul>
<li>컴파일 시점에는 외부 JAR를 의존성으로 포함해서 타입을 직접 <strong><code>import</code></strong>합니다.</li>
<li>패키징 단계에서는 해당 JAR를 <strong><code>BOOT-INF/lib</code></strong>에서 제외하고, 별도 디렉터리(<strong><code>/app/ext-libs</code></strong>)에 복사합니다.</li>
<li>최종 실행 시점에 이 디렉터리를 <strong><code>loader.path</code></strong>로 지정해 classpath에 올립니다.</li>
</ul>
</li>
<li><p>결과 구조 예시</p>
<pre><code>  /app/app.jar
  /app/ext-libs/external-logging-1.0.0.jar</code></pre></li>
<li><p>Dockerfile 예시</p>
<pre><code class="language-docker">  FROM eclipse-temurin:17-jre
  WORKDIR /app

  # 1) 앱 JAR 복사
  COPY build/libs/my-app-0.0.1-SNAPSHOT.jar app.jar

  # 2) 외부 JAR를 이미지 안의 별도 디렉터리에 복사
  COPY libs/external-logging-1.0.0.jar ext-libs/external-logging-1.0.0.jar

  # 3) 외부 JAR 디렉터리를 loader.path 로 추가
  ENTRYPOINT [&quot;java&quot;,
      &quot;-Dloader.path=ext-libs/&quot;,
      &quot;-Dloader.main=com.example.MyApplication&quot;,
      &quot;-cp&quot;,&quot;app.jar&quot;,
      &quot;org.springframework.boot.loader.PropertiesLauncher&quot;]</code></pre>
</li>
</ul>
<p>이 방식의 핵심은 “외부 JAR 교체를 앱 재빌드 없이 하고 싶다”는 요구를 만족시키는 것입니다.</p>
<hr>
<h2 id="3-런타임-시점에-넣는-경우">3. 런타임 시점에 넣는 경우</h2>
<p>런타임 시점은 이미 만들어진 Spring Boot 실행 JAR를 띄울 때, <strong>프로세스 실행 명령에서 외부 JAR를 classpath에 추가하는 단계</strong>를 의미합니다.</p>
<ul>
<li><p>대표 패턴</p>
<ul>
<li>Spring Boot <strong><code>PropertiesLauncher</code></strong> + <strong><code>loader.path</code></strong> 사용.</li>
<li>혹은 애플리케이션 코드에서 <strong><code>URLClassLoader</code></strong> 등을 이용해 특정 디렉터리의 JAR를 동적으로 로딩하는 플러그인 구조.</li>
</ul>
</li>
<li><p>특징</p>
<ul>
<li>외부 JAR를 교체한 뒤 애플리케이션을 재기동하거나, 잘 설계하면 플러그인 형태로 동적 로딩도 가능합니다.</li>
<li>컴파일 타임에 타입을 모르는 경우가 많기 때문에, 인터페이스/리플렉션 기반 설계가 필요합니다.</li>
<li>설계가 복잡해지면 classloader 문제와 유지보수 난이도가 크게 올라갑니다.</li>
</ul>
</li>
<li><p>디렉터리 예시</p>
<pre><code class="language-docker">  /app/app.jar
  /app/ext-libs/external-logging-1.0.0.jar</code></pre>
</li>
<li><p>실행 명령 예시 (<strong><code>PropertiesLauncher</code></strong> 사용)</p>
<pre><code class="language-bash">  java \
    -cp app.jar \
    -Dloader.path=ext-libs/ \
    -Dloader.main=com.example.MyApplication \
    org.springframework.boot.loader.PropertiesLauncher</code></pre>
</li>
</ul>
<p>또는 단순히 JVM classpath를 이용해 외부 JAR를 포함하는 형태도 가능합니다.</p>
<pre><code class="language-bash">java -classpath &quot;app.jar:ext-libs/*&quot; com.example.MyApplication</code></pre>
<hr>
<h2 id="4-시점별-비교-정리">4. 시점별 비교 정리</h2>
<table>
<thead>
<tr>
<th><strong>시점</strong></th>
<th><strong>classpath 기준 위치 예시</strong></th>
<th><strong>코드에서 <code>import</code> 가능한가?</strong></th>
<th><strong><code>app.jar</code> 안에 포함 여부</strong></th>
<th><strong>주 사용 목적</strong></th>
</tr>
</thead>
<tbody><tr>
<td>컴파일 시점</td>
<td>Gradle/Maven 의존성(<strong><code>implementation</code></strong>, <strong><code>api</code></strong>)</td>
<td>가능 (O)</td>
<td>보통 포함되도록 설정되는 경우가 많음</td>
<td>일반 라이브러리 의존, 강한 타입 의존</td>
</tr>
<tr>
<td>빌드 시점</td>
<td><strong><code>BOOT-INF/lib</code></strong>에 넣을지 vs 외부 디렉터리로 뺄지</td>
<td>새로 결정 X (컴파일 때 이미 결정)</td>
<td>플러그인 설정·Docker COPY로 포함/미포함 제어</td>
<td>fat JAR 구성, <strong><code>app.jar + ext-libs</code></strong> 구조</td>
</tr>
<tr>
<td>런타임 시점</td>
<td><strong><code>loader.path</code></strong>(<strong><code>/ext-libs</code></strong> 등), 커스텀 ClassLoader</td>
<td>직접 <strong><code>import</code></strong>는 보통 X</td>
<td>보통 실행 JAR 밖에 두고 필요 시 로딩</td>
<td>플러그인, 고객별 모듈, 교체 가능한 확장</td>
</tr>
</tbody></table>
<p><strong><code>import</code></strong> 가능 여부는 오직 “컴파일 시점에 그 JAR가 의존성으로 classpath에 올라가 있었는가”에 의해 결정됩니다.</p>
<hr>
<h2 id="5-어떤-패턴을-선택할지-기준">5. 어떤 패턴을 선택할지 기준</h2>
<p>Spring Boot 프로젝트에 외부 JAR를 “넣어야 하는” 상황에서는 다음 기준으로 선택하는 것이 좋습니다.</p>
<ul>
<li>코드가 그 JAR에 강하게 의존하고, 자주 바뀌지 않는 경우<ul>
<li>컴파일·빌드 시점에 일반 의존성으로 포함하고, fat JAR에 함께 패키징하는 것이 관리에 유리합니다.</li>
</ul>
</li>
<li>고객/환경마다 다른 구현을 꽂아야 하거나, 자주 교체되는 경우<ul>
<li>빌드 결과에는 최소한의 훅만 두고, 런타임에 <strong><code>loader.path</code></strong> 또는 플러그인 구조로 외부 JAR를 올리는 패턴이 더 적합합니다.</li>
</ul>
</li>
</ul>
<p>Docker에서는 “이미지 빌드 시점(복사 위치)”과 “컨테이너 실행 시점(java 옵션)”을 구분해 생각하면, 각 시점별 설계가 훨씬 명확해집니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[도커 환경에서의 build 산출물 구조 정리 (내부 빌드 vs 호스트 빌드)]]></title>
            <link>https://velog.io/@dev_osoli/%EB%8F%84%EC%BB%A4-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-build-%EC%82%B0%EC%B6%9C%EB%AC%BC-%EA%B5%AC%EC%A1%B0-%EC%A0%95%EB%A6%AC-%EB%82%B4%EB%B6%80-%EB%B9%8C%EB%93%9C-vs-%ED%98%B8%EC%8A%A4%ED%8A%B8-%EB%B9%8C%EB%93%9C</link>
            <guid>https://velog.io/@dev_osoli/%EB%8F%84%EC%BB%A4-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-build-%EC%82%B0%EC%B6%9C%EB%AC%BC-%EA%B5%AC%EC%A1%B0-%EC%A0%95%EB%A6%AC-%EB%82%B4%EB%B6%80-%EB%B9%8C%EB%93%9C-vs-%ED%98%B8%EC%8A%A4%ED%8A%B8-%EB%B9%8C%EB%93%9C</guid>
            <pubDate>Sun, 28 Dec 2025 10:14:57 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_osoli/post/a16e5719-82ed-442a-913f-7ba4244fb412/image.png" alt=""></p>
<h1 id="도커-빌드-산출물-구조-내부-빌드-vs-호스트-빌드">도커 빌드 산출물 구조: 내부 빌드 vs 호스트 빌드</h1>
<h2 id="개요">개요</h2>
<p>도커 기반 개발에서는 애플리케이션의 컴파일, 테스트, 패키징 단계를 어디서 수행하느냐에 따라 빌드 산출물의 위치와 관리 방식이 크게 달라집니다. 이 글에서는 컨테이너 내부 빌드와 호스트 빌드 방식의 구조, 장단점, 선택 기준을 정리합니다.</p>
<hr>
<h2 id="용어-정리">용어 정리</h2>
<ul>
<li><strong>내부 빌드 (in-container build)</strong>: 컴파일/테스트/패키징을 Dockerfile에서 모두 실행하며, 결과물을 이미지 안에만 보관하는 방식입니다.</li>
<li><strong>호스트 빌드 (host build)</strong>: 호스트에서 바이너리, JAR 등을 만든 뒤 Dockerfile에서는 산출물만 복사(COPY)해 이미지를 완성합니다.</li>
<li><strong>멀티 스테이지 빌드</strong>: 한 Dockerfile 내에서 빌드용·런타임 이미지를 분리해, 산출물만 복사하는 패턴입니다.</li>
</ul>
<hr>
<h2 id="기존-방식의-특징">기존 방식의 특징</h2>
<h2 id="컴파일-테스트-패키징-단계">컴파일, 테스트, 패키징 단계</h2>
<ul>
<li><strong>컴파일:</strong> 소스코드를 바이너리 등 실행 가능한 형태로 변환</li>
<li><strong>테스트:</strong> 빌드한 결과물의 안정성 검증</li>
<li><strong>패키징:</strong> 실제 배포나 배포 이미지로 묶는 단계</li>
</ul>
<p>위 작업들이 Dockerfile에 들어가면 <code>내부 빌드</code>, 외부에서 먼저 수행되면 <code>호스트 빌드</code>가 됩니다.</p>
<hr>
<h2 id="내부-빌드-구조와-동작-방식">내부 빌드 구조와 동작 방식</h2>
<pre><code class="language-docker">FROM gradle:8-jdk AS builder
WORKDIR /app
COPY . .
RUN gradle clean build -x test

FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /app/build/libs/app.jar app.jar
CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]</code></pre>
<ul>
<li>모든 컴파일/테스트/패키징 과정을 Dockerfile 안에서 수행</li>
<li>빌드 산출물(JAR 등)은 컨테이너 레이어 내부에만 존재</li>
</ul>
<h2 id="장점">장점</h2>
<ul>
<li><strong>빌드 환경 일관성 확보:</strong> 어느 환경(CI/개발PC/운영서버)이든 동일하게 재현 가능</li>
<li><strong>멀티 스테이지 빌드:</strong> 최종 이미지에 빌드 도구 미포함, 크기 최적화 가능</li>
</ul>
<h2 id="단점-및-주의점">단점 및 주의점</h2>
<ul>
<li><strong>산출물 활용의 불편함:</strong> 외부 배포용 ZIP, 문서 등 이미지 밖으로 꺼낼 때 추가 작업 필요(docker cp 등)</li>
<li><strong>중간 파일, 캐시 관리 필요:</strong> 자주 변경되는 환경에서는 캐시 및 레이어 효율 설계에 신경 써야 함</li>
</ul>
<hr>
<h2 id="호스트-빌드-구조와-동작-방식">호스트 빌드 구조와 동작 방식</h2>
<ol>
<li>호스트에서 직접 <code>./gradlew build</code>, <code>npm run build</code> 등으로 산출물 생성</li>
<li>Dockerfile에서는 해당 산출물만 복사</li>
</ol>
<pre><code class="language-docker">FROM eclipse-temurin:17-jre
WORKDIR /app
COPY build/libs/app.jar app.jar
CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]</code></pre>
<ul>
<li>빌드 산출물의 1차 위치: 호스트 파일 시스템</li>
</ul>
<h2 id="장점-1">장점</h2>
<ul>
<li><strong>빌드 산출물 재사용 용이:</strong> Docker 이미지 외 ZIP/패키지 등 다양한 배포 채널로 확장 가능</li>
<li><strong>단순한 Dockerfile:</strong> 이미지 빌드는 패키징된 파일 복사 단계만 포함</li>
</ul>
<h2 id="단점-및-주의점-1">단점 및 주의점</h2>
<ul>
<li><strong>환경 일관성 관리 부담:</strong> 빌드 환경(도구 및 버전)이 개발자·CI마다 달라질 위험</li>
<li><strong>이미지 최적화 제한:</strong> 캐시 및 중간 산출물 관리는 Docker가 아닌 빌드 도구에 의존</li>
</ul>
<hr>
<h2 id="구조-비교-표-정리">구조 비교: 표 정리</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>내부 빌드 (in-container)</th>
<th>호스트 빌드 (host build)</th>
</tr>
</thead>
<tbody><tr>
<td>1차 산출물 위치</td>
<td>컨테이너/이미지 내부</td>
<td>호스트 파일 시스템</td>
</tr>
<tr>
<td>Dockerfile 역할</td>
<td>컴파일·테스트·패키징 전부 수행</td>
<td>산출물 복사 및 런타임 이미지화</td>
</tr>
<tr>
<td>빌드 환경 일관성</td>
<td>Dockerfile로 100% 일치</td>
<td>환경(도구/버전) 외부 관리 필요</td>
</tr>
<tr>
<td>산출물 재사용성</td>
<td>외부 사용 시 추출 절차 필요</td>
<td>즉시 다양한 배포 채널 활용 가능</td>
</tr>
<tr>
<td>이미지 크기</td>
<td>멀티스테이지 활용 시 최적화 가능</td>
<td>단순 복사 구조이므로 기본적으로 작음</td>
</tr>
<tr>
<td>개발자 편의성</td>
<td>빌드 도구 별도 설치 불필요</td>
<td>개발 PC에 빌드 도구 설치 필요</td>
</tr>
</tbody></table>
<hr>
<h2 id="각-방식의-적합-사례">각 방식의 적합 사례</h2>
<ul>
<li><strong>내부 빌드 선호</strong><ul>
<li>다수의 서비스, 빌드 환경 표준화가 중요</li>
<li>&quot;빌드는 무조건 컨테이너 내에서&quot; 정책 또는 외부에 빌드 환경이 없는 경우</li>
</ul>
</li>
<li><strong>호스트 빌드 선호</strong><ul>
<li>통합 빌드/배포 체계가 잘 구축된 환경</li>
<li>하나의 산출물을 여러 채널로 동시 배포하는 경우</li>
</ul>
</li>
</ul>
<hr>
<h2 id="실무-문서화-및-관리-팁">실무 문서화 및 관리 팁</h2>
<ul>
<li><strong>빌드 책임 위치</strong> 명확히: 호스트 vs Docker builder</li>
<li><strong>산출물 디렉터리 구조</strong>: (예) 호스트: <code>build/libs/</code>, 컨테이너: <code>/app/build</code></li>
<li><strong>산출물 원본 위치</strong>와 추출 정책: 이미지 내부, 혹은 외부 저장소 구분</li>
<li><strong>공유/마운트 정책</strong>: 개발 환경에서 로그 등 공유 필요 시 볼륨 마운트 등 추가 정의</li>
</ul>
<h2 id="결론-및-개인-의견">결론 및 개인 의견</h2>
<ul>
<li><strong>내부 빌드</strong>는 환경 일관성과 CI 자동화에 강점이 있습니다. 반면 외부 산출물 활용·배포가 필요하다면 <strong>호스트 빌드</strong>가 유리합니다.</li>
<li>필자는 마이크로서비스, 여러 개발자가 참여하는 환경에서는 내부 빌드(Multi-stage) 방식을 추천합니다. 단, 배포나 산출물 활용 경로에 따라 팀 컨벤션을 명확히 정리해 두는 것이 중요하다고 생각합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[도커 마운트 이해하기: 볼륨과 바인드 마운트의 개념과 차이]]></title>
            <link>https://velog.io/@dev_osoli/%EB%8F%84%EC%BB%A4-%EB%A7%88%EC%9A%B4%ED%8A%B8-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-%EB%B3%BC%EB%A5%A8%EA%B3%BC-%EB%B0%94%EC%9D%B8%EB%93%9C-%EB%A7%88%EC%9A%B4%ED%8A%B8%EC%9D%98-%EA%B0%9C%EB%85%90%EA%B3%BC-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@dev_osoli/%EB%8F%84%EC%BB%A4-%EB%A7%88%EC%9A%B4%ED%8A%B8-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-%EB%B3%BC%EB%A5%A8%EA%B3%BC-%EB%B0%94%EC%9D%B8%EB%93%9C-%EB%A7%88%EC%9A%B4%ED%8A%B8%EC%9D%98-%EA%B0%9C%EB%85%90%EA%B3%BC-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Wed, 26 Nov 2025 12:12:27 GMT</pubDate>
            <description><![CDATA[<h2 id="도커-마운트란">도커 마운트란?</h2>
<blockquote>
<p>컨테이너 환경에서 데이터를 안전하게 보존하거나 여러 컨테이너가 데이터를 공유하고자 할 때, 도커 마운트(mount)는 필수적인 메커니즘입니다. 컨테이너는 생성과 삭제가 자유롭기 때문에, 로그, DB 데이터, 업로드 파일처럼 시간이 지나도 유지되어야 하는 데이터는 마운트를 활용해 외부 저장소(호스트의 파일 시스템 또는 도커 볼륨)에 보관합니다.</p>
</blockquote>
<hr>
<h2 id="내부-디렉터리-vs-외부-디렉터리">내부 디렉터리 vs 외부 디렉터리</h2>
<p><img src="https://velog.velcdn.com/images/dev_osoli/post/098e34be-99fb-4c07-bc6a-64c64eaeecbe/image.png" alt=""></p>
<blockquote>
<p>도커 마운트를 이해할 때 가장 먼저 잡아두면 좋은 개념은 “<strong>컨테이너 내부 경로</strong>”와 “<strong>외부 저장 위치(호스트/볼륨)</strong>”입니다.</p>
</blockquote>
<ul>
<li><p>컨테이너 내부 디렉터리  </p>
<ul>
<li>컨테이너 안에서 애플리케이션이 사용하는 경로입니다. 예: <code>/app/logs</code>, <code>/var/lib/mysql</code> 등.</li>
<li>마운트 시 오른쪽(또는 콜론 뒤)에 오는 경로로 표현합니다. 
예: <code>-v something:/app/logs</code> 에서 <code>/app/logs</code>.</li>
</ul>
</li>
<li><p>컨테이너 외부 디렉터리(또는 볼륨)  </p>
<ul>
<li>실제 데이터가 저장되는 위치입니다.</li>
<li>호스트 OS의 경로(예: <code>/home/user/project</code>)일 수도 있고, 도커가 관리하는 볼륨 디렉터리(예: <code>/var/lib/docker/volumes/...</code>)일 수도 있습니다.</li>
</ul>
</li>
</ul>
<p>마운트가 설정되면 “컨테이너 내부 디렉터리”는 더 이상 컨테이너의 레이어가 아니라, 외부 저장소를 바라보게 됩니다. 마치 창문과 같이 겉보기엔 컨테이너 안에 있는 폴더처럼 보이지만, 실제로는 그 폴더 너머에 외부(호스트나 볼륨)에 있는 공간이 연결되어 있습니다. 
즉, 컨테이너를 지워도 그 디렉터리에 쌓인 데이터(ex. log file)는 외부에 남아 재사용할 수 있습니다.</p>
<hr>
<h2 id="볼륨volume-기본-개념">볼륨(volume) 기본 개념</h2>
<p>볼륨은 “<strong>도커 엔진이 관리하는 외부 디렉터리</strong>”입니다.
도커 엔진이 /var/lib/docker/volumes 하위에 디렉터리를 생성해서 이름으로 관리하고, CLI로 손쉽게 생성, 삭제, 조회할 수 있습니다.</p>
<ul>
<li><p>사용 예시</p>
<ul>
<li>볼륨 생성: <code>docker volume create my-data</code></li>
<li>컨테이너에 마운트: <code>docker run -v my-data:/var/lib/mysql mysql</code></li>
</ul>
</li>
<li><p>특징  </p>
<ul>
<li>수명 주기가 컨테이너와 독립적입니다. 컨테이너를 모두 삭제해도 <code>docker volume rm</code> 하지 않으면 데이터는 남습니다.</li>
<li>Docker CLI로 쉽게 조회/생성/삭제할 수 있고(<code>docker volume ls</code>, <code>inspect</code> 등), 백업이나 이동도 비교적 일관된 방식으로 처리할 수 있습니다.</li>
</ul>
</li>
</ul>
<p>실무에서는 데이터베이스 데이터 디렉터리, 애플리케이션의 영속 스토리지 등 “환경마다 경로가 달라지지 않았으면 하는 데이터”에 주로 볼륨을 사용합니다.</p>
<hr>
<h2 id="바인드-마운트bind-mount-기본-개념">바인드 마운트(bind mount) 기본 개념</h2>
<p>바인드 마운트는 “호스트 파일 시스템 경로를 그대로 컨테이너에 연결”하는 방식입니다.
볼륨이 “도커가 관리하는 이름 기반 저장소”라면, 바인드 마운트는 “OS 입장에서 보이는 기존 폴더/파일을 그대로 공유”하는 개념입니다. 즉, 연결된 호스트에서 변경된 파일을 실시간으로 바로 변경되도록 할 수 있습니다. </p>
<ul>
<li><p>사용 예시  </p>
<ul>
<li><code>docker run -v /home/user/project:/app node</code> 처럼, 콜론 앞에 호스트의 절대 경로를 적습니다.</li>
<li>특정 파일도 마운트할 수 있습니다. 예: <code>/home/user/config.yml:/app/config.yml</code>.</li>
</ul>
</li>
<li><p>특징  </p>
<ul>
<li>호스트에서 파일을 수정하면 컨테이너 안에서도 바로 반영되므로, 로컬 개발 환경에서 소스 코드를 공유할 때 많이 사용합니다.</li>
<li>반대로 운영 서버에서는 호스트 디렉터리 구조에 강하게 의존하게 되어, 이식성과 관리 측면에서 리스크가 될 수 있습니다.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="볼륨-vs-바인드-마운트-한눈에-비교">볼륨 vs 바인드 마운트 한눈에 비교</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>볼륨 (Volume)</th>
<th>바인드 마운트 (Bind mount)</th>
</tr>
</thead>
<tbody><tr>
<td>저장 위치</td>
<td>도커가 관리하는 전용 디렉터리 (예: <code>/var/lib/docker/volumes</code>)</td>
<td>호스트의 임의 경로 (예: <code>/home/user/project</code>)</td>
</tr>
<tr>
<td>관리 주체</td>
<td>Docker 엔진이 관리 (CLI, API로 제어)</td>
<td>호스트 OS가 관리, 일반 파일/폴더와 동일</td>
</tr>
<tr>
<td>마운트 지정 방식</td>
<td><code>볼륨이름:/컨테이너경로</code></td>
<td><code>호스트절대경로:/컨테이너경로</code></td>
</tr>
<tr>
<td>컨테이너와의 수명</td>
<td>컨테이너와 독립적, 삭제해도 볼륨은 남음</td>
<td>호스트 폴더가 살아 있는 한 계속 존재</td>
</tr>
<tr>
<td>대표 사용처</td>
<td>DB 데이터, 영속 스토리지, 프로덕션 환경</td>
<td>로컬 개발 시 소스 코드·설정 파일 공유</td>
</tr>
<tr>
<td>장점</td>
<td>이식성·관리성 좋고, Docker 에코시스템과 잘 통합</td>
<td>실시간 코드 반영, 호스트 도구(IDE 등)와 직접 연동</td>
</tr>
<tr>
<td>단점</td>
<td>호스트에서 직접 접근하려면 경로를 따로 찾아야 함</td>
<td>호스트 디렉터리 구조에 강하게 의존, 권한/보안 이슈에 취약</td>
</tr>
</tbody></table>
<hr>
<h2 id="정리-언제-무엇을-쓸까">정리: 언제 무엇을 쓸까</h2>
<ul>
<li><p>볼륨을 쓰기 좋은 상황  </p>
<ul>
<li>운영 환경, 데이터베이스·로그·업로드 파일처럼 “서비스 수명과 함께 가야 하는 데이터”를 다룰 때.</li>
<li>여러 컨테이너가 동일한 데이터를 안정적으로 공유해야 할 때.</li>
</ul>
</li>
<li><p>바인드 마운트를 쓰기 좋은 상황  </p>
<ul>
<li>로컬 개발에서 IDE가 있는 호스트 디렉터리를 그대로 컨테이너에 물려, 코드 변경을 즉시 반영하고 싶을 때.</li>
<li>호스트의 특정 설정 파일이나 디렉터리를 그대로 활용해야 할 때.</li>
</ul>
</li>
</ul>
<blockquote>
<p>실무에서 흔히 “프로덕션 환경에는 볼륨, 로컬 개발에는 바인드 마운트”를 기본 원칙으로 삼으라고 많이 권장하지만, 개인적으로는 아직 바인드 마운트를 실제로 사용할 기회가 많지는 않았습니다.
로컬에서 먼저 개발한 뒤, 테스트 서버에 직접 배포해보며 마운트 방식을 하나씩 적용해 보면 각각의 장점과 상황별 차이를 더 명확하게 체감할 수 있을 것이라 생각합니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[GraphQL란?]]></title>
            <link>https://velog.io/@dev_osoli/GraphQL%EB%9E%80</link>
            <guid>https://velog.io/@dev_osoli/GraphQL%EB%9E%80</guid>
            <pubDate>Tue, 25 Nov 2025 13:41:34 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_osoli/post/6776b5e8-7c1d-4438-b3c5-dd1e51706f1a/image.png" alt=""></p>
<h2 id="graphql-개요">GraphQL 개요</h2>
<blockquote>
<p>현대에 있어서 모바일 애플리케이션 환경이 점점 더 복잡해졌습니다. 때문에 기존의 REST 방식은 필요한 데이터만 가져오기 위해 엔드포인트를 제작하거나, 여러 번 반복했어야 했고, 또한 불필요한 데이터가 오는 오버페칭(Overfetching)도 불가피했습니다.데이터 요금이 제한적이거나 비싼 모바일 사용자에게 지연과 비효율 문제가 발생했습니다. 이에 페이스북 개발팀은 모바일 앱을 확장하는 과정에서 기존 <code>REST API</code>와 <code>FQL</code>(Facebook Query Language)의 한계를 문제를 해결하기 위해 GraphQL을 개발했습니다. </p>
</blockquote>
<p>GraphQL은 클라이언트가 애플리케이션 프로그래밍 인터페이스(API)와 상호 작용하는 방식을 지정하는 오픈 소스 쿼리 및 조작 언어입니다.  </p>
<p>GraphQL의 특징은 다음과 같습니다. </p>
<ul>
<li>GraphQL은 타입 시스템을 사용하여 쿼리를 실행하는 서버사이드 런타임이며, 이 타입 시스템은 클라이언트가 요청할 수 있는 데이터 구조를 엄격하게 정의합니다.</li>
<li>특정 데이터베이스나 플랫폼에 종속적이지 않습니다. </li>
<li>GraphQL은 요청한 필드만 반환해 오버페칭(over-fetching)을 줄이고 효율적입니다. </li>
<li>GraphQL은 단일 요청으로 여러 데이터 항목을 한 번에 가져올 수 있습니다. </li>
</ul>
<p>GraphQL의 이러한 특성 덕분에 클라이언트가 필요한 데이터의 구조를 정의하고, 반환하여 웹 및 모바일 애플리케이션 환경에서 REST API를 대체하는 강력한 도구로 자리잡게 되었습니다. </p>
<p><a href="http://sotagtrends.com/?tags=graphql+rest&amp;relative=true">🔗</a>에 따르면 아직 REST가 주류를 이루고 있음을 보여줍니다. 하지만 <a href="https://www.gartner.com/en/documents/4009103">Gratner</a>의 보고서에 따르면 2025년까지 기업의 50% 이상이 GraphQL을 프로덕션에 사용할 것으로 예측했습니다. </p>
<p><img src="https://velog.velcdn.com/images/dev_osoli/post/74282b42-a69c-4522-9c15-ee1ed480d7f7/image.png" alt=""></p>
<p>이 글에서는 GraphQL과 REST를 살펴보며 GraphQL에 대해 알아보겠습니다. </p>
<h3 id="rest-api와의-차이점">REST API와의 차이점</h3>
<p>API를 위한 쿼리 언어인 GraphQL은 같은 쿼리 언어인 SQL과 비교해보면 서로 목적 자체가 다릅니다. </p>
<ul>
<li>GraphQL은 웹 클라이언트가 데이터를 서버로부터 효율적으로 가져오는 것이 목적인 쿼리 언어</li>
<li>SQL은 데이터베이스 시스템에 저장된 데이터를 효율적으로 가져오는 것이 목적인 쿼리 언어</li>
</ul>
<p>즉, REST에서는 반환되는 응답(response) 데이터가 정해져 있어 컬럼의 수정과 DTO를 추가 생성하거나 수정해야한다.<br>하지만, GraphQL은 쿼리로 요청하기 때문에 원하는 데이터를 뽑아와 커스터마이징할 수 있습니다. </p>
<p>아래 REST와의 차이점을 보며 자세히 알아보겠습니다. </p>
<ol>
<li>URL 엔드포인트</li>
</ol>
<ul>
<li>REST API는 URL + METHOD를 조합으로 다양한 엔드포인트를 사용합니다.</li>
<li>GraphQL는 단일 엔드포인트(주로 POST)를 사용하며, 쿼리 조합을 통해 데이터를 요청합니다.   </li>
</ul>
<ol start="2">
<li>Response 데이터 </li>
</ol>
<ul>
<li>REST API는 미리 정의된 response 구조를 가집니다. API response에 새로운 필드가 추가되면 관련 파일을 수정해야 하며, API마다 다른 response 구조에 따라 각각의 DTO를 생성해야 합니다. </li>
<li>GraphQL은 클라이언트가 쿼리를 통해 원하는 데이터만 요청할 수 있어, 필요한 데이터만 가져와 사용할 수 있습니다. </li>
</ul>
<blockquote>
<p>응답 형식은 동일하지만, REST와 GraphQL은 모두 JSON 형식의 응답을 반환하지만, GraphQL은 데이터 간소화 및 통합에 중점을 둡니다. </p>
</blockquote>
<ol start="3">
<li>캐싱 <ul>
<li>REST API는 <code>GET</code> 요청의 경우 자체적으로 캐싱이 가능합니다. </li>
<li>GraphQL은 캐싱을 위해 클라이언트 또는 서버 개발자가 별로의 캐싱 처리를 구현해야 합니다. </li>
</ul>
</li>
</ol>
<h2 id="graphql의-핵심-개념">GraphQL의 핵심 개념</h2>
<p>GraphQL은 특정 애플리케이션 아키텍쳐나 데이터베이스에 종속되지 않으며, 다양한 환경(IDE)에 배포할 수 있습니다. 기존 API 관리 도구와 함께 사용하거나 REST API 위에서도 구현할 수 있습니다. </p>
<h4 id="동작-방식--graphql-파이프라인👇">동작 방식 : GraphQL 파이프라인👇</h4>
<pre><code class="language-text">[GraphQL Query] 

       ↓ (1. GraphQL 쿼리 서버로 전송)

[Query Language Processor]

   ┌─ Parse      
   └─ Validate

       ↓   (2. 파싱 및 유효성 검사)

[GraphQL Resolver]

    - RDB 

       ↓ (3. 리졸버가 필요한 데이터를 가져옴)

[Output (JSON)] (4. JSON 클라이언트에 반환)</code></pre>
<h2 id="graphql의-주요-용어">GraphQL의 주요 용어</h2>
<h4 id="객체-타입">객체 타입</h4>
<p>데이터 모델의 객체 필드를 정의합니다. 예시로, 사용자 객체는 이름과 이메일 같은 필드를 가집니다. </p>
<blockquote>
<p><code>!</code>는 non-null 타입을 나타냅니다. 해당 값을 가져야 하며, null이 될 수 없습니다.</p>
</blockquote>
<pre><code class="language-graphql"># 객체 타입 정의
type User {
    id: ID!          # 필드
    name : String!
    email: String!
}</code></pre>
<h4 id="필드">필드</h4>
<p>GraphQL 서버가 검색할 수 있는 데이터 타입을 정의합니다. 일반적으로 객체 타입에 속하며, 스칼라 타입(문자열,정수, 부동 소수점, 불리언, ID 등)으로도 정의됩니다. </p>
<h4 id="스키마">스키마</h4>
<p>API 개발자가 만든 스키마는 클라이언트가 요청할 수 있는 객체와 해당 객체의 필드를 정의합니다. GraphQL은 스키마 정의 언어(SDL)로 모든 데이터 타입을 기록하는 강력한 타입 시스템을 사용합니다. </p>
<pre><code class="language-graphql"># 스키마 정의 
schema {
    query: Query
    mutation: Mutation
    subscription: Subscription
}
</code></pre>
<h4 id="쿼리">쿼리</h4>
<p>GraphQL API에서 데이터를 검색(조회)하는 데 사용됩니다. 쿼리는 스키마에 따라 유효성을 검사한 후 실행됩니다. </p>
<pre><code class="language-graphql"># 쿼리 정의 
type Query {
    user(id: ID!) : User
    allUsers: [User!]!
}

# 사용 
query {
    # user 123의 데이터 가져오기
    user(id: &quot;123&quot;) {
        id
        name
        email
    }
}</code></pre>
<h4 id="변이-mutation-서버-데이터-변경용">변이 (Mutation, 서버 데이터 변경용)</h4>
<p>서버의 데이터를 생성, 업데이트, 삭제하는 GraphQL 작업입니다. RESTful API의 POST, PUT, PATCH, DELETE 작업과 유사합니다. </p>
<pre><code class="language-graphql"># 뮤테이션 정의 
type Mutation {
    createUser(name: String!, email: String!) : User!
    updateUser(id: ID!, name: String, email: String) : User
}

# 뮤테이션 사용 
mutation {
    createUser(name: &quot;Alice&quot;, email: &quot;alice@example.com&quot;) {
        id
        name
        email
    }
    updateUser(id: &quot;123&quot;, name: &quot;Alice Updated&quot;, email: &quot;alice@example.com&quot;) {
        id
        name
        email
    }
}</code></pre>
<h4 id="scalar">Scalar</h4>
<p>스칼라 타입은 기본적인 데이터 타입을 나타냅니다. 스칼라 타입은 더 이상 세분화되지 않는 단일 값을 표현합니다. 다음은 스칼라의 종류입니다.👇</p>
<ol>
<li>Int : 32bit integer </li>
<li>Float : 64bit 부동 소수점</li>
<li>String : UTF-8 encoded text</li>
<li>Boolean : true, false</li>
<li>ID : 고유 식별자를 나타내는 <code>문자열</code></li>
</ol>
<p>자바에선 필요한 의존성을 프로젝트에 추가하여 사용가능합니다. </p>
<pre><code class="language-graphql"># Scalar 정의 
scalar Date


# 스칼라 타입을 사용하는 객체 
type Event{
    id: ID!
    title: String!
    date: Date!
}</code></pre>
<h4 id="구독-subscription-실시간-업데이트-수신용">구독 (Subscription, 실시간 업데이트 수신용)</h4>
<p>GraphQL 서버로부터 실시간 데이터 업데이트를 받는 데 사용됩니다. </p>
<pre><code class="language-graphql"># 구독 정의
type Subscription {
    userAdded: User!
}
</code></pre>
<ul>
<li>구독 정의 : 스키마에서 구독을 정의합니다. </li>
<li>리졸버 구현 : 특정 이벤트가 발생할 때 데이터를 발행하는 리졸버를 구현합니다. </li>
<li>클라이언트에서는 구독을 요청하여 실시간 업데이트를 받을 수 있습니다. </li>
</ul>
<blockquote>
<p>구독 기능은 실시간 앱에서 유용하게 사용됩니다. </p>
</blockquote>
<h4 id="리졸버resolver">리졸버(Resolver)</h4>
<p>스키마의 각 필드에 대한 데이터를 채우는 함수입니다. 리졸버는 데이터베이스, 클라우드 서비스, 또는 다른 소스에서 데이터를 검색할 수 있습니다. 이를 통해 legacy 시스템을 GraphQL 기반으로 전환하는 데 활용할 수 있습니다. </p>
<blockquote>
<p>기존의 SQL은 데이터를 가져오기 위해서 SQL을 작성했습니다. 이는 데이터베이스에 데이터를 가져오는 구체적인 과정이 구현되어 있습니다. 하지만 GraphQL는 개발자가 리졸버 함수를 직접 구현해야 하며 각 필드마다 데이터를 어떻게 가져올지 정의해야 합니다. 이 차이로 인해 GraphQL은 더 유연하지만, 개발자가 데이터 검색 로직을 직접 구현해야 하는 책임을 갖게 됩니다. </p>
</blockquote>
<pre><code class="language-javascript">// 리졸버 함수 (javaScript)
const resolvers = {
  Query: {
    user: (parent, args, context) =&gt; {
      // 데이터베이스에서 사용자 조회 로직
    },
    allUsers: (parent, args, context) =&gt; {
      // 모든 사용자 조회 로직
    }
  },
  Mutation: {
    createUser: (parent, args, context) =&gt; {
      // 새 사용자 생성 로직
    },
    updateUser: (parent, args, context) =&gt; {
      // 사용자 정보 업데이트 로직
    }
  },
  Subscription: {
    userAdded: {
      subscribe: (parent, args, context) =&gt; {
        // 새 사용자 추가 이벤트 구독 로직
      }
    }
  }
}</code></pre>
<ul>
<li>parent : 이전 리졸버의 반환 값을 나타냅니다. 중첩된 쿼리에서 상위 필드의 결과를 하위 필드로 전달할 때 사용됩니다. </li>
<li>args: GraphQL 쿼리에서 해당 필드에 제공된 모든 인자를 포함하는 객체입니다. 예를 들어, &#39;user(id: &quot;123&quot;)&#39;와 같은 쿼리에서 args는 &#39;{id: &quot;123&quot;}&#39;이 됩니다. </li>
<li>context: 모든 리졸버가 공유하는 객체로 요청별 상태를 포함합니다. 주로 인증정보, 데이터베이스 연결, 데이터 로더 등 쿼리 해결에 필요한 공통 정보를 저장합니다. </li>
</ul>
<h4 id="인트로스펙션introspection">인트로스펙션(Introspection)</h4>
<p>서버에서 현재 정의된 스키마의 실시간 정보를 공유할 수 있게 하는 기능입니다. 이를 통해 클라이언트는 별로의 API 명세서 없이 서버의 스키마 정보를 실시간으로 확인할 수 있습니다. 보안상의 이유로 상용 환경에서는 이 기능의 사용을 신중히 고려해야 합니다. </p>
<h2 id="graphql의-장단점">GraphQL의 장단점</h2>
<p>GraphQL은 현대적인 API 개발 방식으로, 다양한 장점이 있어 매력적으로 다가오지만 몇 가지 고려해야 할 단점도 있습니다. 이를 정리하면 아래와 같습니다. 👇</p>
<h4 id="장점-1-오버페칭과-언더페칭-문제-해결">장점 1. 오버페칭과 언더페칭 문제 해결</h4>
<p>REST API에서 백엔드는 프론트엔드에 단일 요소만 필요한 경우에도 각 리소스에 사용할 수 있는 데이터를 정의하고 응답 시 모든 데이터를 반환하여 오버페칭을 발생시킵니다. 이와 달리 GraphQL은 클라이언트가 필요한 데이터만 정확히 요청하고 받을 수 있어 오버페칭과 언더페칭 문제를 해결합니다. </p>
<h4 id="장점-2-단일-엔드포인트와-신뢰할-수-있는-단일-소스">장점 2. 단일 엔드포인트와 신뢰할 수 있는 단일 소스</h4>
<p>GraphQL은 단일 엔드포인트를 통해 모든 데이터를 쿼리할 수 있으며, 관리가 용이합니다. 또한, GraphQL 스키마가 신뢰할 수 있는 단일 소스 역활을 합니다.</p>
<h4 id="장점-3-향상된-클라이언트-서버통신">장점 3. 향상된 클라이언트-서버통신</h4>
<p>강력한 타입 시스템으로 인해 클라이언트와 서버 간의 통신이 더 명확해집니다. </p>
<h4 id="장점-4-http-요청-및-응답-최적화">장점 4. HTTP 요청 및 응답 최적화</h4>
<p>단일 쿼리로 여러 리소스를 요청할 수 있어 HTTP 요청 횟수를 줄이고, 필요한 데이터만 응답받아 응답 크기를 최소화할 수 있습니다. </p>
<h4 id="이외의-장점">이외의 장점</h4>
<ul>
<li><strong>확장성과 내성</strong> : API를 쉽게 확장할 수 있으며, 내성적 특성으로 인해 스키마 정보를 쉽게 얻을 수 있습니다. </li>
<li><strong>실시간 업데이트를 위한 Subscription 지원</strong> : 실시간 데이터 업데이트가 필요한 애플리케이션에 적합합니다. </li>
<li><strong>개발자 부담 감소</strong> : 프론트엔드와 백엔드 개발자 간의 의존성을 줄여 개발 부담을 줄여줍니다. </li>
</ul>
<blockquote>
<p>RESTful API에서는 프론트엔드 개발자는 API에서 작성한 request/response 형식에 의존하게 되어 커뮤니케이션이 강제되는 경우가 많습니다.</p>
</blockquote>
<h4 id="graphql의-단점들">GraphQL의 단점들</h4>
<h4 id="단점-1-고유한-api-관리-전략">단점 1. 고유한 API 관리 전략</h4>
<p>GraphQL은 새로운 API 관리 전략이 필요할 수 있습니다. 반면, REST API는 기존 API 관리 모델에 적합한 경향이 있습니다. 만약 GraphQL을 도입하고 새로운 API 관리 전략을 추가하면 전체 비용이 증가할 수 있습니다. 또한 서버 개발자는 쉽게 유지 관리할 수 있는 데이터 모델을 개발하는 데 더 많은 시간을 사용해야 합니다. </p>
<h4 id="단점-2-복잡한-캐싱">단점 2. 복잡한 캐싱</h4>
<p>GraphQL에서의 캐싱은 REST에 비해 더 복잡합니다. 그 이유에는 GraphQL 요청이 HTTP 메서드에서 POST만 사용합니다. HTTP 프로토콜에서 POST 요청은 서버의 상태를 변경할 수 있는 작업에 주로 사용되기 때문에 기본적으로 캐시되지 않습니다.  </p>
<blockquote>
<p>GraphQL에서는 캐싱을 구현하기 위해 추가적인 솔루션이나 커스텀 캐싱 메커니즘이 필요합니다.  </p>
</blockquote>
<p>또한, GraphQL은 단일 엔드포인트만 사용합니다. 이것은 엔드포인트의 URL이 캐시할 수 없는 여러 가지 다양한 응답을 생성한다는 것을 의미합니다. 이러한 이유로 캐싱이 복잡해질 수 있습니다. </p>
<h2 id="결론">결론</h2>
<p>마무리로 주요 장점과 고려사항을 요약하자면 👇</p>
<h4 id="graphql의-주요-이점">GraphQL의 주요 이점</h4>
<ul>
<li>오버페칭과 언더페칭 문제 해결</li>
<li>단일 엔드포인트를 통한 효율적인 데이터 요청 </li>
<li>강력한 타입 시스템으로 인한 명확한 클라이언트-서버 통신</li>
<li>실시간 데이터 업데이트 지원(Subscriptioon)</li>
<li>개발자(ex. 프론트엔드와 백엔드) 간 의존성 감소 </li>
</ul>
<h4 id="도입-시-고려사항">도입 시 고려사항</h4>
<ul>
<li>새로운 API 관리 전략 필요</li>
<li>복잡한 캐싱 메커니즘</li>
</ul>
<blockquote>
<p>GraphQL이 복잡한 웹 및 모바일 애플리케이션에 대응하기 위해 출시됐지만 REST를 완전히 대체하기보다는 각 프로젝트 요구사항에 맞춰 적절히 선택하는 것이 중요합니다. 
각 프로젝트의 요구사항에 따라 적절한 기술을 선택하는 것이 중요해 보입니다. REST와 GraphQL의 장단점을 고려하여 서비스에 가장 적합한 방식을 채택해야 합니다. 특히 복잡한 데이터 구조를 가진 프로젝트나 빠른 개발 주기가 필요한 애자일 환경에서 큰 장점을 발휘할 것이라 생각합니다. 필자가 생각하기에는 신규 기술이 아닌 유지 보수 단계에서는 어려울 것이라 생각하며, 돋보이는 장점이 있지 않은 한, 기존의 레거시 코드에서 넘어가는 것은 힘들 것이라는 생각이 있습니다. </p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>