<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>jong-kyung.log</title>
        <link>https://velog.io/</link>
        <description>잘 하고 싶어요</description>
        <lastBuildDate>Sat, 28 Feb 2026 09:55:53 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>jong-kyung.log</title>
            <url>https://velog.velcdn.com/images/jong-kyung/profile/657b6eb2-2a1c-431b-b662-ce346ca8077a/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. jong-kyung.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jong-kyung" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Rollup의 번들링 과정]]></title>
            <link>https://velog.io/@jong-kyung/Rollup%EC%9D%98-%EB%B2%88%EB%93%A4%EB%A7%81-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@jong-kyung/Rollup%EC%9D%98-%EB%B2%88%EB%93%A4%EB%A7%81-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Sat, 28 Feb 2026 09:55:53 GMT</pubDate>
            <description><![CDATA[<h2 id="rollup이란">Rollup이란?</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/29608bb9-b508-4461-9b2b-eb5bc6e7f8bd/image.png" alt="Rollup bundling"></p>
<p>Rollup은 JavaScript 모듈 번들러로, 여러 모듈을 하나 또는 여러 개의 최적화된 번들 파일로 합쳐주는 도구입니다. ESM을 기반으로 하며, 정적 분석을 통한 <strong>Tree-shaking</strong>을 핵심 기능으로 제공합니다. 또한 플러그인 생태계가 잘되어 있어 Vite에 채택되어 프로덕션 빌드시 사용되는 번들러입니다.</p>
<h3 id="주요-용어-정리">주요 용어 정리</h3>
<p>본격적인 빌드 과정을 알아보기 전에, Rollup 내부에서 사용되는 핵심 용어를 정리해 보겠습니다.</p>
<table>
<thead>
<tr>
<th>컴포넌트</th>
<th><strong>목적</strong></th>
<th><strong>핵심 역할</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong><code>Graph</code></strong></td>
<td>중앙 관리</td>
<td>전체 빌드 단계를 조율하고, 모듈 간의 연결 상태를 담은 모듈 그래프를 관리합니다</td>
</tr>
<tr>
<td><strong><code>Module</code></strong></td>
<td>소스 파일 관리</td>
<td>소스 코드를 AST로 파싱하고, 파일 내의 의존성을 분석하며, <code>export</code>/<code>import</code> 구문을 관리합니다.</td>
</tr>
<tr>
<td><strong><code>Chunk</code></strong></td>
<td>출력물</td>
<td>관된 모듈들을 하나의 파일로 그룹화하고, 생성된 청크들 사이의 의존성을 처리합니다.</td>
</tr>
<tr>
<td><strong><code>ModuleLoader</code></strong></td>
<td>모듈 해석</td>
<td>실제 파일을 로드하고, <code>import</code> 된 파일의 경로를 해석하며, 메모리 상에 모듈 인스턴스를 생성합니다.</td>
</tr>
<tr>
<td><strong><code>PluginDriver</code></strong></td>
<td>플러그인 관리</td>
<td>빌드 프로세스에 맞춰 플러그인 훅을 실행하고, 여러 플러그인 간의 실행 순서와 동작을 조율합니다.</td>
</tr>
<tr>
<td><strong><code>ExternalModule</code></strong></td>
<td>외부 의존성</td>
<td>최종 번들 결과물에 포함시키지 않고 외부 참조로 남겨두는 모듈</td>
</tr>
</tbody></table>
<h2 id="번들링-동작-원리">번들링 동작 원리</h2>
<p>전체적인 빌드 과정은 아래와 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/ec5d6485-8cc1-4667-b7d9-c48e1a3e6a5d/image.png" alt="rollup bundling process"></p>
<p>그리고 이 빌드 과정에서 크게 <strong>Build Phase(빌드 단계)</strong> 와 <strong>Generate Phase(생성 단계)</strong> 두 단계로 나뉩니다.</p>
<h3 id="build-phase">Build Phase</h3>
<p>빌드 단계는 최상위 <code>rollup(inputOptions)</code> 함수에 의해 시작되며 <code>Graph.build()</code> 메서드를 통해 수행됩니다. 이 단계가 끝나면 <code>generate</code> 혹은 <code>wirte</code> 를 포함한 <code>bundle</code> 객체를 반환합니다.</p>
<pre><code class="language-typescript">// src/Graph.ts
class Graph {
  // 생략
    async build(): Promise&lt;void&gt; {
        timeStart(&#39;generate module graph&#39;, 2);
        await this.generateModuleGraph(); // 모듈 그래프 생성
        timeEnd(&#39;generate module graph&#39;, 2);

        timeStart(&#39;sort and bind modules&#39;, 2);
        this.phase = BuildPhase.ANALYSE;
        this.sortModules(); // 모듈 정렬
        timeEnd(&#39;sort and bind modules&#39;, 2);

        timeStart(&#39;mark included statements&#39;, 2);
        this.includeStatements(); // 번들에 포함될 구문을 결정(Tree-shaking)
        timeEnd(&#39;mark included statements&#39;, 2);

        this.phase = BuildPhase.GENERATE;
    }
    // 생략
}</code></pre>
<ol>
<li><p><strong>모듈 그래프 생성</strong></p>
<p> <code>module Loader</code> 는 모든 모듈을 로드하고 파싱하는 역할을 담당합니다. 진입점에서 시작하여 각 모듈의 종속성을 파악하고 재귀적으로 로드합니다.각 모듈에 대해 Rollup은 <code>resolvedId</code>, <code>load</code>, <code>transform</code> 플러그인 훅을 순서대로 호출합니다.</p>
<pre><code class="language-typescript"> // src/Graph.ts
 class Graph {
   // 생략  
     private async generateModuleGraph(): Promise&lt;void&gt; {
         ({ entryModules: this.entryModules, implicitEntryModules: this.implicitEntryModules } =
             await this.moduleLoader.addEntryModules(normalizeEntryModules(this.options.input), true)); // module Loader를 통해 AST 파싱 및 의존성 분석
         if (this.entryModules.length === 0) {
             throw new Error(&#39;You must supply options.input to rollup&#39;);
         }
         for (const module of this.modulesById.values()) {
             module.cacheInfoGetters();
             if (module instanceof Module) {
                 this.modules.push(module);
             } else {
                 this.externalModules.push(module);
             }
         }
     }
     // 생략
 }</code></pre>
</li>
<li><p><strong>모듈 분석 및 정렬</strong>
전체 모듈 그래프가 완성된 후, <code>sortModules</code> 를 통해 모듈을 정렬합니다. 이는 모듈이 올바른 종속성 순서에 따라 처리되도록 보장합니다.</p>
</li>
</ol>
<pre><code class="language-typescript">// src/Graph.ts
class Graph {
  // 생략
    private sortModules(): void {
        const { orderedModules, cyclePaths } = analyseModuleExecution(this.entryModules); // 먼저 실행될 모듈 및 순환 참조 모듈 분석
        for (const cyclePath of cyclePaths) {
            this.options.onLog(LOGLEVEL_WARN, logCircularDependency(cyclePath));
        }
        this.modules = orderedModules;
        for (const module of this.modules) {
            module.bindReferences(); // AST들이 실제로 어떤 모듈에서 import되고 어디서 선언되었는지 연결
        }
        this.warnForMissingExports();
    }
    // 생략
}</code></pre>
<ol start="3">
<li><strong>Tree-Shaking
모듈이 정렬되면 Rollup은 정적 분석을 수행하여 실제로 사용되는 내보내기(exports)와 구문(statements)이 무엇인지 파악하고, 포함할 항목만 마킹하는 트리 쉐이킹을 진행합니다.</strong><pre><code class="language-typescript">// src/Graph.ts
class Graph {
// 생략
 private includeStatements(): void {
     const entryModules = [...this.entryModules, ...this.implicitEntryModules];
     for (const module of entryModules) {
         markModuleAndImpureDependenciesAsExecuted(module); // side Effect가 있는 모듈과 없는 모듈 분리
     }
     if (this.options.treeshake) {
         let treeshakingPass = 1;
         this.newlyIncludedVariableInits.clear();
         do { // 트리셰이킹 알고리즘
             timeStart(`treeshaking pass ${treeshakingPass}`, 3);
             this.needsTreeshakingPass = false;
             for (const module of this.modules) {
                 if (module.isExecuted) {
                     module.hasTreeShakingPassStarted = true;
                     if (module.info.moduleSideEffects === &#39;no-treeshake&#39;) {
                         module.includeAllInBundle();
                     } else {
                         module.include();
                     }
                     for (const entity of this.newlyIncludedVariableInits) {
                         this.newlyIncludedVariableInits.delete(entity);
                         entity.include(createInclusionContext(), false);
                     }
                 }
             }
             if (treeshakingPass === 1) {
                 // We only include exports after the first pass to avoid issues with
                 // the TDZ detection logic
                 for (const module of entryModules) {
                     if (module.preserveSignature !== false) {
                         module.includeAllExports(false);
                         this.needsTreeshakingPass = true;
                     }
                 }
             }
             timeEnd(`treeshaking pass ${treeshakingPass++}`, 3);
         } while (this.needsTreeshakingPass);
     } else {
         for (const module of this.modules) module.includeAllInBundle();
     }
     for (const externalModule of this.externalModules) externalModule.warnUnusedImports();
     for (const module of this.implicitEntryModules) {
         for (const dependant of module.implicitlyLoadedAfter) {
             if (!(dependant.info.isEntry || dependant.isIncluded())) {
                 error(logImplicitDependantIsNotIncluded(dependant));
             }
         }
     }
 }
 // 생략
}</code></pre>
</li>
</ol>
<h3 id="generate-phase">Generate Phase</h3>
<pre><code class="language-typescript">class Bundle {
    // 생략
    async generate(isWrite: boolean): Promise&lt;OutputBundle&gt; {
            timeStart(&#39;GENERATE&#39;, 1);
            const outputBundleBase: OutputBundle = Object.create(null);
            const outputBundle = getOutputBundle(outputBundleBase);
            this.pluginDriver.setOutputBundle(outputBundle, this.outputOptions);

            try {
                timeStart(&#39;initialize render&#39;, 2);

                await this.pluginDriver.hookParallel(&#39;renderStart&#39;, [this.outputOptions, this.inputOptions]);

                timeEnd(&#39;initialize render&#39;, 2);
                timeStart(&#39;generate chunks&#39;, 2);

                const getHashPlaceholder = getHashPlaceholderGenerator();
                const chunks = await this.generateChunks(outputBundle, getHashPlaceholder); // 실제 출력될 청크 (청크 할당/청크 연결)
                if (chunks.length &gt; 1) {
                    validateOptionsForMultiChunkOutput(this.outputOptions, this.inputOptions.onLog); // IIFE, UMD 등 멀티 청크 출력을 지원하지 않는 포맷에 대해 유효성 검사
                }
                this.pluginDriver.setChunkInformation(this.facadeChunkByModule);
                for (const chunk of chunks) {
                    chunk.generateExports();
                    chunk.inlineTransitiveImports();
                }

                timeEnd(&#39;generate chunks&#39;, 2);


                await renderChunks( // AST였던 각 청크를 실제 자바스크립트 문자열 코드로 렌더링 및 소스맵 결합
                    chunks,
                    outputBundle,
                    this.pluginDriver,
                    this.outputOptions,
                    this.inputOptions.onLog
                );
            } catch (error_: any) {
                await this.pluginDriver.hookParallel(&#39;renderError&#39;, [error_]);
                throw error_;
            }

            removeUnreferencedAssets(outputBundle); // 참조되지 않은 에셋 정리

            timeStart(&#39;generate bundle&#39;, 2);

            await this.pluginDriver.hookSeq(&#39;generateBundle&#39;, [
                this.outputOptions,
                outputBundle as OutputBundle,
                isWrite
            ]);
            this.finaliseAssets(outputBundle);

            timeEnd(&#39;generate bundle&#39;, 2);
            timeEnd(&#39;GENERATE&#39;, 1);
            return outputBundleBase;
        }
    // 생략
    private async generateChunks(
        bundle: OutputBundleWithPlaceholders,
        getHashPlaceholder: HashPlaceholderGenerator
    ): Promise&lt;Chunk[]&gt; {
        const { experimentalMinChunkSize, inlineDynamicImports, manualChunks, preserveModules } =
            this.outputOptions; // 청크 분할 전략
        const manualChunkAliasByEntry = // 청크 설정
            typeof manualChunks === &#39;object&#39;
                ? await this.addManualChunks(manualChunks) // 객체 설정
                : this.assignManualChunks(manualChunks); // 함수 설정
        const snippets = getGenerateCodeSnippets(this.outputOptions);
        const includedModules = getIncludedModules(this.graph.modulesById);
        const inputBase = commondir(getAbsoluteEntryModulePaths(includedModules, preserveModules));
        const externalChunkByModule = getExternalChunkByModule(
            this.graph.modulesById,
            this.outputOptions,
            inputBase
        );
        const executableModule = inlineDynamicImports // 청크 할당
            ? [{ alias: null, modules: includedModules }] // 모든 모듈을 1개의 거대한 청크로 병합
            : preserveModules
                ? includedModules.map(module =&gt; ({ alias: null, modules: [module] })) // 모든 모듈을 각각 1:1로 쪼개어 원본 파일 구조 유지
                : getChunkAssignments( // 엔트리 포인트, 동적 임포트, manualChunks 규칙에 따라 그룹핑
                        this.graph.entryModules,
                        manualChunkAliasByEntry,
                        experimentalMinChunkSize,
                        this.inputOptions.onLog
                    );
        const chunks: Chunk[] = new Array(executableModule.length);
        const chunkByModule = new Map&lt;Module, Chunk&gt;();
        let index = 0;
        for (const { alias, modules } of executableModule) {
            sortByExecutionOrder(modules); // 청크내 모듈 순서 정렬
            const chunk = new Chunk(
                modules,
                this.inputOptions,
                this.outputOptions,
                this.unsetOptions,
                this.pluginDriver,
                this.graph.modulesById,
                chunkByModule,
                externalChunkByModule,
                this.facadeChunkByModule,
                this.includedNamespaces,
                alias,
                getHashPlaceholder,
                bundle,
                inputBase,
                snippets
            );
            chunks[index++] = chunk;
        }
        for (const chunk of chunks) {
            chunk.link(); // 청크간 연결
        }
        const facades: Chunk[] = [];
        for (const chunk of chunks) {
            facades.push(...chunk.generateFacades());
        }
        return [...chunks, ...facades];
    }
    // 생략
}</code></pre>
<ol>
<li><p><strong>청크 할당 (Chunk Assignment)</strong></p>
<p> Rollup은 어떤 모듈이 어떤 청크에 속할지 결정합니다. 진입 모듈은 각각 고유한 청크를 할당받으며, 공통으로 사용되는 종속성은 별도의 청크로 분리될 수 있습니다.</p>
</li>
<li><p><strong>청크 연결 (Chunk Linking)</strong></p>
<p> 청크 할당 후, Rollup은 청크 간의 가져오기(imports) 및 내보내기(exports) 관계를 설정하고 여러 청크에 걸쳐 변수명이 충돌하지 않도록 조정(deconflicting)하여 청크들을 서로 연결합니다.</p>
</li>
<li><p><strong>청크 렌더링 (Chunk Rendering)</strong>
각 <code>Chunk</code>는 자신이 포함한 모듈들을 최종 출력 코드로 렌더링합니다. 이 단계에서 <code>renderChunk</code>와 같은 플러그인 훅이 호출됩니다.</p>
</li>
</ol>
<h2 id="ast-파싱-방식">AST 파싱 방식</h2>
<p>Rollup은 WebAssembly(Rust)내에서 AST 파싱을 수행합니다. 현재 Rollup의 대부분은 여전히 TypeScript 기반이므로, 파싱된 결과물을 JavaScript 표현으로 변환해야 합니다. 효율적인 처리를 위해 Rust에서 AST 결과를 JSON 객체로 직렬화하여 복사하여 JS로 넘기는 대신 바이너리 버퍼를 생성하여 TypeScript로 전달합니다.</p>
<blockquote>
<p>버퍼로의 변환은 주로 SWC기반의 <code>converter.rs</code>에서 처리되며, 버퍼가 <a href="https://github.com/estree/estree"><code>ESTree</code></a> 형식을 따르도록 합니다.</p>
</blockquote>
<h2 id="마치며">마치며</h2>
<p>지금까지 Rollup이 어떻게 소스코드를 분석하고, 다시 하나의 파일로 만들어내는지, 그리고 성능을 끌어올리기 위해 내부적으로 어떤 파서를 쓰고 있는지 가볍게 살펴보았습니다.</p>
<p>사실 Vite 팀은 차세대 Rollup으로 Rolldown을 개발하고 있는데, 아직 Vite 8이 안정화 되기 전까지는 Vite 8 이전 버전이 사용될 것 같아서 Rollup을 잘 활용해보기 위해 다음 글에서는 Rollup의 AST 파싱 방식을 보다 깊이 알아보려고 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vite Dual-Mode Architecture]]></title>
            <link>https://velog.io/@jong-kyung/Vite-Dual-Mode-Architecture</link>
            <guid>https://velog.io/@jong-kyung/Vite-Dual-Mode-Architecture</guid>
            <pubDate>Fri, 20 Feb 2026 03:38:20 GMT</pubDate>
            <description><![CDATA[<h2 id="vite의-두-가지-모드">Vite의 두 가지 모드</h2>
<p>Vite가 나오기 이전의 번들러들은 Vite와 같이 모드를 명시적으로 분리하여 각 모드별로 동작 방식의 차이를 두지 않았습니다. 대부분의 번들러는 개발 모드와 배포 모드 상관없이 <strong>모든 파일을 번들링</strong>하였기에, 코드를 수정하면 전체 번들링을 다시 해야 했습니다. 결과적으로 프로젝트 규모가 커짐에 따라 수정 사항을 화면에서 확인하기까지 기다려야 하는 시간 또한 증가했습니다.</p>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/ae1e7bd2-8b26-4f86-a303-fcfefe056b6e/image.png" alt="bundle-based-server"></p>
<p>이에 Vite는 이러한 병목 현상을 개선하고자 기존의 번들러들과 달리 <strong>Development Mode(개발 모드)</strong>와 <strong>Production Mode(빌드 모드)</strong>에 따라 빌드 방식을 분리하였습니다.</p>
<h3 id="development-mode">Development Mode</h3>
<p>Vite의 Development Mode에서는 <code>esbuild</code>를 활용하여 디펜던시를 비롯한 모든 것을 ESM으로 변환합니다.</p>
<p>Vite는 사전 번들링 과정의 일환으로 ESM 종속성을 여러 내부 모듈과 병합하여 단일 모듈로 성능을 최적화합니다. 예를 들어, <code>lodash-es</code>에는 600개 이상의 내부 모듈이 포함되어 있습니다. 브라우저가 네이티브 ESM을 사용할 때, 사전 번들링을 거치지 않고 <code>debounce</code>와 같은 기능을 가져오게 되면 600개 이상의 HTTP 요청을 트리거하게 됩니다. Vite는 이를 단일 모듈로 사전 번들링하여 HTTP 요청을 하나로 줄입니다. 이러한 네트워크 요청의 획기적인 감소는 개발 서버에서 페이지 로드 속도를 비약적으로 향상시킵니다.</p>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/ba1512f3-ae90-45f0-ba11-fa2bec033eb5/image.png" alt="native-esm-based-server"></p>
<h3 id="production-mode">Production Mode</h3>
<p>Vite의 Production Mode에서는 Development Mode와 달리 Rollup을 채택하여 번들링합니다.</p>
<p>Vite의 현재 <a href="https://vite.dev/guide/api-plugin">플러그인 API</a>는 <code>esbuild</code>를 프로덕션 번들러로 사용하는 것과 호환되지 않습니다. <code>esbuild</code>가 속도 면에서는 훨씬 빠르지만, Vite는 esbuild보다 유연한 플러그인 API를 제공하는 Rollup을 채택하여 단순한 속도보다는 프로덕션에서의 유연성과 안정성을 우선시하였습니다.</p>
<p>하지만, 최근 Vite 팀은 Rollup의 Rust 포팅인 <a href="https://rolldown.rs/">Rolldown</a>을 만드는 작업을 진행해 왔으며, Vite 8부터는 이 Rolldown을 통해 Rollup과 esbuild를 모두 대체할 예정입니다. 이를 통해 빌드 성능을 극대화하고 개발과 빌드 환경 사이의 설정 불일치를 제거하도록 준비하고 있습니다.</p>
<h2 id="esbuild와-rollup-vite-7까지의-아키텍처">esbuild와 Rollup (Vite 7까지의 아키텍처)</h2>
<p>Vite 7까지는 esbuild와 Rollup이 함께 사용되었습니다. 크게 두 분류로 보면 esbuild는 개발 모드에서, Rollup은 빌드 모드에서 사용된 것으로 볼 수 있습니다. 하지만 esbuild가 단순히 개발 모드에서만 제한적으로 사용된 것은 아닙니다. 두 도구의 역할을 좀 더 자세히 알아보겠습니다.</p>
<h3 id="esbuild">esbuild</h3>
<p>esbuild는 위에서 설명하였듯이 개발 모드에서 <strong>의존성 사전 번들링</strong>에 사용됩니다. 하지만 사전 번들링 외에도, esbuild는 개발과 빌드 모드 모두에서 TypeScript나 React의 JSX 구문 등을 빠르게 변환하는 <strong>트랜스파일링</strong>을 담당합니다. 또한 빌드 모드에서는 <strong>자바스크립트의 최소화 작업</strong>을 esbuild가 처리합니다.</p>
<blockquote>
<p>Vite 7의 <a href="https://vite.dev/config/build-options#build-minify">Build Options</a> 을 참고하면, 기본 옵션이 ‘esbuild’로 사용되고 있습니다.</p>
</blockquote>
<h3 id="rollup">Rollup</h3>
<p>Rollup은 프로덕션 환경에서 모든 모듈을 <strong>최적화된 파일로 묶어주는 번들러</strong> 역할을 합니다. Rollup은 전체 프로젝트의 <strong>코드 스플리팅</strong>과 <strong>청크</strong>와 같은 최적화를 담당합니다.</p>
<p>프로젝트의 규모가 커지고 다양한 라이브러리와 복잡한 컴포넌트 구조가 도입될수록, Rollup이 제공하는 정교한 번들링 제어와 플러그인 생태계는 매력적이었습니다. 하지만 개발 모드와 빌드 모드에서 사용하는 도구가 esbuild와 rollup으로 다르다 보니 CJS/ESM 처리 방식이 다른 어려움이 있었고 프로덕션 빌드시에서만 발견되는 버그가 있는 등 유지보수가 난해했습니다.</p>
<h2 id="oxc와-rolldown-vite-8의-통합-아키텍처">Oxc와 Rolldown (Vite 8의 통합 아키텍처)</h2>
<p>이렇게 번들러가 나뉘어져있는 한계를 극복하고 성능 향상을 이루기 위해, Vite 8은 Rust 기반의 새로운 통합 툴체인을 전면 도입했습니다.</p>
<h3 id="oxc">Oxc</h3>
<p>Oxc(<code>@oxc-project/runtime</code>)는 기존 esbuild가 담당하던 <strong>JavaScript/TypeScript 변환 및 코드 최소화</strong>를 완벽하게 대체하는 Rust 기반의 고성능 컴파일러입니다. Vite 8에서 Oxc는 구체적으로 다음과 같은 변환 동작을 수행합니다.</p>
<ul>
<li><strong>트랜스파일링:</strong> TypeScript의 타입들을 제거하고, React 등의 프레임워크를 위한 JSX/TSX 구문을 순수 JavaScript로 변환합니다.</li>
<li><strong>구문 변환:</strong> 내부적으로 <code>transformWithOxc()</code> 함수를 거쳐 최신 JS/TS 문법을 타겟 브라우저 환경에 맞는 코드로 다운그레이드합니다.</li>
<li><strong>코드 최소화:</strong> 프로덕션 빌드 시 <code>build.minify: &#39;oxc&#39;</code> 설정을 통해 코드를 최적화하고 용량을 압축합니다.</li>
<li><strong>데코레이터 지원:</strong> TypeScript의 실험적 데코레이터 기능을 지원합니다.</li>
</ul>
<h3 id="rolldown">Rolldown</h3>
<p>Rolldown은 Vite 8의 핵심으로, 기존의 esbuild와 Rollup을 <strong>모두 대체하는 통합 번들러</strong>입니다. </p>
<p>Rust로 작성되어 자바스크립트로 작성된 기존의 Rollup 대비 <strong>10~30배 이상 빠른 빌드 속도이고</strong>, Rollup의 플러그인 API와 호환되도록 설계되어 기존 생태계를 그대로 사용할 수 있습니다. </p>
<ul>
<li><strong>의존성 사전 번들링:</strong> 개발 서버 실행 시 기존 esbuild가 하던 <code>node_modules</code> 최적화를 Rolldown이 담당하여 <code>.vite/deps</code> 폴더에 사전 번들링합니다. 이를 통해 CommonJS 모듈을 ESM으로 변환하고 HTTP 요청 수를 줄입니다.</li>
<li><strong>프로덕션 번들링:</strong> 기존 Rollup의 역할이었던 코드 스플리팅과 트리쉐이킹을 수행하여 최적화된 결과물을 생성합니다.</li>
<li><strong>웹 워커 처리:</strong> 메인 스레드와 분리된 웹 워커를 위한 별도의 번들도 생성하고 관리합니다.</li>
</ul>
<h2 id="결론">결론</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/8c14c1ee-a45b-4a18-9cca-c69ec0034ac9/image.png" alt="Vite Future"></p>
<p>Vite 8의 가장 큰 변화는 Rolldown을 통해 개발 환경과 프로덕션 환경이 드디어 하나의 번들러로 통합되었다는 점입니다. 이로 인해 두 환경 간의 미묘한 동작 불일치가 완전히 사라졌으며, 개발자에게 빠르면서도 일관된 개발 경험을 제공하게 되었습니다.</p>
<p>또한, 아키텍처의 통합은 빌드 파이프라인의 비효율성을 개선했습니다. 기존 환경에서 다양한 플러그인을 조합할 경우 <code>Go 프로세스(esbuild)</code> → <code>JavaScript 메인 스레드(Rollup)</code> → <code>Rust(SWC)</code> → <code>JavaScript</code>로 이어지는 복잡한 파이프라인을 거쳐야 하는 최악의 경우가 발생하기도 했습니다. Vite 8은 이러한 파편화된 도구들을 단일 Rust 기반 환경(Rolldown + Oxc)으로 통합함으로써, 각 프로세스 및 스레드 간 코드를 주고받을 때 발생하는 데이터 전송(AST 직렬화 및 역직렬화) 오버헤드를 줄이고 성능을 높였습니다.</p>
<p>더 나아가, <a href="https://voidzero.dev/">VoidZero(v0)</a> 팀은 단순히 Vite의 개선을 넘어 <strong>자바스크립트 툴체인 통합</strong>을 목표로 하고 있습니다. 프론트엔드 개발 환경의 패러다임이 크게 변화하고 있는 만큼, 앞으로 Vite와 VoidZero 생태계에 지속적인 관심을 가질 필요가 있겠습니다.</p>
<blockquote>
<p><strong>참고</strong>
<a href="https://vite.dev/guide/">https://vite.dev/guide/</a>
<a href="https://rolldown.rs/guide/introduction#why-rolldown">https://rolldown.rs/guide/introduction#why-rolldown</a>
<a href="https://deepwiki.com/vitejs/vite">https://deepwiki.com/vitejs/vite</a>
<a href="https://kinsta.com/blog/vite-vs-webpack/">https://kinsta.com/blog/vite-vs-webpack/</a>
<a href="https://voidzero.dev/posts/announcing-voidzero-inc">https://voidzero.dev/posts/announcing-voidzero-inc</a>
<a href="https://www.youtube.com/watch?v=EKvvptbTx6k">https://www.youtube.com/watch?v=EKvvptbTx6k</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[모듈 시스템과 번들러의 모든 것]]></title>
            <link>https://velog.io/@jong-kyung/%EB%AA%A8%EB%93%88-%EC%8B%9C%EC%8A%A4%ED%85%9C%EA%B3%BC-%EB%B2%88%EB%93%A4%EB%9F%AC%EC%9D%98-%EB%AA%A8%EB%93%A0-%EA%B2%83</link>
            <guid>https://velog.io/@jong-kyung/%EB%AA%A8%EB%93%88-%EC%8B%9C%EC%8A%A4%ED%85%9C%EA%B3%BC-%EB%B2%88%EB%93%A4%EB%9F%AC%EC%9D%98-%EB%AA%A8%EB%93%A0-%EA%B2%83</guid>
            <pubDate>Fri, 06 Feb 2026 08:04:37 GMT</pubDate>
            <description><![CDATA[<p>초기 웹에서 자바스크립트는 UI 토글, 간단한 애니메이션 등 작은 수준의 동적 처리를 담당하는 보조적인 언어였습니다 그래서 모듈 시스템을 별도로 도입할 필요가 크지 않았고, 필요한 스크립트를 HTML에 <code>&lt;script&gt;</code> 태그로 순서대로 추가하는 방식이 일반적이었습니다. 하지만 브라우저의 성능이 좋아지고 프론트엔드가 복잡한 기능을 맡기 시작하면서 코드량이 급격히 늘었고, 파일 간 의존성과 네임스페이스 충돌, 재사용성 문제 때문에 기존 방식만으로는 유지보수가 어려워졌습니다.</p>
<h2 id="전역-스코프-문제">전역 스코프 문제</h2>
<p>초기 브라우저 환경에서 자바스크립트 파일을 여러 개로 나누더라도, <code>&lt;script&gt;</code>로 로드된 코드는 <strong>한 페이지의 전역 컨텍스트</strong> 위에서 <strong>순서대로 실행</strong>됩니다. 그 결과 파일이 분리되어 있어도 전역 변수와 함수는 서로 공유되며, 쉽게 접근 및 수정될 수 있습니다.</p>
<pre><code class="language-javascript">// A.js
var name = &#39;foo&#39;;

// B.js
function sayHello() {
  alert(&#39;Hello &#39; + name); // &#39;Hello foo&#39;
}</code></pre>
<pre><code class="language-html">&lt;!-- index.html --&gt;
&lt;html&gt;
  &lt;script src=&quot;/src/A.js&quot; /&gt;
  &lt;script src=&quot;/src/B.js&quot; /&gt;
&lt;/html&gt;</code></pre>
<p>위 예시에서 <code>name</code>은 전역에 선언되므로 모든 스크립트가 같은 값을 공유합니다. 이는 편리해 보이지만, 다른 파일이 같은 이름을 사용하거나 값을 변경하면 의도치 않은 사이드 이펙트가 생길 수 있고,  <strong>의존성이 명시되지 않는다는 것</strong>입니다. </p>
<h3 id="당시의-임시-해결책">당시의 임시 해결책</h3>
<p>개발자들은 전역 오염을 줄이기 위해 여러 패턴을 사용했습니다. 대표적으로는 코드 영역을 함수로 감싸 전역 노출을 최소화하는 <strong>IIFE(즉시 실행 함수)</strong>, 그리고 전역 객체 하나에 기능을 모아 담는 <strong>네임스페이스 패턴</strong>이 있습니다.</p>
<pre><code class="language-javascript">// 1. IIFE (즉시 실행 함수) 패턴: 함수 스코프로 변수를 보호
(function() {
  var privateVar = &#39;secret&#39;; // 외부에서 접근 불가
  window.myModule = {
    reveal: function() { console.log(privateVar); }
  };
})();

// 2. 네임스페이스 패턴: 객체 하나에 모든 기능을 담음
var MyApp = {};
MyApp.Math = {
  add: function(a, b) { return a + b; }
};
</code></pre>
<p>이러한 패턴은 전역 오염을 어느 정도 완화했지만, 모듈 간 의존성을 명시적으로 선언하거나 재사용 단위를 표준화하는 근본적인 해결책은 아니었습니다. 결과적으로 의존성 관리와 로드 순서 제어는 여전히 개발자가 수동으로 관리해야 했고, 규모가 커질수록 유지보수 비용이 증가했습니다.</p>
<h2 id="모듈-시스템">모듈 시스템</h2>
<h3 id="cjs-commonjs">CJS (CommonJS)</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/9545753e-ad6d-4ecd-9db8-937034c6b836/image.png" alt="CJS"></p>
<p>2009년 Kevin Dangoor 등을 중심으로 서버 사이드 JavaScript에 적합한 모듈 표준을 만들기 위한 논의가 시작되었고, 그 결과 CommonJS가 등장했습니다. 초기에는 ServerJS라는 이름으로 불렸지만, 서버 환경에만 국한되지 않고 범용적인 모듈 표준을 지향하면서 CommonJS로 개명되었습니다. 이후 CommonJS 모듈 사양은 Node.js에 채택되어 <strong>Node.js의 기본 모듈 시스템</strong>으로 자리 잡게 됩니다.</p>
<p>CommonJS에서는 한 파일이 하나의 모듈이며, <code>module.exports</code> 또는 <code>exports</code>를 통해 내보낼 값을 정의하고, 다른 모듈에서는 <code>require()</code> 함수로 이를 불러옵니다.</p>
<pre><code class="language-jsx">// CommonJS 모듈 정의
module.exports = foo;

// CommonJS 모듈 사용
const foo = require(&#39;./foo&#39;);</code></pre>
<p>CommonJS의 중요한 특징은 <strong>모듈 로딩이 동기적</strong>이라는 점입니다. 즉, <code>require()</code>가 호출되면 해당 모듈을 로드하고 평가한 뒤 결과를 반환합니다. 이 설계는 파일 시스템 접근이 자연스러운 서버 환경에서는 유효했지만, 네트워크 지연이 발생하는 브라우저 환경에서는 그대로 적용하기 어려웠습니다.</p>
<p>이를 보완하기 위해 브라우저에서 CommonJS 스타일의 모듈을 사용할 수 있게 해주는 빌드 도구가 등장했고, 그 대표적인 예가 <a href="http://browserify.org/">Browserify</a>입니다.</p>
<h3 id="amd-asyncronous-module-definition">AMD (Asyncronous Module Definition)</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/2e74e59f-b66e-464e-abc0-897584d19657/image.png" alt="AMD"></p>
<p>AMD는 브라우저 환경을 우선적으로 고려해 설계된 JavaScript 모듈 시스템입니다. 당시 CJS 커뮤니티에서도 브라우저에 적합한 모듈 형식을 논의했지만, 요구사항과 접근 방식의 차이로 합의에 이르지 못했습니다. 이후 브라우저 중심의 해결책을 제시하려는 그룹이 분리되어 AMD를 제안했고, <strong>비동기 로딩</strong>을 핵심 목표로 삼았습니다.</p>
<p>브라우저에서 모듈을 동기적으로 가져오면 네트워크 요청이 완료될 때까지 메인 스레드가 막혀 페이지가 멈춘 것처럼 보일 수 있습니다. 따라서 AMD는 모듈을 비동기적으로 다운로드하고, 모든 의존성이 준비된 시점에 모듈 코드를 실행하는 방식을 채택했습니다.</p>
<p>AMD 모듈은 <code>define()</code> 함수로 정의합니다. <code>define</code>은 의존 모듈 배열과 팩토리 함수를 인자로 받고, 의존 모듈을 비동기적으로 로드한 뒤 준비가 끝나면 팩토리 함수를 실행합니다.</p>
<pre><code class="language-jsx">// define 모듈 정의 (AMD)
define([&#39;./util&#39;, &#39;jquery&#39;], function(util, $) {
  // 의존 모듈 util.js와 jQuery를 모두 불러온 뒤 이 함수 실행
  function showValue(x) {
    $(&#39;#result&#39;).text(util.compute(x));
  }
  return { showValue };
});

// require로 모듈 사용 (AMD)
require([&#39;./myModule&#39;], function(myModule) {
  myModule.showValue(42);
});
</code></pre>
<p>AMD는 비동기 로딩을 통해 페이지 로딩을 블로킹하지 않는다는 장점이 있으며, 모듈 단위의 스코프를 제공해 전역 네임스페이스 오염을 줄일 수 있습니다. 다만 AMD는 사양에 해당하므로 실제 프로젝트에서는 <a href="https://requirejs.org/">RequireJS</a> 같은 구현체를 사용해야 합니다.</p>
<h3 id="umd-universal-module-definition">UMD (Universal Module Definition)</h3>
<p>자바스크립트 생태계가 발전하면서 하나의 라이브러리를 서버(Node)와 브라우저 모두에서 사용하려는 수요가 늘었습니다. 그러나 2010년 전후로 많은 라이브러리는 브라우저용(AMD) 빌드와 Node용(CJS) 빌드를 별도로 제공하거나, 둘 중 하나만 지원하는 경우가 많았습니다. 이로 인해 배포 형태가 늘어나고 호환성 문제가 발생하자, 이를 완화하기 위한 패턴으로 UMD가 등장했습니다.</p>
<p>UMD에서는 CJS와 AMD 방식을 모두 호환할 수 있도록 조건문으로 분기하고, 동일한 팩토리 함수에서 모듈을 생성하는 방식으로 구현됩니다.</p>
<pre><code class="language-jsx">(function (root, factory) {
  if (typeof define === &#39;function&#39; &amp;&amp; define.amd) {
    // AMD 환경: define을 사용하여 모듈 정의
    define([&#39;lodash&#39;], factory);
  } else if (typeof exports === &#39;object&#39; &amp;&amp; typeof module !== &#39;undefined&#39;) {
    // CommonJS 환경: module.exports 사용
    module.exports = factory(require(&#39;lodash&#39;));
  } else {
    // 브라우저 전역 환경: window에 붙임
    root.MyLibrary = factory(root._);
  }
}(typeof globalThis !== &#39;undefined&#39; ? globalThis : this, function (_) {
  // 모듈 본체 구현부
  function doSomething() { /* ... */ }

  return { doSomething };   // AMD나 CJS에서는 반환값이 exports
}));</code></pre>
<h3 id="esm-es6-module">ESM (ES6 Module)</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/573510b7-e5fc-4d1b-9d00-923290b1fa56/image.png" alt="ES6"></p>
<p>CommonJS와 AMD는 각자의 방식으로 모듈화를 가능하게 했지만, 언어 차원의 표준 모듈 시스템이 없다는 점은 오랫동안 한계로 남아 있었습니다. 결국 2015년 ECMAScript 6(ES6)에서 ECMAScript Modules(ESM)를 표준 모듈 시스템으로 채택하게 됩니다.</p>
<p>ESM은 <code>import</code>와 <code>export</code> 문법을 통해 모듈 경계를 파일 단위로 명확히 구분합니다. 필요한 값을 <code>export</code>로 내보내고, 다른 모듈의 내용을 <code>import</code>로 가져오는 방식입니다.</p>
<pre><code class="language-jsx">// math.mjs (ES 모듈 정의)
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
export default add;            // 기본(Default) 내보내기

// main.mjs (ES 모듈 사용)
import myAdd, { multiply as mul } from &#39;./math.mjs&#39;;
console.log(myAdd(2, 3));      
console.log(mul(2, 3));        </code></pre>
<p>ESM은 <code>import</code>와  <code>export</code> 기반의 간결한 문법으로 모듈 경계를 명확히 합니다. 정적 <code>import</code>는 모듈 의존성을 선언적으로 표현해 로더가 미리 의존성 그래프를 구성할 수 있고, <code>import()</code>를 통해 필요 시점에 비동기적으로 모듈을 로드하는 방식도 지원합니다. 또한 CommonJS처럼 값을 복사해 가져오는 방식이 아니라 <strong>live binding</strong>으로 내보낸 값의 바인딩을 참조하기 때문에, 순환 참조 상황에서도 더 예측 가능한 동작을 보이며, 정적인 모듈 구조 덕분에 번들러의 정적 분석과 트리 쉐이킹 같은 최적화가 쉬워졌습니다.</p>
<h2 id="번들러의-필요성">번들러의 필요성</h2>
<p>모듈 시스템이 확산되면서 코드를 <strong>모듈 단위로 분리해 개발하는 방식</strong>이 표준이 되었습니다. 하지만 브라우저 환경은 이러한 모듈 방식을 호환성 문제로 실행하기 어렵거나, 많은 모듈 파일을 그대로 내려받기에도 부담이 있었습니다. 번들러는 이러한 제약을 해결해 모듈 기반 코드를 실행 가능한 형태로 제공하기 위해 필요해졌습니다.</p>
<h3 id="브라우저-호환성-및-모듈-해석의-한계">브라우저 호환성 및 모듈 해석의 한계</h3>
<p>초기 브라우저는 <code>require</code>나 <code>import</code> 같은 모듈 문법을 자체적으로 해석할 수 없었습니다. 이후 ESM을 지원하는 브라우저가 늘어나면서 <code>&lt;script type=&quot;module&quot;&gt;</code> 같은 방식으로 모듈을 실행할 수 있게 됐지만, 구형 브라우저 지원과 CJS 기반 npm 패키지 호환을 위해서는 여전히 변환과 번들링 과정이 필요했습니다.</p>
<h3 id="네트워크-효율성-문제">네트워크 효율성 문제</h3>
<p>HTTP/1.1에서는 모듈 파일을 많이 만들수록 개별 요청이 늘어나고, 그만큼 로딩 비용이 커지기 쉬웠습니다.</p>
<ul>
<li>요청마다 연결 설정 비용(DNS/TCP/TLS)이 발생해 오버헤드가 누적됩니다.</li>
<li>브라우저의 동시 연결 제한 때문에 요청이 대기하며 워터폴 현상이 생깁니다.</li>
</ul>
<h2 id="browserify의-탄생">Browserify의 탄생</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/afd829bd-0a9e-4599-a781-682a7b362152/image.png" alt="browserify"></p>
<p>자바스크립트 생태계에 <strong>CJS, AMD 등의 모듈 시스템</strong>이 도입됨에 따라, 코드를 모듈 단위로 분할하여 관리하고자 하는 요구가 증대되었습니다. 그러나 위에서 말했듯이 브라우저 환경은 이를 기본적으로 지원하지 않았고, 모듈 파일을 그대로 다수 로드하는 방식도 네트워크 측면에서 비효율적이었습니다.</p>
<p>이 문제를 해결하기 위해 <strong>Browserify</strong>가 등장했습니다. Browserify는 의존성 그래프를 따라 모듈을 묶어 <strong>브라우저에서 CommonJS 모듈을 실행 가능하게 만들고</strong>, 동시에 <strong>요청 횟수를 줄여 로딩 비용을 완화</strong>했습니다.</p>
<p>결과적으로 초기 번들러는 모듈 기반 코드를 브라우저에서 실행 가능하게 만들기 위해 <strong>파일 병합과 호환성 확보에 집중한 도구</strong>였습니다.</p>
<h2 id="통합-빌드-시스템-webpack">통합 빌드 시스템 Webpack</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/023761ef-b4ad-4198-bdaa-02ef49465884/image.png" alt="webpack"></p>
<p>SPA와 컴포넌트 기반 프레임워크의 확산으로 프론트엔드 코드는 자바스크립트만으로 구성되지 않게 되었습니다. CSS, 이미지, 폰트 같은 정적 리소스도 컴포넌트와 함께 관리 및 배포해야 했고, 번들러는 자바스크립트 병합을 넘어 빌드 전반을 다뤄야 했습니다.</p>
<p>이러한 요구를 반영해 <strong>Webpack</strong>은 번들러의 역할을 에셋 처리와 변환 파이프라인까지 넓혔습니다.</p>
<ul>
<li><strong>에셋 관리의 일원화:</strong> CSS/이미지 등도 모듈처럼 <code>import</code>하여 처리할 수 있게 했습니다.</li>
<li><strong>트랜스파일링 통합:</strong> ES6+와 TypeScript 변환을 빌드 흐름에 포함했습니다.</li>
</ul>
<p>결과적으로 번들러는 단순 병합 도구에서 통합 빌드 시스템으로 발전했습니다.</p>
<h2 id="rollup과-parcel">Rollup과 Parcel</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/acbc1b7c-5da0-4f0a-afd6-ac5a1e3f023e/image.png" alt="rollup"></p>
<p>번들러가 처리하는 범위가 넓어지면서 번들 크기가 커지고 초기 로딩 비용이 증가하는 문제가 나타났습니다. 이에 따라 번들러가 <strong>커진 번들을 어떻게 줄이고 빠르게 로드할 것인가</strong>가 중요해졌습니다.</p>
<p><strong>Rollup</strong></p>
<p>ES Modules 표준을 따르며, 불필요한 코드를 제거하여 더 작고 효율적인 결과물을 만드는 데 집중했습니다.</p>
<ul>
<li><strong>트리 쉐이킹(Tree-shaking):</strong> 정적 분석으로 사용되지 않는 코드를 제거해 번들 크기를 줄입니다.</li>
<li><strong>스코프 호이스팅(Scope Hoisting):</strong> 여러 모듈을 별도의 함수로 감싸지 않고 하나의 스코프로 평탄화하여 실행 속도를 높입니다.</li>
</ul>
<p><strong>Parcel</strong></p>
<p>Webpack의 복잡한 설정에 지친 개발자들을 위해, 별도의 설정 없이 바로 사용할 수 있는 편의성을 제공했습니다.</p>
<ul>
<li><strong>제로 컨피그레이션(Zero Configuration):</strong> 설정 파일 없이 진입점 파일만 지정하면, 필요한 변환 도구를 자동으로 설치하고 번들링합니다.</li>
<li><strong>멀티 코어 캐싱:</strong> 단일 스레드로 동작하던 기존 도구와 달리, 멀티 코어 프로세싱을 적극 활용해 빌드 속도를 개선했습니다.</li>
</ul>
<p>결과적으로 이 시점부터 번들러는 단순한 빌드 도구를 넘어, 성능 최적화와 개발 생산성을 동시에 높이는 방향으로 발전했습니다.</p>
<h2 id="새로운-패러다임-vite">새로운 패러다임 Vite</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/f15abe21-fb2f-4725-a7b8-d7c823259c4e/image.png" alt="vite"></p>
<p>번들러의 기능은 향상되었지만, 프로젝트 규모가 커지고 설정이 복잡해지면서 <strong>빌드 및 리빌드 시간이 증가</strong>하는 문제가 나타났습니다. 이는 개발 과정의 피드백 루프를 느리게 만들어 개발 생산성에 직접적인 영향을 미쳤습니다.</p>
<p>이러한 배경에서 <strong>Vite</strong>는 DX와 속도를 최우선으로 두고, 기존 번들러 중심 개발 흐름과는 다른 접근을 제시했습니다.</p>
<ul>
<li><strong>Native ESM 활용:</strong> 변경이 발생해도 전체 번들을 다시 만들지 않고, 브라우저가 모듈을 불러오는 방식 그대로 <strong>필요한 파일만</strong> 갱신합니다.</li>
<li><strong>esbuild 기반 사전 번들링:</strong> <code>node_modules</code>처럼 변경이 적은 의존성을 esbuild로 미리 처리해 초기 구동과 의존성 해석 비용을 줄입니다.</li>
</ul>
<blockquote>
<p><strong>참고</strong>
<a href="https://blog.sangwook.dev/posts/no-one-asked-library-bundler-01-concept/">https://blog.sangwook.dev/posts/no-one-asked-library-bundler-01-concept/</a>
<a href="https://wormwlrm.github.io/2020/08/12/History-of-JavaScript-Modules-and-Bundlers.html">https://wormwlrm.github.io/2020/08/12/History-of-JavaScript-Modules-and-Bundlers.html</a>
<a href="https://deemmun.tistory.com/86">https://deemmun.tistory.com/86</a>
<a href="https://dev.to/marcogrcr/nodejs-a-brief-history-of-cjs-bundlers-and-esm-2nlb">https://dev.to/marcogrcr/nodejs-a-brief-history-of-cjs-bundlers-and-esm-2nlb</a>
<a href="https://deemmun.tistory.com/87">https://deemmun.tistory.com/87</a>
<a href="https://toss.tech/article/commonjs-esm-exports-field">https://toss.tech/article/commonjs-esm-exports-field</a>
<a href="https://frontend-fundamentals.com/bundling/">https://frontend-fundamentals.com/bundling/</a>
<a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Modules">https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Modules</a>
<a href="https://d2.naver.com/helloworld/12864">https://d2.naver.com/helloworld/12864</a>
<a href="https://v8.dev/features/modules">https://v8.dev/features/modules</a>
<a href="https://bundlers.tooling.report/">https://bundlers.tooling.report/</a>
<a href="http://codilime.com/blog/history-of-javascript-module-systems/">http://codilime.com/blog/history-of-javascript-module-systems/</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[주니어 개발자의 2025년 회고]]></title>
            <link>https://velog.io/@jong-kyung/%EC%A3%BC%EB%8B%88%EC%96%B4-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-2025%EB%85%84-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@jong-kyung/%EC%A3%BC%EB%8B%88%EC%96%B4-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-2025%EB%85%84-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 28 Dec 2025 06:54:00 GMT</pubDate>
            <description><![CDATA[<p>올해로 나는 어느덧 2년 차 개발자가 되었다.
취준생 시절 바라보았던 2, 3년 차 선배들은 연차도 높고 대단해 보여서, 나도 시간이 지나면 자연스럽게 그런 멋진 선배 개발자가 되어 있을 줄 알았다.
하지만 나는 여전히 성장에 목마른 주니어 개발자일 뿐이다. 그래도 올 한 해, 멋진 선배가 되기 위해 나름의 치열한 노력을 해왔다. 😅</p>
<h2 id="문제를-해결하는-사람에서-문제를-정의하는-사람">문제를 해결하는 사람에서 문제를 정의하는 사람</h2>
<p>가장 큰 노력은 <strong>주어진 문제를 단순히 해결하는 사람을 넘어, 문제를 먼저 정의하는 사람이 되기 위해 노력했다는 점이다.</strong>
2025년은 Gen AI와 코딩 에이전트 도구들이 발전한 해였다. 뉴스나 링크드인에서는 MS 같은 해외 빅테크 기업의 인력 감축 사례를 들며 개발자의 입지가 좁아질 것이라는 우려 섞인 목소리가 많았다. 이에 선배들도 입을 모아 &quot;앞으로는 회사에서 정의해준 문제를 푸는 것에 그치지 말고, 스스로 문제를 정의할 줄 아는 개발자가 되어야 한다&quot;라고 조언해 주었다.
아직은 스스로 문제를 정의하는 것이 어색하지만, 이 조언을 새겨듣고 문제를 먼저 정의해보기 위해 노력했다.</p>
<h3 id="fsd-아키텍처-도입-그리고-아직도-해결하지-못-한-문제">FSD 아키텍처 도입, 그리고 아직도 해결하지 못 한 문제</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/80897e0b-7e70-41af-9791-31d8da553e54/image.png" alt="어떻게든 굴러가고 있다"></p>
<p>입사 후 약 1년간 우리 회사는 전형적인 <strong>Component 패턴</strong>을 사용했다. 하지만 모바일 서비스를 시작하는 등 서비스 규모가 커지자 기존 패턴으로는 프로젝트 복잡도를 감당하기 어려워졌고, 필요한 컴포넌트를 찾는 일조차 힘들어졌다. 이에 우리는 1월부터 점진적으로 FSD를 도입했다.</p>
<p>우리는 PWA를 채택하여 모바일과 웹을 모노레포로 분리하지 않고 하나의 프로젝트로 관리하고 있었기에, 우리만의 특별한 장치가 필요했다. 그래서 Layers와 Slice 사이에 <code>Platform</code>을 두어 모바일용, 웹용, 그리고 공통 기능을 나누었다. 초기에는 효율적이고 좋아 보였으나, 신규 기능 개발과 유지보수를 거듭하다 보니 몇 가지 단점이 드러났다.</p>
<p>첫째, <code>Platform</code>이라는 중간 장치 때문에 폴더의 깊이(Depth)가 지나치게 깊어졌다. 이는 초기 기능을 구상할 때 복잡성을 증가시켰다.</p>
<p>둘째, 일부 레이어의 구분이 명확하지 않아 &#39;계륵&#39; 같다는 생각이 들었다. 특히 Three.js를 사용하는 우리 서비스 특성상, 어디까지가 Entity이고 Feature인지 나누기 애매하거나, FSD 규칙(동일 레이어 간 참조 금지) 때문에 구현이 난해한 경우가 많았다.</p>
<p>이러한 문제들을 끊임없이 고민했지만, 아직 완벽한 해결책은 찾지 못했다. 폴더 구조 규칙이 완전히 정립되진 않았지만, 서비스 운영에는 지장이 없으니 말 그대로 &#39;어떻게든 굴러가고 있는&#39; 셈이다. 2026년에는 더 명확한 규칙을 세워 프로젝트의 복잡도를 확실히 낮춰보고 싶다.</p>
<h3 id="wasm-도전-결과는-오버-엔지니어링">WASM 도전, 결과는 오버 엔지니어링</h3>
<p>2025년 2, 3분기에는 정말 재밌는 경험을 했다. 마치 로봇 청소기처럼 우리 로봇의 탐색 경로를 설정하여 해당 영역을 움직이게 하는 기능을 개발해야 했다. 하지만 이 기능에는 문제가 있었다. 로봇의 탐색 경로를 표시하기 위해 네트워크 통신이 불필요하게 많이 발생한다는 점이었다.</p>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/372f55f8-7cf4-4014-8b92-5f7157f06fba/image.png" alt="프로세스 흐름"></p>
<p>이에 ROS에서 사용 중인 경로 생성 로직을 프론트엔드로 옮겨 불필요한 통신을 줄여보고자 했다. 하지만 WASM을 활용해 로직을 이식했음에도 크게 두 가지 문제가 발생했다. </p>
<p>첫째, WASM을 사용했음에도 브라우저가 버거워하며 멈추는 현상이 빈번했다. </p>
<p>둘째, 경로 생성을 위해 프로젝트에 추가적인 C++ 세팅이 필요했고, 이로 인해 CI/CD 파이프라인 시간이 오히려 증가할 것으로 예상되었다.</p>
<p>결국 R&amp;D 단계에서 멈추고 서비스화하지는 못했지만, &#39;오버 엔지니어링&#39;에 대해 배우고 교훈을 얻은 의미 있는 도전이었다.</p>
<h2 id="멘토링-그리고-커뮤니티">멘토링, 그리고 커뮤니티</h2>
<p>회사 밖에서도 정말 감사한 경험을 많이 할 수 있었다.</p>
<h3 id="sesac-후배들과의-고민-상담">SeSAC 후배들과의 고민 상담</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/25c3e3a0-ff95-4e91-bcde-ecee9578950b/image.png" alt="매칭데이"></p>
<p>감사하게도 내가 수료한 청년취업사관학교(SeSAC) 6월 매칭데이의 멘토로 참여하여, 수료 후 취업을 준비하는 후배들의 고민을 들어줄 기회가 생겼다. </p>
<p>취준생 시절의 나 또한 늘 고민이 많았다. 2024년부터 본격적인 취업 준비를 시작했는데, 개발자 공급이 늘어나며 취업 문이 좁아지는 것을 체감했다. 커리어 코치님이 계셨지만, 개발자 지망생으로서 겪는 고충은 좀처럼 해결되지 않아 괴로운 순간도 많았다. </p>
<p>후배들 대부분은 줄어든 신입 채용 공고와 AI 발전에 대한 두려움으로 괴로워하고 있었다. 사실 나도 저연차라 대단한 조언을 해줄 순 없었지만, 내가 마음이 꺾였을 때 어떤 노력을 했는지 경험을 공유하며 그들의 심리적 부담을 조금이라도 덜어주려 노력했다. </p>
<p>2026년에는 채용 시장이 개선되어 모두에게 희망이 가득한 한 해가 되기를 바란다.</p>
<h3 id="it-커뮤니티-그로스로그">IT 커뮤니티, 그로스로그</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/809d86b1-75cf-44cb-a7f5-ea94b299d1a8/image.png" alt="그로스로그"></p>
<p>2024년 방송대 입학과 함께 시작한 IT 커뮤니티 &#39;그로스로그&#39; 에서 함게 하고 있다. 특히 올해는 커뮤니티 멤버들이 많은 추억을 쌓을 수 있도록 노력한 한 해였다. 우리 커뮤니티는 1년 만에 100명이 넘는 오프라인 커뮤니티로 성장했다. 회원들에게 좋은 경험을 제공하기 위해 운영진들과 가족보다 더 자주 만나 회의하고 준비했다. 부족한 점도 있었겠지만, 흔치 않은 오프라인 모임으로서 회원들이 이곳을 &#39;놀이터&#39;처럼 편안하게 느낄 수 있도록 리더들과 함께 계속 발전시켜 나가고 싶다.</p>
<h2 id="2025년을-보내며-2026년에는">2025년을 보내며, 2026년에는</h2>
<p>이번 회고에는 다 담지 못했지만, 꿈에 그리던 FEConf와 TeoConf에 참여해 선배들의 노하우를 들을 수 있어 즐거웠고, 다양한 경험을 하며 개발자이기 이전에 사회인으로서 한층 성숙해질 수 있었던 감사한 한 해였다. 이제 2025년을 떠나보내며, 2026년에는 크게 두 가지 목표를 꼭 이루고 싶다.</p>
<h3 id="오픈-소스-컨트리뷰터">오픈 소스 컨트리뷰터</h3>
<p>개발자를 꿈꾸면서부터 &#39;오픈 소스 기여&#39;는 늘 마음속에 품은 목표였다. 하지만 시작이 어려워 섣불리 시도하지 못했다. 이번에 인제 님이 운영하시는 <a href="https://medium.com/opensource-contributors/%EB%AA%A8%EC%A7%91%EC%A4%91-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EA%B8%B0%EC%97%AC-%EB%AA%A8%EC%9E%84-10%EA%B8%B0-%EC%B0%B8%EA%B0%80%EC%9E%90%EB%A5%BC-%EB%AA%A8%EC%A7%91%ED%95%A9%EB%8B%88%EB%8B%A4-2026-01-%EC%A7%84%ED%96%89-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%ED%82%A4%EB%A7%81-%EA%B5%BF%EC%A6%88-%EC%84%A0%EB%AC%BC%EA%B9%8C%EC%A7%80-e95a8e528056">오픈 소스 기여 모임 10기</a>에 참여하게 된 만큼, 커뮤니티의 힘을 빌려 첫 PR에 도전해 보고 싶다.</p>
<h3 id="같이-일하고-싶은-동료">같이 일하고 싶은 동료</h3>
<p>작년부터 항상 품어온 목표다. 코드를 적절히 추상화하여 동료들이 쉽게 이해할 수 있도록 작성하고, 능숙한 커뮤니케이션으로 프로덕트의 발전에 기여하는 동료가 되고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 테스트 전략]]></title>
            <link>https://velog.io/@jong-kyung/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A0%84%EB%9E%B5</link>
            <guid>https://velog.io/@jong-kyung/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A0%84%EB%9E%B5</guid>
            <pubDate>Sat, 11 Oct 2025 04:36:20 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jong-kyung/post/02fe4420-bdc4-462d-b102-ef1aeea78a89/image.png" alt="테스트 종류"></p>
<p>프론트엔드 테스트는 네 가지로 구분되며 각기 다른 중요도를 갖습니다. </p>
<ul>
<li><strong>End to End</strong> : 사용자처럼 행동하며, 앱을 클릭하고 올바르게 작동하는지 확인합니다.</li>
<li><strong>Integration (통합)</strong> : 여러 장치가 함께 상호작용하여 잘 동작하는지 확인합니다.</li>
<li><strong>Unit (유닛)</strong> : 기능들이 각각 독립적으로 잘 동작하는지 확인합니다.</li>
<li><strong>Static (정적)</strong> : 코드를 작성할 때 오타와 타입 에러를 확인합니다.</li>
</ul>
<p>위 그림의 트로피가 보여주는 이러한 테스트 형식의 크기는 애플리케이션을 테스트할 때 얼마나 집중해야 하는지를 나타냅니다. 일반적으로 코드 작성 단계에서는 정적 분석을 폭넓게 활용하고, 핵심 로직에는 단위 테스트를 작성하며, 주요 기능 흐름은 통합 테스트로 검증하고, 최종 사용자 시나리오는 E2E 테스트로 최소한의 핵심 경로를 점검하는 전략을 취하는게 일반적입니다.</p>
<h2 id="정적-테스트-static-test">정적 테스트 (Static Test)</h2>
<p>정적 테스트는 <strong>소스코드를 실행하지 않고 정적으로 분석</strong>하여 오류를 찾는 테스트 기법입니다. 즉 개발자가 코드를 작성하는 과정에서 오타 혹은 타입 에러를 잡아내어 실행 전에 버그를 예방합니다. 대표적인 도구로 <strong><code>ESLint</code></strong>와 <strong><code>Typescript 컴파일러</code></strong>가 있습니다.</p>
<pre><code class="language-tsx">type User = { name: string; age: number };

const Greeting = ({ user }: User) =&gt; {
  // 오류: user.age는 number 타입인데 함수처럼 호출되고 있습니다.
  return &lt;div&gt;Hello, {user.name}. You are {user.age()} years old.&lt;/div&gt;;
};</code></pre>
<h2 id="단위-테스트-unit-test">단위 테스트 (Unit Test)</h2>
<p>유닛 테스트는 가장 작은 단위의 테스트로, 함수 혹은 컴포넌트를 외부 의존성 없이 검증하는 테스트입니다.</p>
<p>각 기능을 개별 실행하여 예상한 결과가 나오는지 확인하는 테스트로, 대표적인 도구로 <strong><code>Jest</code></strong>, <strong><code>Vitest</code></strong>등이 있습니다.</p>
<pre><code class="language-tsx">// Hello 컴포넌트 (유닛 테스트 대상)
type HelloProps = { name: string };
const Hello = ({ name }: HelloProps) =&gt; &lt;h1&gt;Hello, {name}!&lt;/h1&gt;;

// Hello 컴포넌트에 대한 유닛 테스트 (Jest + Testing Library)
import { render, screen } from &quot;@testing-library/react&quot;;
import Hello from &quot;./Hello&quot;;

test(&quot;Hello 컴포넌트는 전달된 이름을 화면에 표시한다&quot;, () =&gt; {
  render(&lt;Hello name=&quot;Alice&quot; /&gt;);
  expect(screen.getByText(&quot;Hello, Alice!&quot;)).toBeInTheDocument();
});
</code></pre>
<h2 id="통합-테스트-integration-test">통합 테스트 (Integration Test)</h2>
<p>통합 테스트는 애플리케이션의 여러 구성 요소나 모듈이 <strong>조화롭게 상호작용하며 동작하는지</strong> 확인하는 테스트입니다. 개별 기능이 아닌 <strong>전체 기능의 동작</strong>에 초점을 두기 때문에, 사용자가 애플리케이션을 문제없이 사용할 수 있는지를 확인하는 데 중요합니다. 이러한 통합 테스트를 작성할 때는 주로 <strong><code>React Testing Library</code></strong>를 활용하며, 이를 통해 실제 사용자 관점에서 컴포넌트들을 렌더링하고 이벤트를 일으켜 봄으로써 구현 세부사항이 아닌 기능적 결과를 검증합니다</p>
<pre><code class="language-tsx">// 테마 컨텍스트와 이를 소비하는 컴포넌트
const ThemeContext = React.createContext(&quot;light&quot;);

const ShowTheme = () =&gt; {
  const theme = React.useContext(ThemeContext);
  return &lt;div&gt;Theme: {theme}&lt;/div&gt;;
};

// 통합 테스트 (컨텍스트 제공자 + 소비자 통합 동작 검증)
import { render, screen } from &quot;@testing-library/react&quot;;

test(&quot;ShowTheme 컴포넌트가 컨텍스트의 테마 값을 표시한다&quot;, () =&gt; {
  render(
    &lt;ThemeContext.Provider value=&quot;dark&quot;&gt;
      &lt;ShowTheme /&gt;
    &lt;/ThemeContext.Provider&gt;
  );
  expect(screen.getByText(&quot;Theme: dark&quot;)).toBeInTheDocument();
});
</code></pre>
<h2 id="e2e-테스트-end-to-end-test">E2E 테스트 (End to End Test)</h2>
<p>E2E(End-to-End) 테스트는 실제 브라우저 환경에서 애플리케이션을 구동하여 <strong>사용자 시나리오 전반</strong>을 처음부터 끝까지 검증하는 테스트입니다. 브라우저를 통해 사용자 동작을 단계별로 자동화한 시나리오를 실행합니다. 대표적인 도구로는 <strong><code>Cypress</code></strong> 와 <strong><code>Playwright</code></strong>가 있습니다.</p>
<pre><code class="language-tsx">test(&#39;Todo 앱 - 새 할 일 추가&#39;, async ({ page }) =&gt; {
  await page.goto(&#39;/todo&#39;);
  await page.getByRole(&#39;textbox&#39;, { name: &quot;제목을 입력해주세요.&quot; }).fill(&#39;Write tests&#39;);
  await page.keyboard.press(&#39;Enter&#39;);
  await expect(page.locator(&#39;ul.todo-list&#39;)).toContainText(&#39;Write tests&#39;);
});</code></pre>
<blockquote>
<p><strong>참고</strong>
<a href="https://www.youtube.com/watch?v=MN7Pw4mK6lU">[10분 테코톡] 헤다의 프론트엔드 테스트 종류</a>
<a href="https://velog.io/@osohyun0224/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%97%90%EC%84%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8%EA%B0%80-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80%EC%9A%94">[인턴일지] 프론트엔드 개발에서 테스트가 필요한가요?
</a><a href="https://developer-bandi.github.io/post/frontend-testing/">프론트엔드에서 테스트코드를 작성하는 방법</a>
<a href="https://blog.lemonbase.team/%EC%9A%B0%EB%A6%AC-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%ED%8C%80%EC%97%90%EB%8A%94-%EC%96%B4%EB%96%A4-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A5%BC-%EC%A0%81%EC%9A%A9%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C-a3ea48207cd4">우리 프론트엔드 팀에는 어떤 테스트를 적용해야 할까?</a>
<a href="https://medium.com/miopadrex/test-2504d61409d9">Frontend Testing Strategy</a>
<a href="https://soojae.tistory.com/82">프론트엔드에서의 Static, Unit, Integration, E2E 테스트</a>
<a href="https://www.testingjavascript.com/">Learn the smart, efficient way to test any JavaScript application.</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[React와 Vue]]></title>
            <link>https://velog.io/@jong-kyung/React%EC%99%80-Vue</link>
            <guid>https://velog.io/@jong-kyung/React%EC%99%80-Vue</guid>
            <pubDate>Sat, 20 Sep 2025 12:53:14 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jong-kyung/post/e74a6bfd-fba6-40ee-a7d4-93100d0ba3aa/image.png" alt="React&amp;Vue"></p>
<h2 id="철학과-아키텍처">철학과 아키텍처</h2>
<ul>
<li><strong>React</strong>: UI 레이어에 집중합니다. 라우팅, 전역 상태, 데이터 패칭 등은 별도 라이브러리를 조합해 구성합니다. 작고 유연한 코어를 바탕으로 <strong>필요한 만큼 채택</strong>하는 방식입니다.</li>
<li><strong>Vue</strong>: 자주 쓰는 기능(라우터, 상태 관리 등)을 <strong>공식 패키지</strong>로 제공합니다. 싱글 파일 컴포넌트(SFC)와 권장 구조가 있어 <strong>일관된 구조</strong>로 시작하기 쉽습니다.</li>
</ul>
<h3 id="데이터-흐름">데이터 흐름</h3>
<ul>
<li><strong>React</strong>: 기본은 <strong>단방향 데이터 흐름</strong>과 <strong>불변성</strong>입니다. 상태 변경 시 해당 컴포넌트의 <strong>하위 트리</strong>가 재렌더링되고, 커밋 단계에서 실제 DOM을 <strong>최소 변경</strong>으로 갱신합니다. 상태 공유는 명시적 <code>props</code>/<code>context</code> 중심이라 추론이 쉽습니다.</li>
<li><strong>Vue</strong>: <strong>반응성 시스템</strong>이 의존성을 추적합니다. 바뀐 데이터와 연결된 컴포넌트만 선별적으로 갱신되며, 템플릿 컴파일러의 <strong>patch flag</strong>로 불필요 패치를 줄입니다. <code>v-model</code>로 <strong>양방향 바인딩</strong>을 간결히 제공하되, 복잡 폼은 단방향 흐름을 섞어 관리하는 것이 좋습니다.</li>
</ul>
<h2 id="컴포넌트-작성-방식">컴포넌트 작성 방식</h2>
<h3 id="react-jsx"><strong>React (JSX)</strong></h3>
<pre><code class="language-tsx">function Hello({ name }) {
  return &lt;h1&gt;Hello, {name}&lt;/h1&gt;;
}</code></pre>
<ul>
<li>모든 것이 JS(및 TS) 표현식 안에서 이뤄져 <strong>표현력</strong>이 높습니다(조건, 리스트, 함수 합성 등).</li>
<li>JSX 규칙을 익혀야 하고, <strong>마크업·로직이 한 파일</strong>에서 강하게 결합되는 편입니다.</li>
</ul>
<h3 id="vue-template"><strong>Vue (Template)</strong></h3>
<pre><code class="language-tsx">&lt;script setup&gt;
import { ref } from &#39;vue&#39;
const name = ref(&#39;World&#39;)
&lt;/script&gt;

&lt;template&gt;
  &lt;h1&gt;Hello, {{ name }}&lt;/h1&gt;
&lt;/template&gt;</code></pre>
<ul>
<li>HTML 친화적인 템플릿으로 <strong>가독성</strong>이 좋고, <code>&lt;script&gt;</code>, <code>&lt;template&gt;</code>, <code>&lt;style&gt;</code> 분리로 <strong>역할 분담</strong>이 명확합니다.</li>
<li>템플릿에는 <strong>표현식만</strong> 쓸 수 있으므로 복잡 로직은 <code>&lt;script&gt;</code>에 두는 것이 좋습니다.</li>
</ul>
<h3 id="양방향-바인딩-비교"><strong>양방향 바인딩 비교</strong></h3>
<pre><code class="language-tsx">// React
const [text, setText] = useState(&#39;&#39;)
&lt;input value={text} onChange={(e) =&gt; setText(e.target.value)} /&gt;

// Vue
&lt;script setup&gt;
import { ref } from &#39;vue&#39;
const text = ref(&#39;&#39;)
&lt;/script&gt;
&lt;template&gt;
  &lt;input v-model=&quot;text&quot; /&gt;
&lt;/template&gt;</code></pre>
<h2 id="상태-관리">상태 관리</h2>
<ul>
<li><strong>React</strong>는 로컬 상태를 Hooks(<code>useState</code>, <code>useReducer</code>, <code>useContext</code>)로 관리합니다. 전역 상태는 <strong>Redux/MobX/Zustand/Recoil</strong> 등이 사용됩니다.</li>
<li><strong>Vue</strong>는 로컬 상태를 <code>ref/reactive</code>로, 컴포넌트 간 연결은 <code>v-model/props/emits</code> 또는 <code>provide/inject</code>로 처리합니다. 전역 상태는 <strong>Pinia</strong>가 공식 권장 라이브러리입니다.</li>
</ul>
<h2 id="생명-주기">생명 주기</h2>
<ul>
<li><strong>React</strong>: <code>useEffect</code>/<code>useLayoutEffect</code>로 효과를 다룹니다. 의존성 배열 관리가 핵심이며, 개발 모드 <strong>StrictMode의 이중 호출</strong>로 부작용이 드러날 수 있습니다(의도적). 커스텀 훅으로 관심사를 분리하는 패턴이 일반적입니다.</li>
<li><strong>Vue</strong>: <code>onMounted</code>/<code>onUnmounted</code>/<code>watch</code>/<code>watchEffect</code>로 처리합니다. 반응형 참조의 <strong>의존성 자동 추적</strong>으로 로직이 간결해지는 장점이 있습니다.</li>
</ul>
<h2 id="최적화-전략">최적화 전략</h2>
<h3 id="렌더링">렌더링</h3>
<ul>
<li><strong>React</strong>: 상태 변경 시 하위 트리를 재렌더링하고, 커밋 단계에서 <strong>DOM 최소 변경</strong>을 적용합니다. 불필요 렌더는 <code>memo</code>, <code>useMemo</code>, <code>useCallback</code> 등으로 제어합니다.</li>
<li><strong>Vue</strong>: 반응성 의존성 추적으로 <strong>필요한 컴포넌트만</strong> 갱신합니다. 기본 패턴을 지키면 자동 최적화 이점을 얻기 쉽습니다.</li>
</ul>
<h3 id="ux상호작용">UX/상호작용</h3>
<ul>
<li><strong>React</strong> :  <code>startTransition</code>, <code>useDeferredValue</code>로 입력·스크롤을 우선 처리해 <strong>체감 성능</strong>을 높일 수 있습니다.</li>
<li><strong>Vue</strong> : <strong>부분/지연 하이드레이션</strong> 등으로 초기 비용을 상황에 맞게 지연하여 초기 상호작용성을 개선할 수 있습니다.</li>
</ul>
<h3 id="ssr하이드레이션">SSR/하이드레이션</h3>
<ul>
<li><strong>React</strong>: <strong>Streaming SSR</strong>, <code>hydrateRoot</code>, SSR용 <code>&lt;Suspense&gt;</code>를 통해 <strong>먼저 보여주고 점진 완성</strong>하는 패턴에 강합니다.</li>
<li><strong>Vue</strong>: 템플릿 컴파일 최적화와 Nuxt(3)를 통한 <strong>부분·지연 하이드레이션</strong>으로 초기 비용을 정교하게 관리할 수 있습니다.</li>
</ul>
<blockquote>
<p><a href="https://react.dev/">React Docs</a>
<a href="https://ko.vuejs.org/guide/introduction.html">Vue Docs</a>
<a href="https://medium.com/@ajonesb/vue-3-vs-react-18-a-comprehensive-comparison-from-a-developers-perspective-b351df59715a">Vue 3 vs React 18: A Comprehensive Comparison from a Developer’s Perspective</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Web Assembly 개요]]></title>
            <link>https://velog.io/@jong-kyung/Web-Assembly-%EA%B0%9C%EC%9A%94</link>
            <guid>https://velog.io/@jong-kyung/Web-Assembly-%EA%B0%9C%EC%9A%94</guid>
            <pubDate>Fri, 08 Aug 2025 10:26:11 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/ac1bffac-fe74-4fca-a7cd-3f17ed811433/image.png" alt="wasm"></p>
<p>WebAssembly(WASM)는 웹 브라우저에서 실행할 수 있는 <strong>이진 포맷의 저수준 코드</strong>입니다. 이는 JavaScript의 성능 한계를 보완하고 네이티브 수준에 가까운 퍼포먼스를 제공하기 위해 설계된 웹 표준 기술로, 2017년 W3C 권고안으로 채택된 이후 주요 브라우저에서 폭넓게 지원되고 있습니다. 특히 게임, 이미지 및 영상 처리, 실시간 데이터 분석 등 고성능이 요구되는 웹 애플리케이션에서 각광받고 있습니다.</p>
<h2 id="작동-원리">작동 원리</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/df2ce7e2-a3f9-4d77-bdd0-323071df9230/image.png" alt="WASM 작동 원리"></p>
<ul>
<li>WebAssembly는 <strong>스택 기반 가상 머신</strong> 모델을 채택하고 있으며, 개발자는 직접 WASM 코드를 작성하기보다는 C/C++, Rust 등의 고수준 언어로 작성한 소스를 컴파일합니다.</li>
<li>생성된 <code>.wasm</code> 바이너리 파일은 브라우저에서 로드하고 실행됩니다.</li>
<li>WASM은 <strong>스트리밍 컴파일</strong>을 지원하므로, 다운로드 도중에도 컴파일이 가능해 로딩 지연을 최소화합니다.</li>
<li>코드 구조는 함수 단위로 나뉘며, 명령어는 스택을 통해 효율적으로 처리됩니다. 이는 빠른 실행뿐 아니라 보안성에도 기여합니다.</li>
</ul>
<h2 id="javascript와의-관계">JavaScript와의 관계</h2>
<ul>
<li>WebAssembly는 JavaScript의 <strong>성능</strong> <strong>보완</strong>을 위해 설계되었습니다.</li>
<li>JavaScript 코드에서 <code>&lt;script&gt;</code> 태그나 WebAssembly API를 이용해 <code>.wasm</code> 모듈을 로드하고, 이를 통해 함수를 호출하거나 메모리(ArrayBuffer)를 공유할 수 있습니다.</li>
<li>Web API는 WASM에서 직접 사용할 수 없으며, 반드시 JavaScript를 통해 우회해야 합니다.</li>
<li>이처럼 두 기술은 역할 분담이 가능하며, 계산 집약적인 로직은 WASM에, UI 및 DOM 조작은 JS에 맡기는 하이브리드 구조가 주로 사용됩니다.</li>
</ul>
<h2 id="브라우저에서의-실행-과정-차이">브라우저에서의 실행 과정 차이</h2>
<h3 id="javascript">Javascript</h3>
<ul>
<li>JS 파일을 다운로드</li>
<li>파서가 코드를 읽어서 AST 생성</li>
<li>AST를 바이트코드로 변환</li>
<li>인터프리터가 바이트코드 실행</li>
<li>자주 사용되는 코드는 JIT 컴파일러가 기계어로 변환</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/91069cad-c9b3-4261-8958-fdd8c5e85433/image.png" alt="js"></p>
<h3 id="wasm">WASM</h3>
<ul>
<li>이미 컴파일된 바이너리(.wasm) 파일을 다운로드</li>
<li>바이너리 형식 유효성 검사</li>
<li>디코딩</li>
<li>거의 기계어에 가까운 형태로 즉시 컴파일</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/3bc0ac75-5169-4613-8a78-0f81ed436389/image.png" alt="wasm"></p>
<h2 id="성능상의-장점과-제약">성능상의 장점과 제약</h2>
<h3 id="장점">장점</h3>
<ul>
<li>바이트코드 형태의 이진 포맷으로 인해 <strong>파싱과 컴파일 속도</strong>가 빠릅니다.</li>
<li>인터프리팅 단계가 없어 실행 속도가 빠릅니다.</li>
<li>파일 용량이 작고, 이미 컴파일된 바이너리(<code>.wasm</code>) 파일 덕분에 초기 로딩 시간이 짧습니다.</li>
</ul>
<h3 id="제약">제약</h3>
<ul>
<li>WASM은 GC(Garbage Collection)를 자체적으로 지원하지 않기 때문에, Java나 C# 같은 언어는 별도의 런타임 포함이 필요합니다.</li>
<li>파일 시스템이나 OS 레벨 리소스 접근은 불가능하며, 서버 측에서는 WASI 등의 별도 표준이 요구됩니다.</li>
<li>디버깅 및 핫 리로드 기능 등을 지원하지 않습니다.</li>
</ul>
<h2 id="주요-지원-언어-및-특징">주요 지원 언어 및 특징</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/720000b6-a4a4-43d0-92e3-c3073622c29f/image.png" alt="언어 특징"></p>
<h3 id="cc">C/C++</h3>
<ul>
<li>WebAssembly에 가장 적합한 언어로, 직접 메모리 제어가 가능하고 최적화에 유리합니다.</li>
<li>다양한 네이티브 라이브러리를 웹 환경에서 재사용할 수 있어 생산성이 높습니다.</li>
</ul>
<h3 id="rust">Rust</h3>
<ul>
<li>메모리 안전성과 성능의 균형을 갖춘 현대적 시스템 언어로, WASM의 주요 타겟 중 하나입니다.</li>
<li>암호화, 이미지 처리, 스마트 컨트랙트 등 고신뢰성 요구 영역에서 주로 활용됩니다.</li>
</ul>
<h3 id="assemblyscript">AssemblyScript</h3>
<ul>
<li>TypeScript와 유사한 문법을 갖춘 언어로, JavaScript 개발자가 WASM을 쉽게 도입할 수 있도록 합니다.</li>
<li>진입 장벽이 낮고, 간단한 성능 향상 모듈 제작에 적합하지만, 생태계나 성능 면에서는 C++/Rust에 비해 한계가 있습니다.</li>
</ul>
<h2 id="메모리-및-보안-모델">메모리 및 보안 모델</h2>
<h3 id="메모리-모델">메모리 모델</h3>
<ul>
<li>WebAssembly는 <strong>선형 메모리(linear memory)</strong> 구조를 따르며, 바이트 배열 기반의 독립된 주소 공간을 사용합니다.</li>
<li>메모리 접근 시 경계 검사가 반드시 수행되며, 허용되지 않은 접근은 즉시 트랩(trap)으로 처리되어 보안사고를 방지합니다.</li>
</ul>
<h3 id="보안-모델">보안 모델</h3>
<ul>
<li>WASM은 기본적으로 <strong>샌드박스 환경</strong>에서 실행되며, 호스트 시스템으로부터 철저히 격리됩니다.</li>
<li>함수 호출은 미리 정의된 시그니처에 따라 제한되며, 스택 버퍼 오버플로우나 ROP(Return-Oriented Programming)와 같은 공격에 취약하지 않습니다.</li>
<li>실행 도중 오류가 발생하면 호스트에 예외를 전달하고, 해당 모듈 실행을 안전하게 중단합니다.</li>
</ul>
<h2 id="결론">결론</h2>
<p>WebAssembly는 웹의 성능 한계를 뛰어넘어, 다양한 고성능 애플리케이션을 가능하게 하는 핵심 기술로 자리잡았습니다. 향후 GC, 멀티스레드, 컴포넌트 모델 등의 기능 확장을 통해 서버, IoT, 엣지 컴퓨팅 등 웹이 아닌 환경에서도 활용될 전망입니다. 웹 개발자뿐 아니라 시스템 프로그래머에게도 매력적인 기술 도구로, 전략적인 활용이 점점 중요해지고 있습니다.</p>
<blockquote>
<p><strong>참고</strong>
<a href="https://velog.io/@jeongmo511/Wasm-WebAssembly-%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80">[Wasm] WebAssembly 란 무엇인가?</a>
<a href="https://tech.hancom.com/2024-09-25-wasm/">WebAssembly(WASM) &amp; WASI 2024 : 크로스플랫폼의 미래</a>
<a href="https://developer.chrome.com/blog/hotpath-with-wasm?hl=ko">앱의 JavaScript에서 핫 패스를 WebAssembly로 대체</a>
<a href="https://web.dev/articles/wasm-av1?hl=ko">WebAssembly로 브라우저 확장</a>
<a href="https://tech.kakao.com/posts/438">FE개발자의 성장 스토리 08 : WebAssembly 개발기</a>
<a href="https://techblog.samsung.com/blog/article/19">러스트로 만나는 WebAssembly 톺아보기: 개념부터 예제까지</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바스크립트 모듈 시스템]]></title>
            <link>https://velog.io/@jong-kyung/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%AA%A8%EB%93%88-%EC%8B%9C%EC%8A%A4%ED%85%9C</link>
            <guid>https://velog.io/@jong-kyung/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%AA%A8%EB%93%88-%EC%8B%9C%EC%8A%A4%ED%85%9C</guid>
            <pubDate>Sat, 26 Jul 2025 11:53:22 GMT</pubDate>
            <description><![CDATA[<h2 id="javascript-모듈-시스템">JavaScript 모듈 시스템</h2>
<pre><code class="language-javascript">// A.js
var name = &#39;foo&#39;;
function getName() {
  return name;
}

// B.js
function sayHello() {
  alert(&#39;Hello &#39; + name); // Hello foo
}
sayHello();

// index.html
&lt;html&gt;
  &lt;script src=&quot;/src/A.js&quot; /&gt;
  &lt;script src=&quot;/src/B.js&quot; /&gt;
&lt;/html&gt;</code></pre>
<p>최초의 JavaScript는 매우 단순한 모듈 시스템만을 제공했습니다. HTML 페이지에서 JavaScript 소스 파일을 <code>&lt;script&gt;</code> 태그로 직접 로드하고, 브라우저가 이를 순차적으로 실행하는 방식이었습니다. 하지만 이 방식은 여러 JavaScript 파일이 <strong><code>하나의 전역 스코프</code></strong>를 공유하기 때문에 변수 충돌이 자주 발생하거나 로드 순서에 따라 의존성이 깨지는 문제가 빈번했습니다.</p>
<p>이러한 문제를 완화하기 위해 즉시 실행 함수(IIFE) 패턴이나 네임스페이스 객체를 활용해 변수의 범위를 격리하는 기법이 도입되었지만, 근본적인 한계를 극복하지는 못했습니다.</p>
<p>이 같은 배경에서 2008년 Google이 개발한 <a href="https://v8.dev/">V8 엔진</a>이 등장하면서 JavaScript 생태계에 큰 변화가 시작되었습니다. 특히 2009년부터는 JavaScript의 모듈 시스템을 본격적으로 표준화하려는 움직임이 나타나기 시작했습니다.</p>
<h2 id="cjs-commonjs">CJS (CommonJS)</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/a13c9885-2d07-40c4-9289-59fa6c201f47/image.png" alt="CJS"></p>
<p>2009년 Kevin Dangoor 등을 중심으로 서버 사이드 JavaScript에 적합한 모듈 표준을 만들기 위한 움직임이 시작되었고, 그 결과 CommonJS가 탄생했습니다. 초기에는 <strong>ServerJS</strong>라고 불렸으나, 서버 환경에만 국한되지 않고 범용적인 모듈 표준을 목표로 하면서 CommonJS로 개명되었습니다. 이후 CommonJS 모듈 사양은 Node.js에 채택되어 <strong>Node.js의 기본 모듈 시스템</strong>으로 자리 잡게 되었습니다.</p>
<p>CommonJS에서는 한 파일이 하나의 모듈이며, <code>module.exports</code> 또는 <code>exports</code> 객체를 통해 내보낼 값을 정의하고 다른 모듈에서는 <code>require()</code> 함수를 통해 이를 불러옵니다.</p>
<pre><code class="language-javascript">// CommonJS 모듈 정의
module.exports = foo;

// CommonJS 모듈 사용
const foo = require(&#39;./foo&#39;);</code></pre>
<p>CommonJS의 가장 큰 특징은 <strong>모듈 로딩이 동기적</strong>이라는 점입니다. 즉, <code>require()</code> 호출이 발생하면 코드의 실행을 멈추고, 즉시 해당 모듈 파일을 로드한 뒤 평가하여 그 결과를 반환합니다.</p>
<p>하지만 이 동기적 설계는 처음부터 비동기 로딩을 고려하지 않았기 때문에 브라우저 환경에서는 CommonJS 모듈 시스템을 직접 사용할 수 없다는 근본적인 한계를 갖고 있었습니다. 따라서 CommonJS는 Node.js 프로젝트에서는 효과적이었지만 브라우저 환경에서는 제한적일 수밖에 없었고, 이를 극복하기 위한 첫 번째 빌드 도구로 <a href="http://browserify.org/">Browserify</a>가 등장하게 되었습니다.</p>
<h2 id="amd-asyncronous-module-definition">AMD (Asyncronous Module Definition)</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/067e8e1a-bc63-47d2-8e29-14826e80b1ce/image.png" alt="AMD"></p>
<p>AMD는 브라우저 환경에서 사용할 수 있는 JavaScript 모듈 시스템입니다. 본래 CommonJS 진영에서 브라우저에서도 활용 가능한 JavaScript 모듈 방식을 논의했으나 합의에 이르지 못해 별도의 그룹으로 독립하여 만들어졌습니다. 초창기 AMD가 만들어진 주된 이유 역시 브라우저에서의 모듈 실행을 우선적으로 고려했기 때문입니다. 브라우저 환경에서는 필요한 모듈들을 네트워크를 통해 비동기적으로 다운로드한 후에야 사용할 수 있었기 때문입니다.</p>
<p>AMD 모듈은 <code>define()</code> 함수를 호출하여 정의합니다. <code>define</code> 함수는 모듈 ID(선택 사항), 의존 모듈의 배열, 그리고 팩토리 함수를 인자로 받습니다. 의존 모듈을 비동기적으로 불러온 뒤 모든 준비가 완료되면 팩토리 함수를 실행합니다.</p>
<pre><code class="language-javascript">// define 모듈 정의 (AMD)
define([&#39;./util&#39;, &#39;jquery&#39;], function(util, $) {
  // 의존 모듈 util.js와 jQuery를 모두 불러온 뒤 이 함수 실행
  function showValue(x) {
    $(&#39;#result&#39;).text(util.compute(x));
  }
  return { showValue };
});

// require로 모듈 사용 (AMD)
require([&#39;./myModule&#39;], function(myModule) {
  myModule.showValue(42);
});
</code></pre>
<p>CommonJS와 달리 AMD는 모듈을 <strong>비동기적으로 로드</strong>하기 때문에 브라우저가 파일을 내려받는 동안 다른 작업을 동시에 수행할 수 있으며, 이는 <strong>페이지 로딩을 블로킹하지 않는다는 장점</strong>으로 이어집니다. 또한 AMD는 <strong>전역 네임스페이스 오염을 방지하고 모듈별 스코프를 보장</strong>하여 여러 스크립트 간의 충돌 문제를 예방합니다.</p>
<p>한편 AMD는 CommonJS와 마찬가지로 모듈 시스템에 대한 사양일 뿐이므로, 실제 프로젝트에서 사용하려면 <a href="https://requirejs.org/">RequireJS</a>와 같은 별도의 구현체를 포함해야 합니다.</p>
<h2 id="umd-universal-module-definition">UMD (Universal Module Definition)</h2>
<p>자바스크립트 생태계가 발전하면서 하나의 라이브러리를 <strong>서버(Node)와 브라우저 모두에서 사용</strong>하려는 수요가 늘었습니다. 2010년 전후로 많은 라이브러리들이 브라우저용(AMD) 빌드와 Node용(CommonJS) 빌드를 별도로 제공하거나, 또는 둘 중 하나만 지원하여 사용자에게 호환성 문제라는 제약을 주었습니다. 이를 개선하기 위해 UMD가 등장하게됩니다.</p>
<p><em>Universal</em> 이라는 이름에서 알 수 있듯이, CommonJS와 AMD 방식을 모두 호환할 수 있도록 조건문으로 분기하고, 이를 팩토리 패턴으로 구현했습니다.</p>
<pre><code class="language-javascript">(function (root, factory) {
  if (typeof define === &#39;function&#39; &amp;&amp; define.amd) {
    // AMD 환경: define을 사용하여 모듈 정의
    define([&#39;lodash&#39;], factory);
  } else if (typeof exports === &#39;object&#39; &amp;&amp; typeof module !== &#39;undefined&#39;) {
    // CommonJS 환경: module.exports 사용
    module.exports = factory(require(&#39;lodash&#39;));
  } else {
    // 브라우저 전역 환경: window에 붙임
    root.MyLibrary = factory(root._);
  }
}(typeof globalThis !== &#39;undefined&#39; ? globalThis : this, function (_) {
  // 모듈 본체 구현부
  function doSomething() { /* ... */ }

  return { doSomething };   // AMD나 CJS에서는 반환값이 exports
}));</code></pre>
<h2 id="esm-es6-module">ESM (ES6 Module)</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/41f51cd3-c3b3-43cf-ae1b-a3564520ee1b/image.png" alt="ESM"></p>
<p>과거 JavaScript의 모듈화는 주로 CommonJS와 AMD가 각자의 방식으로 구현했습니다. 그러나 언어 차원의 표준 모듈 시스템이 없다는 점은 계속해서 문제로 남아 있었습니다. 결국 2015년에 발표된 ECMAScript 6(ES6)에서 공식적으로 ECMAScript Modules(ESM)를 표준 모듈 시스템으로 채택하게 됩니다.</p>
<p>ESM은 <code>import</code>와 <code>export</code>라는 간결한 키워드를 도입하여 모듈을 파일 단위로 명확히 구분합니다. 변수나 함수를 외부로 공개할 때는 선언 앞에 <code>export</code>를 붙이고, 다른 모듈의 내용을 가져올 때는 <code>import</code>를 사용하면 됩니다.</p>
<pre><code class="language-javascript">// math.mjs (ES 모듈 정의)
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
export default add;            // 기본(Default) 내보내기

// main.mjs (ES 모듈 사용)
import myAdd, { multiply as mul } from &#39;./math.mjs&#39;;
console.log(myAdd(2, 3));      
console.log(mul(2, 3));        </code></pre>
<p>ESM은 동기적 로딩과 비동기적 로딩을 모두 지원하며, 문법도 간결하고 직관적입니다. 또한 CommonJS와 달리 실제 객체나 함수의 바인딩을 직접 참조하기 때문에 순환 참조 문제도 쉽게 관리할 수 있습니다. 특히 정적인 모듈 구조 덕분에 트리 쉐이킹을 통한 최적화도 용이해졌습니다.</p>
<h2 id="정리">정리</h2>
<p>자바스크립트의 모듈 시스템은 <strong>단일 스크립트 파일 시절 → CommonJS/AMD로 이원화 → UMD로 임시 통합 → ES6 표준으로 공식 통합</strong>의 과정을 거쳤습니다. 현재는 ESM이 공식 표준으로 자리 잡았으며, <strong>프런트엔드와 백엔드 모두 ESM을 사용하는 방향</strong>으로 나아가고 있습니다. </p>
<table>
<thead>
<tr>
<th><strong>모듈 시스템</strong></th>
<th><strong>의도된 환경</strong></th>
<th><strong>로딩 방식</strong></th>
<th><strong>문법 예시</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>CommonJS (CJS)</strong></td>
<td>서버 (Node.js)</td>
<td>동기 (require)</td>
<td><code>const m = require(&#39;x&#39;)module.exports = ...</code></td>
</tr>
<tr>
<td><strong>AMD</strong></td>
<td>브라우저 (RequireJS)</td>
<td>비동기 (define)</td>
<td><code>define([&#39;dep&#39;], function(dep){ ... })</code></td>
</tr>
<tr>
<td><strong>UMD</strong></td>
<td>범용 (브라우저+Node)</td>
<td>혼합 (런타임 분기)</td>
<td>IIFE 패턴으로 AMD/CJS 분기<code>if (define.amd) define(...); else if (module.exports)...</code></td>
</tr>
<tr>
<td><strong>ESM (ES Module)</strong></td>
<td>표준 (브라우저+Node)</td>
<td>비동기 (정적 선언)</td>
<td><code>import {foo} from &#39;m.js&#39;export function foo(){}</code></td>
</tr>
</tbody></table>
<blockquote>
<p><strong>참고</strong>
<a href="https://leetrue.hashnode.dev/javascript-module-system">JavaScript Module System</a>
<a href="https://d2.naver.com/helloworld/12864">JavaScript 표준을 위한 움직임: CommonJS와 AMD</a>
<a href="https://ui.toast.com/fe-guide/ko_DEPENDENCY-MANAGE">의존성 관리</a>
<a href="https://medium.com/@hong009319/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%9D%98-%ED%91%9C%EC%A4%80-%EC%A0%95%EC%9D%98-commonjs-vs-es-modules-306e5f0a74b1">자바스크립트의 표준 정의 : CommonJS vs ES Modules</a>
<a href="https://wormwlrm.github.io/2020/08/12/History-of-JavaScript-Modules-and-Bundlers.html">JavaScript 번들러로 본 조선시대 붕당의 이해</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[다양한 로그인 인증방식]]></title>
            <link>https://velog.io/@jong-kyung/%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9D%B8%EC%A6%9D%EB%B0%A9%EC%8B%9D</link>
            <guid>https://velog.io/@jong-kyung/%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9D%B8%EC%A6%9D%EB%B0%A9%EC%8B%9D</guid>
            <pubDate>Tue, 08 Jul 2025 11:28:25 GMT</pubDate>
            <description><![CDATA[<h2 id="쿠키-인증">쿠키 인증</h2>
<p>쿠키는 <strong>클라이언트(브라우저)</strong>에 저장되는 Key-Value형태의 데이터입니다. 클라이언트는 서버에 요청을 보낼 때, 쿠키를 함께 전송하여 사용자 식별이나 로그인 상태 유지에 활용합니다. HTTP는 기본적으로 무상태(stateless)이므로, 쿠키를 통해 생성된 사용자 정보(쿠키)를 계속 전달하여 로그인 유지 등의 상태를 관리하는 것 입니다.</p>
<h3 id="인증-방식">인증 방식</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/6e67d034-3590-4b67-b2f8-96ae122c529f/image.png" alt="쿠키인증"></p>
<ol>
<li>클라이언트(브라우저)가 서버에 요청(접속)을 보냅니다.</li>
<li>서버는 클라이언트의 요청에 대한 응답을 작성할 때, 클라이언트 측에 저장하고 싶은 정보를 응답 헤더의 <code>Set-Cookie</code>에 담아 전달합니다.</li>
<li>이후 클라이언트는 요청을 보낼 때마다, 저장된 쿠키를 요청 헤더의 <code>Cookie</code> 에 담아 보냅니다.
서버는 쿠키에 담긴 정보를 바탕으로 해당 요청의 클라이언트가 누군지 식별합니다.</li>
</ol>
<h3 id="장점">장점</h3>
<ul>
<li>클라이언트가 인증 정보를 관리하므로 서버 부하가 적습니다.</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li>쿠키에 인증 정보를 포함하므로 사용자가 쿠키 값을 임의로 수정하거나 탈취 될 경우 위조될 수 있습니다.</li>
<li>저장 용량(도메인당 최대 20개, 하나당 4KB)에 제한이 있고, 쿠키가 커지면 네트워크 부하가 증가합니다.</li>
</ul>
<h2 id="세션-인증">세션 인증</h2>
<p>세션은 클라이언트의 민감한 인증 정보를 브라우저가 아닌 <strong>서버에 저장하고 관리하는 방식입니다</strong>. 사용자가 로그인 하면 서버 메모리나 DB에 저장하고, 그 세션을 식별할 세션 ID를 발급합니다. 서버는 세션 ID로 내부의 세션 저장소를 조회해 로그인 상태를 확인합니다.</p>
<h3 id="인증-방식-1">인증 방식</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/600c0758-5479-470e-a98c-c085fb58607f/image.png" alt="세션 인증"></p>
<ol>
<li>사용자가 로그인하면 서버는 세션을 생성하여 메모리나 DB등에 사용자 정보를 저장하고, 고유한 세션 ID를 발급합니다. 이 세션 ID는 쿠키를 통해 클라이언트에 전달됩니다.</li>
<li>이후 클라이언트는 요청을 보낼 때마다 세션 ID가 담긴 쿠키를 전송하고, 서버는 세션 저장소에서 해당 ID로 저장된 사용자 정보를 조회하여 인증 여부를 판단합니다.</li>
</ol>
<h3 id="장점-1">장점</h3>
<ul>
<li>민감한 인증 정보를 클라이언트에 저장하지 않기 때문에 보안성이 높습니다.</li>
<li>사용자 상태를 서버에서 관리하므로 세밀한 제어가 가능합니다.<ul>
<li>디바이스별 로그인 관리 등</li>
</ul>
</li>
</ul>
<h3 id="단점-1">단점</h3>
<ul>
<li>서버에 상태를 저장하기 때문에 서버의 부하가 증가합니다.</li>
</ul>
<h2 id="jwt-인증">JWT 인증</h2>
<p>JWT는 인증에 필요한 정보들을 암호화시킨 JSON 토큰을 의미합니다. JWT 자체에 사용자 인증에 필요한 정보가 포함되고 <strong>전자 서명</strong>으로 위변조 여부를 검증할 수 있기 때문에, 별도의 서버 세션 저장소 없이 <strong>무상태(stateless)</strong> 인증이 가능합니다.</p>
<p>토큰 탈취에 대비해 <strong>Access Token의 만료 시간을 짧게 부여하고 Refresh token을 사용하여 재발급하는 체계</strong>를 갖추는게 일반적이며, 이 Refresh Token은 <code>HttpOnly 쿠키</code> 로 저장하여 엄격히 보호하는 방식도 많이 사용됩니다.</p>
<h3 id="인증방식">인증방식</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/86f80a5b-729c-4bb3-9d80-6117b25b78ef/image.png" alt="JWT 인증"></p>
<ol>
<li>사용자가 로그인하면 서버는 <code>비밀 키</code>로 서명한 JWT 토큰(Access Token)을 생성하여 클라이언트에 발급합니다.</li>
<li>클라이언트는 JWT 토큰을 브라우저 저장소(local storage, cookie 등)에 저장하고, 이후 서버에 요청을 보낼 때 마다 HTTP Authorization 헤더에 JWT를 포함하여 전송합니다.</li>
<li>서버는 JWT의 서명을 검증하여 서버가 발급한 토큰인지 확인하고, 페이로드에서 사용자 정보를 추출해 해당 요청이 인가된 사용자의 요청인지 판단합니다. JWT에 포함된 만료 시간도 확인하여 토큰이 유효한지 확인한 후 요청에 대한 응답을 반환합니다.</li>
</ol>
<h3 id="장점-2">장점</h3>
<ul>
<li>클라이언트가 토큰을 관리하기 때문에 다른 방식들에 비해 서버 부하가 적습니다.</li>
<li>토큰 검증 단계를 거치기 때문에 쿠키 인증 방식보다 안전합니다.</li>
</ul>
<h3 id="단점-2">단점</h3>
<ul>
<li>서버에서는 토큰을 검증하는 것뿐이지, 해당 토큰을 관리하는 것이 아니기에 세션보다는 보안상으로 취약합니다.</li>
</ul>
<h2 id="oauth-인증">OAuth 인증</h2>
<p>OAtuh 2.0는  인증이 아닌 인가를 위한 표준 <code>프로토콜</code>로, 제 3자 서비스에 인증을 위임하는 방식입니다. OAuth를 사용하면 사용자가 자신의 아이디/비밀번호를 서비스에 직접 제공하지 않고도 신뢰 가능한 외부 플랫폼에 인증을 맡겨 그 결과만 받아오는 형태로, 사용자 입장에서는 새로운 계정 생성 없이 기존 계정으로 서비스를 이용할 수 있습니다.</p>
<h3 id="인증-방식authorization-code-grant">인증 방식(Authorization Code Grant)</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/47ec77b0-789f-4afb-88d2-4559b9108102/image.png" alt="OAuth"></p>
<ol>
<li>클라이언트가 인증 서버에게 <strong>인가</strong> <strong>요청</strong>을 합니다.</li>
<li>사용자가 OAuth 제공자의 로그인 화면에서 <strong>아이디/비밀번호로 인증</strong>하고, 해당 애플리케이션이 요청한 권한 범위에 <strong>동의</strong>합니다.</li>
<li>OAuth 제공자는 인증이 완료되면 클라이언트에 <code>인가 코드(Authorization Code)</code>를 발급하여 <strong>리다이렉션 URL</strong>로 전달합니다.</li>
<li>클라이언트는 받은 인가 코드를 OAuth 제공자의 <strong>토큰 엔드포인트</strong>로 보내 Access Token을 교환받습니다. </li>
<li>클라이언트는 발급받은 Access Token을 사용해 OAuth 제공자의 API 호출하거나, <strong>사용자 프로필 정보를 요청</strong>하여 로그인 처리를 완료합니다. <ul>
<li>예를 들어 구글의 사용자 정보 API를 호출해 사용자 이메일 등을 얻은 뒤 자체 서비스에서 세션을 생성</li>
</ul>
</li>
</ol>
<h3 id="장점-3">장점</h3>
<ul>
<li>신뢰할 수 있는 OAuth 제공자에게만 비밀번호를 입력하면 되므로 보안성이 뛰어납니다.</li>
</ul>
<h3 id="단점-3">단점</h3>
<ul>
<li>OAuth 구현 복잡성이 증가합니다.</li>
</ul>
<blockquote>
<p><strong>참고</strong>
<a href="https://youtu.be/tosLBcAX1vk?si=-cCIAjn1AYj2heMQ">세션 vs 토큰 vs 쿠키? 기초개념 잡아드림. 10분 순삭!
</a><a href="https://velog.io/@gusdnr814/%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9D%B8%EC%A6%9D-4%EA%B0%80%EC%A7%80-%EB%B0%A9%EB%B2%95">로그인 인증 4가지 방법
</a><a href="https://ksh-coding.tistory.com/113">[인증/인가] 쿠키 VS 세션 VS 토큰 (JWT) 방식 중 무엇을 사용할까?
</a><a href="https://f-lab.kr/insight/efficient-login-system-authentication-methods">효율적인 로그인 시스템 구축을 위한 인증 방식 비교
</a><a href="https://80000coding.oopy.io/1f213f10-185c-4b4e-8372-119402fecdd0">로그인 인증방식 어떤게 좋을까? Session VS JWT
</a><a href="https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-JWTjson-web-token-%EB%9E%80-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC">🌐 JWT 토큰 인증 이란? (쿠키 vs 세션 vs 토큰)</a><a href="https://velog.io/@aurora1026/%EC%84%B8%EC%85%98-%EB%B0%8F-%EC%BF%A0%ED%82%A4-JWT-OAuth2">세션 및 쿠키, JWT, OAuth2?
</a><a href="https://velog.io/@se0kcess/OAuth-2.0%EC%9D%98-%EA%B0%9C%EB%85%90%EA%B3%BC-%EC%9D%B8%EC%A6%9D-%EB%B0%A9%EC%8B%9D">OAuth 2.0의 개념과 인증 방식
</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[SSR, CSR 그리고 SSG 렌더링]]></title>
            <link>https://velog.io/@jong-kyung/SSR-CSR-%EA%B7%B8%EB%A6%AC%EA%B3%A0-SSG-%EB%A0%8C%EB%8D%94%EB%A7%81</link>
            <guid>https://velog.io/@jong-kyung/SSR-CSR-%EA%B7%B8%EB%A6%AC%EA%B3%A0-SSG-%EB%A0%8C%EB%8D%94%EB%A7%81</guid>
            <pubDate>Sat, 28 Jun 2025 12:35:06 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jong-kyung/post/0c85c3ec-0abf-4ecf-882b-1c83dcc1a5df/image.png" alt="Rendering"></p>
<h2 id="csr-client-side-rendering">CSR (Client-Side Rendering)</h2>
<p>CSR 방식은 서버가 <strong>최소한의 HTML 문서를 제공</strong>하고, 실제 <strong>콘텐츠 렌더링은 클라이언트에서 JavaScript로 수행</strong>하는 방식입니다. 주로 SPA(Single Page Application) 형태의 웹사이트에 널리 사용됩니다.</p>
<h3 id="작동-원리-및-프로세스">작동 원리 및 프로세스</h3>
<ol>
<li>초기 접속 시 서버는 빈 HTML과 JavaScript 파일을 클라이언트로 전송합니다.</li>
<li>클라이언트는 JavaScript를 실행하여 DOM을 동적으로 구성합니다.</li>
<li>추가적인 데이터 요청은 비동기 API 호출로 이루어지고, UI가 동적으로 업데이트됩니다.</li>
</ol>
<h3 id="csr-장점">CSR 장점</h3>
<ul>
<li>동적 인터랙션과 뛰어난 사용자 경험 제공</li>
<li>서버 자원 소모가 적고, 확장성이 뛰어남</li>
<li>클라이언트 측 라우팅 및 상태 관리가 용이함</li>
</ul>
<h3 id="csr-단점">CSR 단점</h3>
<ul>
<li>초기 로딩이 상대적으로 느림(JavaScript 로딩 및 실행)</li>
<li>SEO 성능 저하 가능성 (크롤러의 콘텐츠 수집 제한)</li>
<li>자바스크립트 의존성이 큼</li>
</ul>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot; /&gt;
    &lt;title&gt;My React App&lt;/title&gt;
    &lt;script src=&quot;/static/js/bundle.js&quot;&gt;&lt;/script&gt;  &lt;!-- 번들된 JS --&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;  &lt;!-- React가 렌더링될 컨테이너 --&gt;
    &lt;noscript&gt;이 사이트는 JavaScript를 필요로 합니다.&lt;/noscript&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<pre><code class="language-jsx">import { createRoot } from &#39;react-dom/client&#39;;
import App from &#39;./App&#39;;

createRoot(document.getElementById(&#39;root&#39;)).render(&lt;App /&gt;);</code></pre>
<h2 id="ssr-server-side-rendering">SSR (Server-Side Rendering)</h2>
<p>SSR은 요청을 받을 때마다 <strong>서버가 HTML 콘텐츠를 동적으로 생성</strong>하여 클라이언트에 전달하는 방식으로, 초기 콘텐츠 표시가 매우 빠르고 SEO에 유리합니다.</p>
<h3 id="작동-원리-및-프로세스-1">작동 원리 및 프로세스</h3>
<ol>
<li>서버는 클라이언트 요청을 받은 후 HTML을 즉시 렌더링합니다.</li>
<li>생성된 HTML은 클라이언트로 전송되어 즉시 렌더링됩니다.</li>
<li>클라이언트에서는 JavaScript가 로딩된 후 hydration 과정을 통해 상호작용 가능한 상태로 만듭니다.</li>
</ol>
<h3 id="ssr-장점">SSR 장점</h3>
<ul>
<li>초기 로딩 속도가 뛰어나며 사용자 경험이 우수함</li>
<li>SEO 성능이 뛰어나 검색 엔진 크롤러가 즉시 콘텐츠를 수집 가능</li>
<li>사용자 맞춤형 콘텐츠를 실시간으로 제공 가능</li>
</ul>
<h3 id="ssr-단점">SSR 단점</h3>
<ul>
<li>서버 부하가 증가하여 확장성 문제가 발생할 수 있음</li>
<li>페이지 전환 시 전체 페이지가 재로딩되어 부드럽지 않은 UX 가능성</li>
<li>프론트엔드와 백엔드 환경 설정과 관리 복잡성이 증가</li>
</ul>
<pre><code class="language-jsx">import express from &#39;express&#39;;
import { renderToString } from &#39;react-dom/server&#39;;
import App from &#39;./App&#39;;

const app = express();
app.get(&#39;*&#39;, (req, res) =&gt; {
  const html = renderToString(&lt;App /&gt;);
  res.send(`
    &lt;!DOCTYPE html&gt;
    &lt;html&gt;
      &lt;body&gt;
        &lt;div id=&quot;root&quot;&gt;${html}&lt;/div&gt;
        &lt;script src=&quot;/bundle.js&quot;&gt;&lt;/script&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  `);
});

app.listen(3000);
</code></pre>
<h2 id="ssg-static-site-generation">SSG (Static Site Generation)</h2>
<p>SSG 방식은 <strong>빌드 과정에서 미리 모든 콘텐츠를 정적 HTML 파일로 생성</strong>하여 제공하는 방법입니다. 문서, 블로그, 마케팅 페이지 등 정적 콘텐츠 위주로 구성된 사이트에서 주로 사용됩니다.</p>
<h3 id="작동-원리-및-프로세스-2">작동 원리 및 프로세스</h3>
<ol>
<li>빌드 시 모든 콘텐츠를 HTML 파일 형태로 사전 생성합니다.</li>
<li>생성된 파일은 CDN 또는 정적 호스팅 서비스에 배포됩니다.</li>
<li>클라이언트 요청 시 사전 생성된 정적 HTML이 즉시 제공됩니다.</li>
</ol>
<h3 id="ssg-장점">SSG 장점</h3>
<ul>
<li>매우 빠른 초기 로딩 속도와 우수한 성능</li>
<li>높은 SEO 최적화 성능</li>
<li>트래픽 폭증에도 안정적 운영 가능</li>
</ul>
<h3 id="ssg-단점">SSG 단점</h3>
<ul>
<li>동적 데이터 반영이 어렵고 변경 시마다 재배포가 필요</li>
<li>개인화된 콘텐츠 제공이 제한적</li>
<li>대규모 사이트의 경우 빌드 시간이 오래 걸릴 수 있음</li>
</ul>
<pre><code class="language-jsx">// pages/posts/[id].jsx (Next js)
export default function Post({ post }) {
  return &lt;div&gt;{post.title}&lt;/div&gt;;
}

export async function getStaticPaths() {
  const res = await fetch(&#39;https://api.example.com/posts&#39;);
  const posts = await res.json();
  const paths = posts.map(post =&gt; ({ params: { id: post.id } }));
  return { paths, fallback: false };
}

export async function getStaticProps({ params }) {
  const res = await fetch(`https://api.example.com/posts/${params.id}`);
  const post = await res.json();
  return { props: { post } };
}
</code></pre>
<h2 id="ssr에서-hydration이-필요한-이유">SSR에서 Hydration이 필요한 이유</h2>
<p>Server-Side Rendering(SSR)은 초기 로딩 속도를 높이고 SEO(Search Engine Optimization)에 유리한 환경을 제공합니다. 그러나 서버에서 생성된 HTML은 기본적으로 정적이어서, 사용자와의 동적인 상호작용을 처리할 수 없습니다. 이를 보완하기 위해 등장한 것이 바로 <strong>Hydration</strong>입니다.</p>
<p>Hydration은 서버가 렌더링하여 클라이언트로 전달한 정적 HTML을 클라이언트 측 JavaScript와 연결하여 동적으로 만드는 과정입니다. 이 과정이 없으면 사용자는 화면을 보기는 하지만 버튼 클릭같은 기본적인 인터렉션을 수행할 수 없습니다.</p>
<h2 id="react에서의-hydration-작동-방식">React에서의 Hydration 작동 방식</h2>
<p>React의 Hydration 과정은 크게 세 단계로 이루어집니다.</p>
<h3 id="1-가상-dom과-실제-dom-비교-reconciliation">1. 가상 DOM과 실제 DOM 비교 (Reconciliation)</h3>
<p>서버에서 받은 HTML과 클라이언트에서 생성한 가상 DOM(vDOM)을 비교하여, 서로 일치하는 부분은 재사용하고 일치하지 않는 부분은 재렌더링합니다.</p>
<h3 id="2-이벤트-리스너-연결-및-상태-관리">2. 이벤트 리스너 연결 및 상태 관리</h3>
<p>서버에서 내려온 HTML 요소에 React가 관리하는 이벤트 핸들러와 상태를 연결하여 상호작용 가능한 상태로 만듭니다.</p>
<h3 id="3-인터랙티브한-ui-활성화">3. 인터랙티브한 UI 활성화</h3>
<p>Hydration이 완료되면, 사용자는 정적인 페이지가 아닌 완전히 동작 가능한 인터랙티브한 애플리케이션을 이용할 수 있게 됩니다.</p>
<h2 id="react-18-이후-새롭게-도입된-hydration-전략">React 18 이후 새롭게 도입된 Hydration 전략</h2>
<h3 id="streaming-ssr">Streaming SSR</h3>
<p>페이지 전체가 준비될 때까지 기다리지 않고 HTML 콘텐츠를 조각(chunk) 단위로 나누어 스트리밍 형태로 클라이언트에 전달할 수 있습니다. 이를 통해 사용자는 더 빨리 콘텐츠를 확인할 수 있고, 일부 콘텐츠가 늦게 도착하더라도 사용자 경험에 큰 영향을 미치지 않습니다.</p>
<h3 id="selective-hydration">Selective Hydration</h3>
<p>모든 콘텐츠를 한 번에 Hydration하지 않고 중요한 요소부터 우선적으로 Hydration합니다. 이를 통해 사용자가 실제로 보는 콘텐츠부터 빠르게 활성화되고, 나머지 요소는 필요에 따라 점진적으로 활성화됩니다.</p>
<h3 id="lazy-hydration">Lazy Hydration</h3>
<p>일부 콘텐츠의 Hydration을 사용자의 특정 행동(예: 스크롤, 클릭)까지 연기하는 전략입니다. 초기 로딩 속도를 높이고 전체 애플리케이션 성능을 효과적으로 향상시킬 수 있습니다.</p>
<h2 id="hydration-성능-최적화-기법">Hydration 성능 최적화 기법</h2>
<h3 id="partial-hydration">Partial Hydration</h3>
<p>페이지 전체를 Hydration할 필요 없이, 인터랙티브한 부분만 Hydration하고 정적인 부분은 그대로 둠으로써 성능 부담을 최소화합니다.</p>
<h3 id="progressive-hydration">Progressive Hydration</h3>
<p>페이지를 한 번에 Hydration하지 않고 중요도에 따라 순차적으로 처리합니다. 주요 콘텐츠는 즉시 Hydration하고, 덜 중요한 콘텐츠는 사용자의 브라우저가 유휴 상태일 때 처리하여 성능 효율성을 극대화합니다.</p>
<h3 id="islands-architecture">Islands Architecture</h3>
<p>페이지를 독립적인 UI 모듈(섬)로 나누어 각 섬이 독자적으로 Hydration을 수행하도록 합니다. 이를 통해 각 컴포넌트가 서로 독립적으로 동작하며, 전체 페이지 성능이 개선됩니다.</p>
<blockquote>
<p><strong>참고</strong>
<a href="https://nextjs.org/docs/pages/building-your-application/rendering/server-side-rendering">Server-side Rendering (SSR)
</a><a href="https://www.daleseo.com/spa-ssg-ssr/">SPA와 SSG, 그리고 SSR
</a><a href="https://web.dev/articles/rendering-on-the-web">웹에서 렌더링</a>
<a href="https://helloinyong.tistory.com/315">Next.js의 Hydrate란?</a>
<a href="https://blog.hwahae.co.kr/all/tech/13604">React의 hydration mismatch 알아보기</a>
<a href="https://www.gatsbyjs.com/docs/conceptual/react-hydration/">Understanding React Hydration</a>
<a href="https://github.com/facebook/react/blob/main/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js">react hydration test case</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[V8 엔진의 내부 동작]]></title>
            <link>https://velog.io/@jong-kyung/V8-%EC%97%94%EC%A7%84%EC%9D%98-%EB%82%B4%EB%B6%80-%EB%8F%99%EC%9E%91</link>
            <guid>https://velog.io/@jong-kyung/V8-%EC%97%94%EC%A7%84%EC%9D%98-%EB%82%B4%EB%B6%80-%EB%8F%99%EC%9E%91</guid>
            <pubDate>Wed, 11 Jun 2025 14:07:06 GMT</pubDate>
            <description><![CDATA[<h2 id="v8-엔진의-실행-구조">V8 엔진의 실행 구조</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/96de4b28-0560-4e3f-9b82-267d817e74a0/image.png" alt="v8 엔진 실행"></p>
<pre><code>JavaScript 소스 코드
    ↓
파싱 (Abstract Syntax Tree - AST 생성)
    ↓
Ignition 인터프리터의 바이트코드 생성
    ↓
Ignition 바이트코드 실행 (인터프리팅)
    ↓
실행 중 프로파일링 및 핫 코드 판별
    ↓
Sparkplug 베이스라인 JIT 컴파일
    ↓
TurboFan 최적화 JIT 컴파일
    ↓
디옵티마이제이션 (필요한 경우 최적화 해제)</code></pre><h2 id="v8-동작-원리">V8 동작 원리</h2>
<h3 id="1-파싱-및-바이트코드-생성">1. 파싱 및 바이트코드 생성</h3>
<p>V8은 JavaScript 소스 코드를 분석하여 AST(추상 구문 트리)를 만듭니다. AST는 코드 구조를 명확하게 표현하며, Ignition 인터프리터가 이를 바이트코드로 압축하여 변환합니다. 이 바이트코드는 메모리 효율성을 높이고 실행 속도를 최적화하기 위한 압축 표현을 사용합니다.</p>
<h3 id="2-ignition-인터프리터의-실행">2. Ignition 인터프리터의 실행</h3>
<p>Ignition 인터프리터는 바이트코드를 하나씩 읽고 즉시 실행합니다. 이 단계에서는 타입 체크, 값 로딩, 계산 등 모든 작업을 실시간으로 수행하며, 타입 정보 등 실행 프로파일링 데이터를 수집합니다. 이러한 데이터는 향후 코드 최적화의 중요한 기초로 활용됩니다.</p>
<h3 id="3-프로파일링과-핫-코드-식별">3. 프로파일링과 핫 코드 식별</h3>
<p>코드가 반복 실행되면, V8 엔진은 프로파일링 정보를 활용해 &quot;핫 코드&quot;를 판단합니다. 핫 코드는 반복 실행되어 성능 개선의 효과가 큰 코드 영역을 의미하며, 이 코드 영역이 명확해지면 JIT 컴파일 단계로 넘어갑니다.</p>
<h3 id="4-sparkplug-베이스라인-jit-컴파일">4. Sparkplug 베이스라인 JIT 컴파일</h3>
<p>Sparkplug는 빠르게 바이트코드를 머신 코드로 직접 변환하는 경량 JIT 컴파일러입니다. 중간 IR(Intermediate Representation)을 거치지 않고 바이트코드에서 즉시 간단한 머신 코드를 생성하여, Ignition 인터프리터의 실행 속도보다 빠르게 실행되게 합니다. 이 단계는 즉각적인 성능 개선을 위한 초기 JIT 컴파일입니다.</p>
<h3 id="5-turbofan-최적화-jit-컴파일">5. TurboFan 최적화 JIT 컴파일</h3>
<p>TurboFan은 Sparkplug보다 더욱 복잡한 최적화를 수행하는 고급 JIT 컴파일러입니다. 이 컴파일러는 Sea of Nodes라는 그래프 기반의 중간 표현을 이용해 고급 최적화를 수행합니다. 함수 인라이닝, 타입 특화, 죽은 코드 제거, 범위 검사 제거 등 고급 최적화 기법을 적용해 가장 효율적인 머신 코드를 생성합니다.</p>
<h3 id="6-디옵티마이제이션">6. 디옵티마이제이션</h3>
<p>런타임 동안 코드 실행 환경이 예상과 다르게 변화하거나 타입의 가정이 깨지는 경우가 발생할 수 있습니다. 이런 경우 V8은 즉각적으로 기존의 최적화 코드를 폐기하고, 이전 단계의 바이트코드 또는 Sparkplug 코드를 다시 사용합니다. 이후 보수적이면서도 안전한 방식으로 다시 최적화를 진행할 수 있습니다.</p>
<blockquote>
<p><strong>참고</strong>
<a href="https://velog.io/@heonys/JS-V8%EC%97%94%EC%A7%84%EA%B3%BC-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94">[JS] V8엔진과 성능 최적화</a>
<a href="https://medium.com/@yanguly/sparkplug-v8-baseline-javascript-compiler-758a7bc96e84">Sparkplug, the new lightning-fast V8 baseline JavaScript compiler</a>
<a href="https://mathiasbynens.be/notes/shapes-ics">JavaScript engine fundamentals: Shapes and Inline Caches
</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Micro Frontend 알아보기]]></title>
            <link>https://velog.io/@jong-kyung/Micro-Frontend-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@jong-kyung/Micro-Frontend-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Mon, 02 Jun 2025 11:27:17 GMT</pubDate>
            <description><![CDATA[<h2 id="기본-개념">기본 개념</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/5bc1276e-0a76-41e7-8138-c3241151fd7d/image.png" alt="프론트엔드 구조"></p>
<p>마이크로 프론트엔드 아키텍처는 대규모 프론트엔드 애플리케이션을 <strong>여러 개의 작은 단위로 분리</strong>하여 개발하고 통합하는 방법입니다.</p>
<p>전통적인 <strong>모놀리식 프론트엔드(Monolithic Frontend)</strong>에서는 하나의 프론트엔드 애플리케이션이 모든 기능을 포함하여 한 번에 배포됩니다. 이는 개발과 배포가 단순하지만, 작은 변경에도 <strong>전체 애플리케이션을 다시 배포</strong>해야 하고, 하나의 거대한 코드베이스에서 여러 팀이 함께 작업할 경우 충돌이 발생하기 쉽습니다. 또한 모든 팀이 동일한 기술 스택을 사용해야 하므로 새로운 기술 도입이나 개별 최적화에 제약이 있습니다</p>
<p>이에 반해, <strong>마이크로 프론트엔드(Micro Frontend)</strong>에서는 프론트엔드를 도메인이나 기능별로 잘게 나눈 <strong>독립적인 프론트엔드 모듈</strong>들로 구성합니다. 각 모듈은 자체 코드베이스와 빌드 파이프라인을 가지며, 독립적으로 개발·테스트·배포가 가능합니다. 다시 말해 <strong>“독립적으로 배포 가능한 여러 개의 프론트엔드 애플리케이션을 모아 전체 애플리케이션을 구성하는”</strong> 아키텍처입니다. 이러한 분산 구조 덕분에 필요에 따라 각 모듈마다 <strong>서로 다른 기술 스택</strong>을 사용할 수도 있어 전체 애플리케이션 내에서 기술 다각화를 허용합니다. </p>
<table>
<thead>
<tr>
<th>비교 항목</th>
<th>모놀리식 프론트엔드</th>
<th>마이크로 프론트엔드</th>
</tr>
</thead>
<tbody><tr>
<td><strong>코드베이스 구조</strong></td>
<td>하나의 거대한 저장소(repo) 또는 프로젝트에 전체 UI 코드 통합</td>
<td>여러 개의 독립된 코드베이스 (기능/도메인 별 분리)</td>
</tr>
<tr>
<td><strong>배포 단위</strong></td>
<td>프론트엔드 전체를 일괄 배포 (한 부분 수정 시 전체 재배포)</td>
<td>마이크로앱 단위로 개별 배포 (다른 모듈 영향 없이 부분적 배포)</td>
</tr>
<tr>
<td><strong>기술 스택</strong></td>
<td>전체 애플리케이션에 단일 기술 스택 적용 (한 가지 프레임워크/버전)</td>
<td>모듈마다 최적 기술 선택 가능 (여러 프레임워크 공존 허용)</td>
</tr>
<tr>
<td><strong>스케일링</strong></td>
<td>애플리케이션 전체로만 확장/최적화 가능</td>
<td>트래픽 많은 모듈만 별도 확장 또는 최적화 가능 (독립 스케일링)</td>
</tr>
<tr>
<td><strong>배포 빈도</strong></td>
<td>릴리스 단위가 크고 낮은 빈도의 배포 (예: 수주일 간격)</td>
<td>작고 잦은 배포 가능 (모듈 단위로 수시 출시)</td>
</tr>
<tr>
<td><strong>장애 영향</strong></td>
<td>한 부분의 버그가 전체 프론트엔드에 영향 줄 수 있음</td>
<td>개별 모듈 장애는 해당 부분만 영향 (격리 및 신속한 롤백 용이)</td>
</tr>
<tr>
<td><strong>초기 구축 난이도</strong></td>
<td>아키텍처 단순, 설정 용이</td>
<td>아키텍처 복잡, 통합/배포 설정 난이도 높음</td>
</tr>
</tbody></table>
<h2 id="구현-방식">구현 방식</h2>
<h3 id="build-time-mfe--monorepo">Build-Time MFE : MonoRepo</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/18683ee5-bc2e-45b7-8385-ca2f7ddbe0b2/image.png" alt="BuildTime MFE">
빌드 타임 MFE는 각 마이크로 프론트엔드를 <strong>빌드 시점에서 미리 통합</strong>하는 방식입니다. 즉, 개발 단계에서 코드가 통합되어 하나의 번들로 만들어지는 접근법입니다.</p>
<ul>
<li><strong>단일 저장소 관리</strong><ul>
<li>모든 마이크로 프론트엔드 모듈이 하나의 리포지토리 안에 구성됩니다.</li>
<li>주로 <strong>Yarn workspaces, pnpm workspace, Nx, Turborepo</strong> 같은 툴을 이용해 관리합니다.</li>
</ul>
</li>
<li><strong>의존성 관리의 용이성</strong><ul>
<li>중복된 라이브러리 관리가 용이하며 공통된 라이브러리를 쉽게 공유할 수 있습니다.</li>
<li>동일한 버전 관리 및 코드 통합 관리가 쉽습니다.</li>
</ul>
</li>
<li><strong>빌드 및 배포</strong><ul>
<li>빌드/배포 파이프라인을 중앙에서 통합적으로 관리합니다.</li>
<li>배포 단위가 크며, 변경 사항이 생기면 전체 또는 일부 앱이 함께 배포됩니다.</li>
</ul>
</li>
<li><strong>장점</strong><ul>
<li>빌드 과정에서 전체 애플리케이션이 최적화되어 로드 시간과 효율성이 향상됩니다.</li>
<li>하나의 통합된 번들을 사용하면 여러 배포 복잡성에 대한 배포 및 보호가 더 쉬워집니다.</li>
<li>모든 측면이 특정 종속성 버전을 기반으로 구축되도록 하여 충돌을 방지합니다.</li>
</ul>
</li>
<li><strong>단점</strong><ul>
<li>독립적으로 업데이트하거나 배포할 수 없으므로 팀의 유연성이 떨어집니다.</li>
<li>모든 구성 요소를 구축 과정에서 통합하고 테스트해야 하므로 복잡하고 시간이 많이 걸립니다.</li>
<li>확장성 문제는 다른 부분에 영향을 주지 않고 특정 애플리케이션 부분을 확장해야 할 때 발생합니다.</li>
<li>구성 요소는 빌드 시점에 긴밀하게 결합됩니다.</li>
</ul>
</li>
</ul>
<pre><code>my-monorepo/
├── packages
│   ├── mfe-cart
│   ├── mfe-payment
│   └── shared-ui
├── apps
│   └── container-app
├── package.json (워크스페이스 설정)
└── turbo.json / nx.json</code></pre><h3 id="run-time-mfe--module-federation">Run-Time MFE : Module Federation</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/c62fe6f4-6ae1-432d-9325-2d4d495a9c3e/image.png" alt="RunTime MFE"></p>
<p>런타임 MFE는 마이크로 프론트엔드 모듈들이 <strong>런타임에 동적으로 통합</strong>되는 방식입니다. 배포 이후에도 동적으로 모듈을 로드하거나 변경할 수 있습니다.</p>
<p><strong>특징</strong></p>
<ul>
<li><strong>URL 경로에 따라 애플리케이션을 분할</strong>하는 방식입니다.</li>
<li>각 경로는 완전한 독립적인 앱으로 구분됩니다.</li>
<li>화면이 완전히 교체될 때 사용하는 방식입니다.</li>
</ul>
<p><strong>예시</strong></p>
<ul>
<li><code>/cart</code>, <code>/product</code>, <code>/user-profile</code> 각각의 경로가 다른 애플리케이션으로 구성되어 별도 로드됩니다.</li>
</ul>
<pre><code>- /cart → cart-app 로드
- /payment → payment-app 로드</code></pre><p><strong>장점</strong></p>
<ul>
<li>시스템의 각 부분은 독립적으로 확장될 수 있습니다.</li>
<li>각 팀은 소프트웨어 구성 요소를 개발할 때 자신이 가장 선호하는 도구와 기술을 사용할 수 있습니다.</li>
<li>여러 팀이 동시에 개발할 수 있어 개발 시간이 단축됩니다.</li>
<li>단일 마이크로 프론트엔드 내의 문제는 전체 애플리케이션을 중단시키지 않습니다.</li>
<li>다양한 프로젝트와 애플리케이션에서 구성 요소의 재사용을 향상합니다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>여러 구성 요소를 통합하는 것은 매우 어려울 수 있습니다.</li>
<li>동적 로딩 및 통합 프로세스는 성능 지연이나 오버헤드로 이어질 수 있습니다.</li>
<li>다양한 마이크로 프론트엔드에서 일관된 사용자 인터페이스와 경험을 제공하는 것은 어렵습니다.</li>
<li>구성 요소 전체의 호환성과 기능을 테스트하기 위한 포괄적인 테스트 전략이 필요합니다.</li>
</ul>
<blockquote>
<p><strong>참고</strong>
<a href="https://haesoo9410.tistory.com/411"> Module Federation으로 알아보는 Micro Front-end</a>
<a href="https://newsletter.systemdesign.one/p/micro-frontends">Everything You Need to Know About Micro Frontends</a>
<a href="https://www.syncfusion.com/blogs/post/micro-frontend-run-time-vs-build-time">Micro Frontend: Run-Time Vs. Build-Time Integration</a>
<a href="https://engrmuhammadusman108.medium.com/microfrontend-build-time-intergation-vs-run-time-integration-concepts-cc56bddfbc85">Microfrontend: Build-Time Intergation vs Run-Time Integration Concepts</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Zustand 파헤치기]]></title>
            <link>https://velog.io/@jong-kyung/Zustand-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0</link>
            <guid>https://velog.io/@jong-kyung/Zustand-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0</guid>
            <pubDate>Sat, 24 May 2025 16:20:34 GMT</pubDate>
            <description><![CDATA[<h2 id="zustand의-상태-관리-방식">Zustand의 상태 관리 방식</h2>
<h3 id="상태-저장소-구조">상태 저장소 구조</h3>
<p>Zustand는 <strong>전역 상태 스토어</strong>를 생성하고 사용하는 간단한 구조를 가집니다. <code>create</code> 함수를 호출하여 하나의 스토어를 정의하면, 이 <strong>저장소 자체가 React 훅(hook)</strong>으로 반환됩니다. 따라서 Context Provider로 래핑할 필요 없이 애플리케이션 어디서나 이 훅을 불러 상태를 조회할 수 있습니다. 하나의 store 안에 원하는 상태 값들과 해당 상태를 변경하는 액션 함수들을 함께 정의하며, 필요에 따라 여러 개의 store를 분리하여  관리할 수도 있습니다.</p>
<h3 id="불변성-유지-여부">불변성 유지 여부</h3>
<p>Zustand의 <code>set</code> 함수는 전달된 <strong>부분 상태 객체를 현재 상태에 얕은 복사(shallow copy)</strong>로 병합하여 상태를 변경합니다. 변경된 부분만 새 객체로 만들어주기 때문에 상태 변경 감지가 가능해집니다. 단, <strong>중첩된 객체</strong>를 업데이트할 때는 얕은 복사를 하기 때문에, 필요한 경우 직접 깊은 복사를 하거나 Immer 미들웨어를 사용해야 합니다.</p>
<h3 id="얕은-비교를-통한-리렌더링-방지">얕은 비교를 통한 리렌더링 방지</h3>
<p>Zustand의 큰 장점 중 하나는 <strong>선택적 구독</strong>을 통해 불필요한 리렌더링을 줄일 수 있다는 것입니다. zustand의 <code>useStore</code> 훅을 사용할 때 전체 상태를 가져오는 대신, <strong>selector 함수</strong>를 인자로 전달하여 특정 상태 값만 구독할 수 있습니다. 예를 들어 <code>const count = useStore(state =&gt; state.count)</code>처럼 사용하면 해당 컴포넌트는 store의 <code>count</code> 값만을 바라보게 됩니다. 이 경우 store의 다른 값이 변경될 때는 컴포넌트가 리렌더링되지 않고, 오직 <code>count</code>가 바뀔 때만 리렌더링이 일어납니다. 이는 selector 함수의 반환값을 이전 값과 <strong>비교</strong>하여 변경된 경우에만 컴포넌트를 갱신하기 때문입니다. </p>
<p>만약 selector가 객체나 배열 등 <strong>복합 자료형</strong>을 반환한다면, <strong>얕은 비교</strong>가 필요할 수 있습니다. Zustand는 이를 위해 <code>zustand/shallow</code> 모듈에서 제공하는 얕은 비교 함수를 사용할 수 있게 합니다.</p>
<h2 id="zustand-코어-및-api-흐름">Zustand 코어 및 API 흐름</h2>
<pre><code class="language-tsx">const createStoreImpl: CreateStoreImpl = (createState) =&gt; {
  type TState = ReturnType&lt;typeof createState&gt;
  type Listener = (state: TState, prevState: TState) =&gt; void
  let state: TState // 스토어의 상태는 클로저로 관리 됨.
  const listeners: Set&lt;Listener&gt; = new Set()

  const setState: StoreApi&lt;TState&gt;[&#39;setState&#39;] = (partial, replace) =&gt; {
    const nextState =
      typeof partial === &#39;function&#39;
        ? (partial as (state: TState) =&gt; TState)(state)
        : partial
    if (!Object.is(nextState, state)) {
      const previousState = state
      state =
        (replace ?? (typeof nextState !== &#39;object&#39; || nextState === null))
          ? (nextState as TState)
          : Object.assign({}, state, nextState)
      listeners.forEach((listener) =&gt; listener(state, previousState))
    }
  }

  const getState: StoreApi&lt;TState&gt;[&#39;getState&#39;] = () =&gt; state

  const getInitialState: StoreApi&lt;TState&gt;[&#39;getInitialState&#39;] = () =&gt;
    initialState

  const subscribe: StoreApi&lt;TState&gt;[&#39;subscribe&#39;] = (listener) =&gt; {
    listeners.add(listener)
    // Unsubscribe
    return () =&gt; listeners.delete(listener)
  }

  const api = { setState, getState, getInitialState, subscribe }
  const initialState = (state = createState(setState, getState, api))
  return api as any
}

export const createStore = ((createState) =&gt;
  createState ? createStoreImpl(createState) : createStoreImpl) as CreateStore
</code></pre>
<h3 id="상태-저장">상태 저장</h3>
<p>스토어의 상태는 클로저로 관리 됩니다.</p>
<pre><code class="language-tsx">let state: TState // 스토어의 상태는 클로저로 관리</code></pre>
<p>배열로 관리하게 될 경우 중복된 리스너에 대한 처리가 어렵기에 상태 변경을 구독할 리스너를 Set으로 관리합니다. </p>
<pre><code class="language-tsx"> // 상태 변경을 구독할 리스너를 Set으로 관리한다.
const listeners = new Set();</code></pre>
<h3 id="상태-변경">상태 변경</h3>
<p>현재 상태를 기반으로 새로운 상태를 리턴하는 함수 혹은, 아예 변경하려는 상태 값을 전달받습니다.</p>
<ul>
<li><code>partial</code> : 새로운 상태 또는 상태를 변경하는 함수</li>
<li><code>replace</code> :  상태를 특정 상태로 대체할 것인지 여부</li>
</ul>
<pre><code class="language-tsx">const setState: StoreApi&lt;TState&gt;[&#39;setState&#39;] = (partial, replace) =&gt; {
    const nextState =
      typeof partial === &#39;function&#39;
        ? (partial as (state: TState) =&gt; TState)(state)
        : partial
    if (!Object.is(nextState, state)) {
      const previousState = state
      state =
        (replace ?? (typeof nextState !== &#39;object&#39; || nextState === null))
          ? (nextState as TState)
          : Object.assign({}, state, nextState)
      listeners.forEach((listener) =&gt; listener(state, previousState))
    }
}</code></pre>
<p>다음 상태와 현재 상태가 다를 경우 state를 갱신하거나 대체합니다.</p>
<p>이후, 모든 리스너를 순회하며 모든 구독자에게 <strong>새로운 상태와 이전 상태를 전달</strong>합니다.</p>
<pre><code class="language-tsx">if (!Object.is(nextState, state)) { // 얕은 복사로 다음 상태와 현재 상태 비교
      const previousState = state
    state =
        (replace ?? (typeof nextState !== &#39;object&#39; || nextState === null))
        ? (nextState as TState)
        : Object.assign({}, state, nextState)
    listeners.forEach((listener) =&gt; listener(state, previousState))
  }</code></pre>
<h3 id="상태-반환">상태 반환</h3>
<p>getState를 통해 현재 상태를 가져올 수 있고, getInitialState를 통해 초기 상태를 가져올 수 있습니다.</p>
<pre><code class="language-tsx">  const getState: StoreApi&lt;TState&gt;[&#39;getState&#39;] = () =&gt; state

  const getInitialState: StoreApi&lt;TState&gt;[&#39;getInitialState&#39;] = () =&gt;
    initialState</code></pre>
<h3 id="상태-구독">상태 구독</h3>
<p>subscribe함수는 호출되면 인자로 들어온 함수를 listeners에 추가합니다.</p>
<p>구독을 해제할 수 있는 함수를 반환하여, 이를 호출하면 listeners에 추가된 함수를 제거시킵니다.</p>
<pre><code class="language-tsx">const subscribe: StoreApi&lt;TState&gt;[&#39;subscribe&#39;] = (listener) =&gt; {
  listeners.add(listener)
  // Unsubscribe
  return () =&gt; listeners.delete(listener)
}</code></pre>
<pre><code class="language-tsx">// count 값의 변화만 구독하기
const unsubscribe = useCounterStore.subscribe(
  state =&gt; state.count,                      // 구독할 부분: count 값
  (currentCount, previousCount) =&gt; {
    console.log(`Count 변경: ${previousCount} -&gt; ${currentCount}`);
  }
);
// 나중에 구독 해제
unsubscribe();
</code></pre>
<p>위와 같이 <strong>selector와 리스너 함수를 전달</strong>하여 subscribe하면, <code>count</code> 값이 바뀔 때마다 해당 콜백이 호출됩니다. 이 기능은 React 컴포넌트 바깥에서 상태 변화를 감지하여 별도의 사이드 이펙트를 처리하거나, 혹은 React로 관리하지 않는 UI 요소를 수동으로 업데이트할 때 유용합니다. Subscribe로 등록한 리스너는 <code>set</code>을 통해 상태가 바뀔 때마다 호출되며, 필요 시 반환된 <code>unsubscribe()</code> 함수를 호출하여 구독을 해제할 수 있습니다.</p>
<h3 id="zustand-usestore의-동작-방식">Zustand useStore의 동작 방식</h3>
<p>Zustand의 <code>useStore</code> 훅 구현은 내부적으로 <code>useSyncExternalStore</code>를 활용합니다. 이는 React 18 버전에서 공개된 외부 상태 구독 훅으로, Concurrent Mode가 도입되며 외부 상태 관리 패키지를 사용할 때 <a href="https://github.com/reactwg/react-18/discussions/69"><strong>티어링</strong></a> 이슈가 발생할 수 있게 되어 이를 보완하고자 만들어진 방식입니다.  Zustand와 같은 <strong>외부 저장소의 상태</strong>를 React 컴포넌트에 안전하게 동기화해줍니다. 덕분에 Zustand를 사용하는 컴포넌트들은 Concurrent Mode에서도 <strong>일관성 있게</strong> 최신 상태를 얻을 수 있고, 렌더링 도중 상태가 변경되는 문제를 예방합니다. 요약하면, Zustand store는 React 바깥에서 관리되는 외부 상태이지만 <code>useSyncExternalStore</code>를 통해 React와 깔끔하게 연결되어 동작합니다.</p>
<pre><code class="language-tsx">function useStore&lt;TState, StateSlice&gt;(
  api: ReadonlyStoreApi&lt;TState&gt;,
  selector: (state: TState) =&gt; StateSlice = identity as any,
) {
  const slice = useSyncExternalStore(
    api.subscribe,
    () =&gt; selector(api.getState()),
    () =&gt; selector(api.getInitialState()),
  )
  useDebugValue(slice)
  return slice
}

const createImpl = &lt;T&gt;(createState: StateCreator&lt;T, [], []&gt;) =&gt; {
    const api = createStore(createState)

  const useBoundStore: any = (selector?: any) =&gt; useStore(api, selector)

  Object.assign(useBoundStore, api)

  return useBoundStore
}</code></pre>
<blockquote>
<p><strong>참고</strong>
<a href="https://ui.toast.com/posts/ko_20210812">https://ui.toast.com/posts/ko_20210812</a>
<a href="https://ykss.netlify.app/react/flux/?ref=codenary">https://ykss.netlify.app/react/flux/?ref=codenary</a>
<a href="https://ted-projects.com/react-use-sync-external-store">https://ted-projects.com/react-use-sync-external-store</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[실행 컨텍스트란?]]></title>
            <link>https://velog.io/@jong-kyung/execution-context</link>
            <guid>https://velog.io/@jong-kyung/execution-context</guid>
            <pubDate>Sat, 17 May 2025 15:03:34 GMT</pubDate>
            <description><![CDATA[<h2 id="실행-컨텍스트-개요">실행 컨텍스트 개요</h2>
<p>실행 컨텍스트(Execution Context)란 <strong>자바스크립트 코드가 실행되는 환경 정보들의 모음</strong>입니다. 자바스크립트 엔진은 실행 가능한 코드 블록을 만날 때 마다 해당 코드 실행에 필요한 환경을 담은 실행 컨텍스트를 만들고 관리합니다.</p>
<p>이 컨텍스트에는 현재 사용할 수 있는 <strong>식별자(변수, 함수 등)의 정보, 상위 스코프(outer scope)에 대한 참조</strong>, 그리고 <strong>this 키워드가 가리키는 대상</strong> 등이 포함됩니다.</p>
<h2 id="실행-컨텍스트의-구성-요소">실행 컨텍스트의 구성 요소</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/6bc4e1b4-e1d6-4210-8f30-54dd99cadddc/image.png" alt="실행 컨텍스트"></p>
<p>자바스크립트 엔진의 실행 컨텍스트는 세 가지로 구성됩니다.</p>
<h3 id="variable-environment">Variable Environment</h3>
<p>현재 컨텍스트의 <code>식별자(변**수)</code>들에 대한 환경으로, <code>var</code><strong>로 **</strong>선언된 변수와 <strong>함수 선언문으로 생성된 함수 정보</strong>를 담<strong>고</strong> 있으며, 컨텍스트 생성 시 초기 스냅샷으로 저장됩니다. 이 스냅샷은 실행 도중 변화되지 않고, 새 변수 선언<code>(var)</code>은 이 환경에 추가됩니다.</p>
<h3 id="lexical-environment">Lexical Environment</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/8253723f-4b15-4119-90b2-99ab6d30a642/image.png" alt="Lexical Scope"></p>
<p>현재 컨텍스트의 <strong>렉시컬 환경</strong>으로, 변수 정보와 더불어  <strong>외부 렉시컬 환경에 대한 참조(outer environment reference)</strong>를 포함합니다. 컨텍스트 생성 시에는 <strong>Variable Environment</strong>와 동일한 내용을 가지지만, <strong>실행 중 새로운 렉시컬 환경이 생성되면 Lexical Environment는 해당 환경을 가리키도록 변경</strong>됩니다.</p>
<p>Lexical Environment 내부에는 <strong>환경 레코드(Environment Record)</strong>와 외부 환경 참조 정보가 있어, 현재 스코프에 없는 식별자를 찾을 때 <strong>스코프 체인(Scope Chain)</strong>을 따라 외부 스코프로 접근할 수 있게 합니다.</p>
<blockquote>
<p><strong>환경 레코드(Environment Record)</strong></p>
</blockquote>
<ul>
<li><strong>Environment Record</strong>는 “식별자와 그에 대응하는 값을 저장·관리하는 추상 레코드”를 의미합니다.</li>
<li>이 레코드는 다양한 타입(Declarative, Object, Global, Module)으로 구현되어, 스코프별 바인딩을 정확히 처리합니다.</li>
<li>렉시컬 환경은 이 레코드와 외부 참조(<code>outer</code>)를 통해 식별자 해석(Identifier Resolution)을 수행합니다.</li>
</ul>
<h3 id="this-binding">This Binding</h3>
<p>해당 컨텍스트에서의 <strong><code>this</code> 키워드가 가리키는 객체에 대한 참조</strong>입니다. </p>
<p>위 구성 요소들은 <strong>컨텍스트 생성 단계(Creation Phase)</strong>에서 초기화되고 역할을 수행합니다.</p>
<p>일반적으로 Variable Environment와 Lexical Environment는 컨텍스트 생성 시 동일한 환경으로 시작하지만, 특정 상황에서 Lexical Environment가 새로운 환경을 가리키게 되어 두 환경이 달라질 수 있습니다.</p>
<p>이렇게 하는 이유는 예를 들어 블록 스코프의 <code>let</code> / <code>const</code> 변수가 생길 때 새 렉시컬 환경을 만들어도, var 선언은 여전히 기존 함수 레벨 환경에 추가하기 위함입니다. 따라서 스코프 체인을 따라 식별자를 검색할 때는 Lexical Environment 기준으로 하고, 새로운 변수 선언(특히 var)은 Variable Environment에 추가되어 호이스팅 등 동작을 결정합니다.</p>
<h3 id="variable-environment-vs-lexical-envrionment">Variable Environment vs Lexical Envrionment</h3>
<p>아래 코드는 함수 내부의 블록에서 <code>var</code>와 <code>let</code>으로 변수를 선언하는 경우를 비교합니다.</p>
<pre><code class="language-jsx">function scopeTest() {
  var x = 1;              // 함수 스코프 변수 (VariableEnvironment에 등록)
  if (true) {
    var x = 2;            // 같은 함수 스코프의 변수 x를 다시 선언 (기존 var x를 변경)
    let y = 3;            // 블록 스코프 변수 (새 LexicalEnvironment에 등록)
    console.log(x);       // 2  -&gt; var는 함수 전체에서 하나의 변수로 취급됩니다.
    console.log(y);       // 3  -&gt; let은 블록 내에서만 유효합니다.
  }
  console.log(x);         // 2  -&gt; 블록에서 var로 변경된 값이 유지됩니다.
//console.log(y);         // ReferenceError -&gt; y는 블록을 벗어나면 접근 불가
}
scopeTest();</code></pre>
<p><strong>Step 1: 함수 실행 컨텍스트 생성</strong></p>
<ul>
<li><code>scopeTest()</code>가 호출되면 새로운 <strong>함수 실행 컨텍스트(Function Execution Context)</strong>가 생성됩니다.</li>
<li>이 컨텍스트는 두 가지 환경을 가지고 있습니다.<ul>
<li><strong>Variable Environment:</strong> 함수 내 <code>var</code> 선언 변수가 저장되는 환경</li>
<li><strong>Lexical Environment:</strong> 블록 스코프(<code>let</code>, <code>const</code>) 변수가 저장되는 환경</li>
</ul>
</li>
</ul>
<p><strong>Step 2: 함수 스코프 (<code>var</code>)</strong></p>
<ul>
<li>처음 <code>var x = 1;</code>이 선언될 때, 변수 <code>x</code>는 <strong>Variable Environment(함수 스코프)</strong>에 저장됩니다.</li>
<li>이 변수는 함수 전체에서 유효하며, 함수 범위 내 어디에서든 접근 가능합니다.</li>
</ul>
<p><strong>Step 3: 블록 스코프 진입</strong></p>
<ul>
<li><code>if (true)</code> 블록을 만나면, 자바스크립트 엔진은 새로운 블록 전용 <strong>Lexical Environment</strong>를 생성합니다.</li>
<li>이 환경은 블록 스코프(<code>let</code>, <code>const</code>)로 선언된 변수만 저장합니다.</li>
</ul>
<p><strong>Step 4: 블록 내 변수 선언 (<code>var</code> vs <code>let</code>)</strong></p>
<ul>
<li>블록 내에서 <code>var x = 2;</code>는 기존 <strong>함수의 Variable Environment</strong>에서 선언된 변수 <code>x</code>를 재사용하여 값을 변경합니다.<ul>
<li>따라서 블록 밖에서도 값 변경이 유지됩니다.</li>
</ul>
</li>
<li>블록 내에서 <code>let y = 3;</code>는 블록 전용 Lexical Environment에 새롭게 등록됩니다.<ul>
<li>이 변수는 블록 내에서만 유효하며, 블록 밖에서는 접근이 불가능합니다.</li>
</ul>
</li>
</ul>
<p><strong>Step 5: 변수 참조 결과</strong></p>
<ul>
<li>블록 내:<ul>
<li><code>console.log(x); // 2</code> : 기존의 함수 스코프 변수 <code>x</code>가 변경된 값으로 출력됩니다.</li>
<li><code>console.log(y); // 3</code> : 블록 스코프 변수 <code>y</code>가 출력됩니다.</li>
</ul>
</li>
<li>블록 밖:<ul>
<li><code>console.log(x); // 2</code> : 블록 내에서 변경된 함수 스코프 변수 <code>x</code>의 값이 유지됩니다.</li>
<li><code>console.log(y); // ReferenceError</code> : 블록이 끝난 후 Lexical Environment에서 사라지므로 접근 불가능합니다.</li>
</ul>
</li>
</ul>
<h2 id="this-바인딩과-실행-컨텍스트">This 바인딩과 실행 컨텍스트</h2>
<p>자바스크립트의 this 키워드는 <strong>현재 실행중인 컨텍스트와 호출 방식에 따라 결정되는 값</strong>입니다. 실행 컨텍스트 객체의 구성 요소 중 <code>This Binding</code> 에 해당 컨텍스트에서의 <code>this</code> 가 저장되며, 함수가 어떻게 호출되었느냐에 따라 <code>this</code> 에 바인딩될 대상이 동적으로 정해집니다.</p>
<h3 id="this-바인딩-규칙">This 바인딩 규칙</h3>
<ul>
<li>전역 컨텍스트<ul>
<li>전역에서의 this는 기본적으로 <strong>전역 객체</strong>를 가리킵니다.</li>
<li>전역에서 선언한 변수나 함수는 전역 객체의 프로퍼티로 접근되는데, 이는 전역 실행 컨텍스트의 Lexical Environment가 전역 객체를 활용하기 때문입니다.</li>
<li>단, ES 모듈이나 <code>‘use strict’</code> 모드의 전역 코드에서는 this가 undefined가 될 수 있습니다.</li>
</ul>
</li>
<li>일반 함수 호출<ul>
<li>어떤 함수가 독립적으로 호출될 때 그 함수 내부의 this는 전역 객체로 설정됩니다.</li>
<li>함수가 특정 객체의 메서드로 호출되지 않았다면 기본적으로 전역 컨텍스트처럼 동작합니다.</li>
<li>단, <code>‘use strict’</code> 모드에서는 undefined로 설정됩니다.</li>
</ul>
</li>
<li>메서드 호출<ul>
<li>객체의 프로퍼티로 함수를 호출할 경우, 해당 함수 내부의 this는 호출한 <strong>객체 인스턴스</strong>를 가리킵니다.</li>
</ul>
</li>
<li>생성자 호출<ul>
<li>함수를 new 키워드를 사용하여 <code>new Func()</code> 와 같이 호출하면 새로운 빈 객체가 만들어지고, 함수 내부의 this 는 그 신규 객체를 가리키게 됩니다.</li>
<li>이때 함수가 별도로 객체를 반환하지 않으면 <code>new</code> 호출 표현식의 결과로 그 새로운 객체가 반환됩니다.</li>
</ul>
</li>
<li>명시적 바인딩<ul>
<li>함수 객체의 메서드인 <code>call</code>, <code>apply</code>, <code>bind</code> 를 사용하면 함수 호출 시 임의의 객체를 this로 지정할 수 있습니다.</li>
</ul>
</li>
<li>화살표 함수<ul>
<li>화살표 함수 내에서 this를 참조하면 <strong>자신을 감싼 외부 컨텍스트</strong>의 this를 그대로 사용합니다.</li>
<li>화살표 함수는 정의될 때 this 바인딩이 이미 정적으로 결정되며, 이후 어떤 객체의 메서드로서 호출되어도 this값이 변하지 않습니다.</li>
</ul>
</li>
</ul>
<pre><code class="language-jsx">// 1. 전역 컨텍스트
console.log(this);          // window 객체 (브라우저 환경 가정)

// 2. 일반 함수 호출
function independent() {
  console.log(this);
}
independent();              // window (비엄격 모드), undefined (엄격 모드)

// 3. 메서드 호출
const obj = {
  value: 42,
  getValue: function() {
    console.log(this);
    return this.value;
  }
};
obj.getValue();             // obj 객체 (this가 obj를 가리킴)

// 4. 생성자 호출
function Person(name) {
  this.name = name;
}
const alice = new Person(&#39;Alice&#39;);
console.log(alice.name);    // &quot;Alice&quot; (`this`가 새로 생성된 alice 객체)

// 5. 화살표 함수
const arrowFunc = () =&gt; console.log(this);
arrowFunc();                // window (자신의 상위 컨텍스트가 전역인 경우)
// 화살표 함수를 객체 메서드로 설정해도 여전히 상위 스코프(this)는 전역
obj.arrowMethod = arrowFunc;
obj.arrowMethod();          // window (화살표 함수는 obj에 바인딩되지 않음)

// 6. 명시적 바인딩
function showThis() {
  console.log(this);
}
showThis.call(obj);         // obj (call로 명시적으로 this를 obj로 설정)
</code></pre>
<h2 id="클로저와-실행-컨텍스트">클로저와 실행 컨텍스트</h2>
<p>클로저란 <strong>어떤 함수가 자신이 정의된 환경(렉시컬 스코프)의 변수들을 계속해서 접근</strong>할 수 있는 현상을 말합니다.</p>
<p>클로저는 함수가 중첩되어 정의될 때 내부 함수가 외부 함수의 변수에 접근하는 것을 가능하게 하며, 외부 함수 실행이 종료된 이후에도 내부 함수가 그 변수를 기억하고 사용할 수 있도록 해줍니다. 이러한 동작이 가능한 이유는 자바스크립트가 렉시컬 스코프를 따르는 언어이기 떄문입니다. 함수가 선언될 때 자신의 상위 스코프에 대한 참조를 내부 슬롯에 저장하고, 함수가 실행될 때 그 참조를 토대로 상위 Lexical Environment를 연결하여 스코프 체인을 구성합니다.</p>
<p>일반적으로 함수 실행이 끝나면 그 실행 컨텍스트와 지역 변수들은 스택에서 제거되어 가비지 컬렉터에 의해 메모리가 회수됩니다. 그러나 클로저를 형성하는 경우, 이미 종료된 <strong>외부 함수의 변수들도 내부 함수에서 계속 참조되고 있으므로 메모리에 유지</strong>됩니다.</p>
<p>내부 함수가 살아있는 한, 내부 함수가 참조하는 외부 Lexical Environment에 있는 변수들도 <strong>가비지 컬렉션이 되지 않고 유지</strong>되는 것 입니다. 이는 의도한 동작으로서 우리가 클로저를 통해 값을 보존할 수 있는 이유지만, 동시에 불필요하게 오래 참조가 남아있는 변수들은 메모리를 점유하는 부작용도 있습니다.</p>
<p>따라서 클로저를 사용할 때는 필요 없어진 참조를 제거하여 메모리 누수를 방지하는 것이 중요합니다.</p>
<pre><code class="language-jsx">function createCounter() {
  let count = 0;            // 외부 함수의 지역 변수 (createCounter 실행 컨텍스트 내)
  console.log(&quot;Counter 생성, 초기 count =&quot;, count);
  return function() {       // 내부 함수 반환 (클로저 형성)
    count++;                // 외부 함수의 지역 변수에 접근하여 증가
    console.log(`현재 count 값: ${count}`);
  };
}

const counterA = createCounter();  // counterA는 내부 함수에 대한 참조
counterA();  // 출력: &quot;현재 count 값: 1&quot;
counterA();  // 출력: &quot;현재 count 값: 2&quot;

const counterB = createCounter();  // 새로운 카운터 생성 (별도 렉시컬 환경)
counterB();  // 출력: &quot;현재 count 값: 1&quot; (counterA의 count와 독립적인 값)
</code></pre>
<blockquote>
<p><strong>참고</strong>
<a href="https://www.youtube.com/watch?v=zdGfo6I1yrA&amp;t=95s">https://www.youtube.com/watch?v=zdGfo6I1yrA&amp;t=95s</a>
<a href="https://meetup.nhncloud.com/posts/86">https://meetup.nhncloud.com/posts/86</a>
<a href="https://junilhwang.github.io/TIL/Javascript/Domain/Execution-Context/#summary">https://junilhwang.github.io/TIL/Javascript/Domain/Execution-Context/#summary</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes Probe & ArgoCD]]></title>
            <link>https://velog.io/@jong-kyung/Kubernetes-Probe-ArgoCD</link>
            <guid>https://velog.io/@jong-kyung/Kubernetes-Probe-ArgoCD</guid>
            <pubDate>Fri, 16 May 2025 12:08:09 GMT</pubDate>
            <description><![CDATA[<h2 id="pod의-생명주기">Pod의 생명주기</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/e569bb8d-ec2a-4bb1-a0a1-d934633b1c04/image.png" alt="Pod 생명주기"></p>
<p>쿠버네티스에서 가장 기본적인 관리 단위가 <strong>Pod</strong>입니다. Pod는 하나 이상의 컨테이너 그룹을 포함하며, 컨테이너의 실행 상태를 단계별로 관리합니다. 주요 단계는 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th>Phase</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td>Pending</td>
<td>Pod Lifecycle 의 첫 시작 단계</td>
</tr>
<tr>
<td>Pod 가 Node 에 할당되었지만 아직 Container 설정이 진행되고 있거나 실행 준비가 되지 않은 상태</td>
<td></td>
</tr>
</tbody></table>
<ul>
<li>Node 를 사용할 수 없는 상태</li>
<li>Container 를 생성하기 위한 자원이 부족한 상태</li>
<li>Container Image 를 아직 가져오지 못한 상태 |
| Running | Pod Container 가 생성되었고 1개 이상의 Container 가 실행중이거나 시작, 재시작 상태 |
| Succeeded | Pod 내부의 모든 Container 가 성공적으로 종료된 상태 |
| Failed | Pod 내부 모든 Container 가 종료되었고, 적어도 1개 이상의 Container 가 실패로 종료된 상태 |
| Unknown | 어떤 이유에 의해서 Pod 의 상태를 얻을 수 없는 상태
일반적으로 파드와 노드간의 통신 오류 |</li>
</ul>
<h2 id="probe-매커니즘">Probe 매커니즘</h2>
<p>쿠버네티스에서는 컨테이너의 상태를 주기적으로 점검(Health Check)하여 애플리케이션의 가용성을 높입니다. 이것을 <strong>Probe</strong>라고 합니다. Probe는 컨테이너가 정상 작동하는지를 주기적으로 확인하여 문제가 발생하면 재시작 등의 조치를 자동으로 수행합니다.</p>
<h3 id="probe-방식">Probe 방식</h3>
<ul>
<li><strong>exec</strong>: 컨테이너 내부에서 <strong>특정 명령어를 실행</strong>하여 상태 확인</li>
<li><strong>grpc</strong>: <strong>gRPC 프로토콜</strong>로 상태를 확인 (일반적으로 전문적인 서비스에 사용)</li>
<li><strong>httpGet</strong>: <strong>HTTP 요청</strong>을 보내 상태를 확인 (가장 흔히 사용됨)</li>
<li><strong>tcpSocket</strong>: <strong>TCP 포트</strong>를 통해 서비스가 열려있는지 확인</li>
</ul>
<h2 id="probe-종류">Probe 종류</h2>
<h3 id="livenessprobe">livenessProbe</h3>
<ul>
<li>컨테이너가 살아있는지를 주기적으로 점검합니다.</li>
<li>실패 시, 컨테이너를 재시작하여 자동으로 복구합니다.</li>
</ul>
<p><strong>livenessProbe 생성</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  labels:
    test: liveness
  name: liveness-http
spec:
  containers:
  - name: liveness
    image: registry.k8s.io/e2e-test-images/agnhost:2.40
    args:
    - liveness
    livenessProbe:
      httpGet:
        path: /healthz
        port: 8080
        httpHeaders:
        - name: Custom-Header
          value: Awesome
      initialDelaySeconds: 3 # 첫 번째 프로브를 수행하기 전 3초 대기
      periodSeconds: 3 # kubelet이 3초마다 LivenessProbe를 실행</code></pre>
<p>위 pod를 배포하면 아래와 같이 실패할 경우 재시작되는 것을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/4d18e8a4-2f1f-4503-8ee9-fb8f23c6fb22/image.png" alt="pod 실행 상태 1"></p>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/9eceb3e4-49a0-4b4d-ae87-e7faac5ea8f5/image.png" alt="pod 실행 상태 2"></p>
<h3 id="readinessprobe">readinessProbe</h3>
<ul>
<li>서비스 트래픽을 받을 준비가 되었는지를 점검합니다.</li>
<li>실패하면 서비스로부터 컨테이너를 재기동하지 않습니다.</li>
</ul>
<p><strong>readliness Probe 생성</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  labels:
    test: readiness
  name: readiness-http
spec:
  containers:
  - name: readiness
    image: registry.k8s.io/e2e-test-images/agnhost:2.40
    args:
    - liveness
    readinessProbe:
      httpGet:
        path: /healthz
        port: 8080
        httpHeaders:
        - name: Custom-Header
          value: Awesome
      initialDelaySeconds: 3
      periodSeconds: 3</code></pre>
<p>위 pod를 배포하면 아래와 같이 확인할 수 있다. liveness probe와 달리 <strong>컨테이너를 재기동하지 않으며 트래픽을 차단</strong>한다.</p>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/f272da90-df92-4a34-bd75-01600b9bd818/image.png" alt="readiness probe1"></p>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/ce60a0b5-e7cb-4976-ae4e-7dee729b9396/image.png" alt="readiness probe2"></p>
<h3 id="startupprobe">startupProbe</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/28a74e1e-7023-4214-8844-28474070f1f2/image.png" alt="startup probe"></p>
<ul>
<li>컨테이너 내의 <strong>애플리케이션이 시작되었는지 여부</strong></li>
<li>스타트업 프로브 (startup probe) 가 주어진 경우, <strong>성공할 때까지 다른 나머지 프로브는 활성화되지 않는다.</strong></li>
<li>만약 스타트업 프로브가 실패하면, <strong>kubelet이 컨테이너를 죽이고, 컨테이너는 재시작 정책에 따라 처리</strong>된다.</li>
<li>컨테이너에 스타트업 프로브가 없는 경우, 기본 상태는 Success</li>
</ul>
<h2 id="manifest-관리">Manifest 관리</h2>
<p>쿠버네티스에서 리소스를 생성하거나 변경하기 위해 사용하는 YAML 파일을 <strong>Manifest</strong>라고 합니다. Manifest는 관리가 복잡할 수 있어서, 다양한 도구를 통해 효율적으로 관리합니다.</p>
<h3 id="manifest-관리의-필요성">Manifest 관리의 필요성</h3>
<ul>
<li>리소스 정의 및 관리의 용이성</li>
<li>배포 환경의 일관성 유지</li>
<li>리소스 변경 이력 관리</li>
</ul>
<h3 id="kustomize">Kustomize</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/b2402b44-cce2-43f3-b923-9603a1687556/image.png" alt="kustomize"></p>
<ul>
<li>쿠버네티스 리소스를 템플릿 없이 YAML 파일을 직접 패치하거나 병합하여 관리하는 도구입니다.</li>
<li>여러 환경(dev, staging, prod)에 따른 리소스 설정을 쉽게 관리할 수 있습니다.</li>
</ul>
<p><strong>디렉토리 구조</strong></p>
<ul>
<li><p>Base</p>
<ul>
<li>Kustomize 를 통해 <strong>변경할 yaml 파일이 저장된 디렉토리</strong></li>
<li>재사용이 가능한 yaml 로 구성<ul>
<li>deployment.yaml, service.yaml 같은 공통 파일</li>
</ul>
</li>
<li>Terraform - Module / Helm - Helm Chart 와 유사</li>
</ul>
</li>
<li><p>Overlay</p>
<ul>
<li>Base 에 저장된 yaml 파일에 적용할 <strong>kustomization.yaml 이 저장된 디렉토리</strong></li>
<li>환경별 차이점 정의 (dev, stg, prd)<ul>
<li>overlays/dev, overlays/stg, overlays/prd</li>
</ul>
</li>
</ul>
</li>
<li><p>kustomization.yaml</p>
<ul>
<li>kustomize 가 실행될 때 어떤 필드를 재정의 할 것인가를 설정하는 파일</li>
<li>resource, patches, namePrefix, ConfigMapGenerator 등 다양한 설정</li>
</ul>
</li>
</ul>
<p><strong>Kustomize 실행</strong></p>
<pre><code class="language-bash"># kustomize 설치
brew install kustomize</code></pre>
<ol>
<li>kustomize.yaml 생성</li>
</ol>
<pre><code class="language-bash">resources:
  - pod.yaml

images:
  - name: nginx
    newName: new-nginx
    newTag: 1.23.1</code></pre>
<ol>
<li>pod.yaml 생성</li>
</ol>
<pre><code class="language-bash">apiVersion: v1
kind: Pod
metadata:
  labels:
    name: nginx
  name: nginx
spec:
  containers:
  - image: nginx:latest
    name: nginx
    resources:
      limits:
        cpu: 100m
        memory: 64Mi</code></pre>
<ol>
<li>kustomize 실행</li>
</ol>
<pre><code class="language-bash">kubectl kustomize &lt;PATH&gt;</code></pre>
<p>위 명령어를 실행하면 기존의 pod를 수정하지 않고 필드만 재정의된 것을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/7a902a75-52bd-4b4e-8f85-1e6179361aa5/image.png" alt="kustomize 배포"></p>
<h3 id="helm">Helm</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/8cf6c42e-bdca-4fb6-a9f4-eba2914e6717/image.png" alt="helm"></p>
<p>쿠버네티스의 패키지 매니저 역할을 하며, Manifest 파일을 쉽게 관리하고 배포할 수 있도록 도와줍니다.</p>
<h3 id="hem-구조">Hem 구조</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/ba752b1b-cc21-465c-a52f-8cb3e43562b3/image.png" alt="helm 구조"></p>
<ul>
<li><p><strong>Chart</strong>: Helm의 패키지 단위로, 리소스 정의를 포함합니다.</p>
</li>
<li><p><strong>Repository</strong>: Helm Chart를 저장하고 관리하는 공간입니다.</p>
</li>
<li><p><strong>Release</strong>: 특정 Chart를 클러스터에 배포한 상태를 나타냅니다.</p>
</li>
<li><p><strong>Values</strong>: Chart에서 동적으로 설정 가능한 변수입니다.</p>
<p>  <img src="https://velog.velcdn.com/images/jong-kyung/post/95f5648a-beab-41e5-b967-1fcbb17c336a/image.png" alt="파일구조"></p>
</li>
</ul>
<h3 id="helm-기본-명령어">Helm 기본 명령어</h3>
<pre><code class="language-bash"># helm 생성
helm create myChart

# helm install &lt;RELEASE_NAME&gt; &lt;패키지 경로&gt; [flags]
helm install myapp .

# Helm Release 확인
helm list

# Helm 삭제
helm delete myapp</code></pre>
<h2 id="gitops">GitOps</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/223b6c8e-e2de-4ed1-bc9b-b8e21193664a/image.png" alt="GitOps"></p>
<p>GitOps는 Git 저장소를 이용해 인프라 및 애플리케이션 배포를 자동화하는 방식을 말합니다.</p>
<h3 id="핵심-개념">핵심 개념</h3>
<ul>
<li><strong>선언적(Declarative) 정의</strong>: 모든 배포 상태가 Git에 저장된 코드로 선언됩니다.</li>
<li><strong>버전 관리</strong>: Git을 통해 모든 변경사항을 추적하며, Rollback도 간편합니다.</li>
<li><strong>자동 동기화</strong>: 클러스터의 상태를 Git의 상태와 일치하게 유지합니다.</li>
</ul>
<h2 id="argocd">ArgoCD</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/c1cf0ed4-a1ef-4487-91a3-8bd9287b8d96/image.png" alt="ArgoCD"></p>
<p>쿠버네티스를 위한 대표적인 GitOps 도구 중 하나로, 지속적 배포(Continuous Delivery)를 수행합니다. Git 저장소의 변경을 모니터링하고, 선언된 상태를 클러스터에 자동으로 적용합니다.</p>
<ul>
<li><p><strong>선언적 접근 방식(Declarative approach)</strong></p>
<p>  클러스터의 모든 상태를 Git 저장소에 명시적으로 선언된 코드로 관리합니다.</p>
</li>
<li><p><strong>Git 중심의 버전 관리(Git-centric version management)</strong></p>
<p>  모든 변경 사항은 Git을 통해 관리되며, 코드 리뷰, 추적 및 롤백을 간편하게 수행할 수 있습니다.</p>
</li>
</ul>
<h3 id="argocd-핵심-개념">ArgoCD 핵심 개념</h3>
<ol>
<li><strong>선언적 관리 (Declarative Management)</strong><ul>
<li>원하는 애플리케이션 상태를 Git에 선언적으로 정의하여 유지 관리합니다.</li>
<li>개발자는 인프라가 아닌 애플리케이션 코드에 더 집중할 수 있습니다.</li>
</ul>
</li>
<li><strong>자동 동기화 (Automated Synchronization)</strong><ul>
<li>Git 저장소와 쿠버네티스 클러스터의 상태를 지속적으로 비교하여 자동 동기화합니다.</li>
<li>클러스터 상태가 Git의 선언적 상태에서 벗어나면 이를 자동으로 정정합니다.</li>
</ul>
</li>
<li><strong>즉각적인 Rollback 및 Rollout</strong><ul>
<li>Git에 커밋한 내용을 기준으로 배포 상태를 쉽게 되돌릴 수 있습니다.</li>
<li>버전 변경 이력이 명확하여 문제 발생 시 빠르게 이전 상태로 돌아갈 수 있습니다.</li>
</ul>
</li>
<li><strong>셀프 힐링(Self-Healing)</strong><ul>
<li>클러스터 상태가 선언된 Git 상태와 다를 경우 자동으로 상태를 원상 복구합니다.</li>
<li>의도하지 않은 수동 변경 또는 오류가 발생했을 때, 자동으로 수정 가능합니다.</li>
</ul>
</li>
</ol>
<h3 id="argocd-아키텍처">ArgoCD 아키텍처</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/e6829a94-8585-4950-97db-95d1cfd8dbef/image.png" alt="ArgoCD 아키텍처"></p>
<table>
<thead>
<tr>
<th>구성 요소</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><strong>API Server</strong></td>
<td>웹 UI, CLI, API를 통해 사용자와 상호작용하는 인터페이스 제공</td>
</tr>
<tr>
<td><strong>Repository Server</strong></td>
<td>Git 저장소로부터 Manifest(YAML 파일 등)를 가져와 처리</td>
</tr>
<tr>
<td><strong>Application Controller</strong></td>
<td>클러스터의 상태와 Git 저장소의 상태를 비교하여 배포를 수행</td>
</tr>
<tr>
<td><strong>Redis</strong></td>
<td>캐싱과 세션 상태 관리를 위한 저장소</td>
</tr>
</tbody></table>
<p>각 구성 요소는 다음과 같은 방식으로 상호작용합니다.</p>
<ol>
<li>사용자가 Git 저장소에 리소스를 선언적으로 정의합니다.</li>
<li>ArgoCD의 <strong>Repository Server</strong>는 Git 저장소에서 이 변경사항(Manifest)을 가져옵니다.</li>
<li><strong>Application Controller</strong>는 Git 상태와 실제 쿠버네티스 클러스터 상태를 비교하여 배포 또는 상태 변경 작업을 수행합니다.</li>
<li>상태 정보는 <strong>Redis</strong>에서 빠르게 캐싱되어 성능을 높입니다.</li>
<li>사용자는 <strong>API Server</strong>를 통해 UI 및 CLI에서 배포 상태를 실시간으로 확인 가능합니다.</li>
</ol>
<h3 id="argocd-설치-및-접속">ArgoCD 설치 및 접속</h3>
<pre><code class="language-bash"># ArgoCD Namespace 생성
kubectl create ns argocd

# Helm Repo 등록
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update

helm install argocd argo/argo-cd --set server.service.type=NodePort --namespace argocd

# ArgoCD 서비스 접근을 위한 노드포트 변경
kubectl patch svc argocd-server -n argocd \
  -p &#39;{&quot;spec&quot;: {&quot;ports&quot;: [{&quot;port&quot;: 443, &quot;targetPort&quot;: 8080, &quot;nodePort&quot;: 31001}]}}&#39;

# 리소스 확인
kubectl get all -n argocd

# 초기 비밀번호 확인
kubectl patch svc argocd-server -n argocd \
  -p &#39;{&quot;spec&quot;: {&quot;ports&quot;: [{&quot;port&quot;: 443, &quot;targetPort&quot;: 8080, &quot;nodePort&quot;: 31001}]}}&#39;
</code></pre>
<h3 id="argocd-best-practice">ArgoCD Best Practice</h3>
<ul>
<li>GitOps의 핵심은 Git 저장소를 <strong>Single Source of Truth</strong>로 사용하는 것입니다.</li>
<li>선언적 정의를 통해 모든 환경(dev, staging, production)을 통일된 방식으로 관리하는 것이 중요합니다.</li>
<li>모든 배포 변경사항을 Git을 통해 관리하여 추적성, 감사성, 그리고 롤백 가능성을 극대화합니다.</li>
<li>ArgoCD의 자동 동기화 기능을 활성화하여 배포의 안정성을 높이는 것이 추천됩니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Backend For Frontend (a.k.a.  BFF)]]></title>
            <link>https://velog.io/@jong-kyung/Backend-For-Frontend-a.k.a.-BFF</link>
            <guid>https://velog.io/@jong-kyung/Backend-For-Frontend-a.k.a.-BFF</guid>
            <pubDate>Sun, 04 May 2025 03:50:03 GMT</pubDate>
            <description><![CDATA[<h2 id="bff의-개념과-등장-배경">BFF의 개념과 등장 배경</h2>
<h3 id="개념">개념</h3>
<p><strong>Backend For Frontend(BFF)</strong>는 말 그대로 <strong>프론트엔드를 위한 백엔드</strong>를 의미합니다. 하나의 공용 API 서버 대신 <strong>각각의 클라이언트 환경에 특화된 별도의 백엔드 서비스(API)를 두는 아키텍처 패턴</strong>입니다. BFF는 프론트엔드와 백엔드 서비스들 사이에 위치한 <strong>중간 계층</strong>으로, 해당 UI에 필요한 데이터와 기능만을 모아 <strong>맞춤형 API</strong>를 제공합니다. 예를 들어 모바일 앱과 웹 사이트 각각에 그들의 요구에 최적화된 <strong>전용 백엔드</strong>(BFF)가 존재하고, 이 BFF가 내부의 여러 서비스들과 통신하여 클라이언트에 꼭 맞는 응답을 만들어줍니다. 이를 통해 프론트엔드 개발자는 불필요한 데이터나 로직 처리 없이 <strong>필요한 것만</strong> 전달받고, 백엔드 개발자는 <strong>UI 요구 사항에 따라 API를 빠르게 수정</strong>하거나 확장할 수 있어 협업 효율이 높아집니다</p>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/592fb584-fb6d-4837-a7b6-1e041ddf52f8/image.png" alt="bff"></p>
<h3 id="등장-배경">등장 배경</h3>
<p>BFF 패턴이 등장하게 된 배경에는 <strong>클라이언트 종류의 다양화</strong>와 <strong>마이크로서비스 아키텍처</strong>의 확산이 있습니다. 과거에는 웹/모바일 등 여러 클라이언트를 하나의 <strong>범용 백엔드 API</strong>로 처리하는 접근이 흔했습니다. 그러나 모바일 환경은 화면 크기, 네트워크 제약 등이 데스크톱 웹과 달라 동일한 API로 두 종류를 모두 만족시키기 어렵다는 문제가 곧 드러났습니다. 실제로 모바일 사용자는 더 적은 데이터 전송과 신속한 응답을 원하지만, 데스크톱 웹 앱은 비교적 많은 정보를 한 번에 받아볼 수 있습니다. 하나의 API로 <strong>모바일과 웹 양쪽의 요구사항을 절충</strong>해야 하면 어느 쪽에도 완벽히 최적화되지 못하는 문제가 발생합니다.</p>
<p>한편, <strong>모놀리식</strong> 백엔드를 <strong>마이크로서비스(MSA)</strong>로 전환하면서 프론트엔드에서는 한 화면을 완성하기 위해 <strong>여러 서비스로부터 데이터를 호출</strong>하고 직접 조합하는 부담이 커졌습니다. 다시 말해 백엔드가 잘게 분리될수록 그 <strong>복잡성</strong>이 프론트엔드로 전가되는 양상이 나타난 것입니다. 원래 훌륭한 API 디자인 원칙 중 하나는 복잡성은 API 제공자가 흡수하고, 소비자인 프론트엔드는 단순하게 유지한다”는 것인데, 마이크로서비스 환경에서는 이 원칙이 지켜지기 어려워졌습니다.</p>
<p>이러한 문제를 해결하기 위해 <strong>클라이언트와 백엔드 서비스들 사이에 새로운 계층</strong>을 추가하는 아이디어가 나왔습니다.</p>
<p><strong>2013년경 SoundCloud</strong>에서는 하나의 공용 API로 모든 클라이언트(사내 제품 및 서드파티)를 지원하던 방식을 버리고, 클라이언트별 전용 API 서비스를 도입하기 시작했는데 이를 <strong>“Backends for Frontends (BFF)”</strong>라 명명했습니다. BFF를 도입하면 모바일과 웹 각각 <strong>자신만의 백엔드(BFF)</strong>를 통해 필요한 데이터만 제공받을 수 있으므로, 앞서 언급한 클라이언트별 차이를 효과적으로 관리할 수 있게 되었습니다.</p>
<p>다음 그림은 BFF 패턴을 적용한 <strong>전용 API</strong> 구조를 보여줍니다. 모바일 앱과 데스크톱 웹에 각기 <strong>별도의 BFF 서버</strong>를 두고, 각 BFF가 자신이 담당하는 클라이언트에 필요한 데이터만을 여러 내부 서비스에서 취합하여 제공합니다. 이렇게 <strong>클라이언트별로 백엔드가 분리</strong>되면 모바일과 웹이 서로 영향을 주지 않고 각자 최적화된 API를 가질 수 있어 유연성이 높아집니다.</p>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/adf7b0fd-cacf-4330-8b68-b8167ba0783b/image.png" alt="bff structure"></p>
<hr>
<h2 id="bff-목적">BFF 목적</h2>
<ol>
<li><strong>클라이언트 맞춤형 데이터 제공</strong><ul>
<li>클라이언트가 필요한 데이터만 전달함으로써 성능 개선</li>
<li>클라이언트에서의 복잡한 데이터 처리 로직 감소</li>
</ul>
</li>
<li><strong>클라이언트와 백엔드의 결합도 감소</strong><ul>
<li>백엔드 서비스의 변경사항으로부터 클라이언트 보호</li>
<li>각 프론트엔드에서 필요한 API를 독립적으로 관리 가능</li>
</ul>
</li>
<li><strong>성능 및 효율성 향상</strong><ul>
<li>여러 API 호출을 하나의 BFF 계층에서 병합하여 네트워크 비용을 절약</li>
<li>캐싱, 데이터 가공, 인증 및 권한 관리 등의 공통 작업을 처리하여 프론트엔드의 부담 완화</li>
</ul>
</li>
<li><strong>독립적 개발 및 배포</strong><ul>
<li>각 프론트엔드 전용 BFF를 통해 독립적인 배포 사이클 제공</li>
<li>서비스의 배포 주기 단축, 지속적 배포(CD) 활성화 가능</li>
</ul>
</li>
</ol>
<hr>
<h2 id="bff-구조">BFF 구조</h2>
<p>BFF는 시스템 전체 구조에서 <strong>프론트엔드와 내부 백엔드 서비스들 사이에 위치</strong>합니다. 클라이언트(프론트엔드)가 BFF에 요청을 보내면, BFF는 내부의 여러 서비스들과 통신하여 <strong>데이터를 취합 및 가공한 후, 클라이언트에 꼭 맞는 형태로 응답을 반환</strong>합니다. 이러한 BFF 서버는 보안상 <strong>내부 네트워크</strong> 안에서 동작하는 서버이며, 프론트엔드 팀이 직접 관리하는 경우가 많습니다. 이를 통해 프론트엔드 요구사항 변화에 따른 API 수정이 수월해지고, <strong>프론트엔드 앱과 BFF를 동일한 주기로 배포</strong>하는 것도 가능합니다.</p>
<h3 id="bff-구조-종류"><strong>BFF 구조 종류</strong></h3>
<ol>
<li>다중 클라이언트 ↔ BFF ↔ MSA Server</li>
<li>다중 클라이언트 ↔ 클라이언트별 BFF ↔ MSA Server</li>
<li>클라이언트 ↔ 기능별 BFF ↔ MSA Server<ul>
<li>네이버를 예를 들면, Auth BFF, Blog BFF, Pay BFF 등</li>
</ul>
</li>
</ol>
<h3 id="bff-동작-흐름">BFF 동작 흐름</h3>
<p>일반적인 BFF 동작 흐름은 아래와 같습니다.</p>
<ol>
<li>프론트엔드에서 <strong>BFF의 API 엔드포인트</strong>를 호출합니다. 이때 BFF는 REST API 형태이든 GraphQL API 형태이든 상관 없으며, 클라이언트가 필요한 데이터를 요청합니다.</li>
<li>BFF 서버가 요청을 수신하면, 관련된 백엔드 마이크로서비스들(또는 기존 모놀리식 서버)의 API를 <strong>필요한 만큼 호출</strong>합니다.</li>
<li>BFF는 각 서비스로부터 받은 <strong>데이터를 취합하고 변환하여 프론트엔드가 필요로 하는 형태로 가공</strong>합니다. 불필요한 필드는 제거하고, 여러 서비스의 응답을 하나로 합치거나, 프론트엔드에서 쓰기 좋도록 포멧을 변경하는 작업을 합니다.</li>
<li>최종적으로 BFF는 <strong>가공된 맞춤 응답 데이터를 클라이언트에 반환</strong>합니다. 프론트엔드는 하나의 응답으로 필요한 정보를 모두 얻을 수 있고, 추가적인 데이터 조합 로직을 프론트엔드에 구현할 필요가 없어집니다.</li>
</ol>
<p>프론트엔드는 BFF를 마치 백엔드처럼 투명하게 호출하고, BFF는 <strong>백엔드들의 복잡한 세부사항을 감추는 인터페이스</strong>로 동작합니다. 예를 들어, 인증/인가가 필요한 경우 BFF에서 토큰을 검사한 뒤 내부 서비스 호출에 포함시키고, 오류가 발생하면 BFF가 이를 적절히 처리하여 <strong>프론트엔드에 이해하기 쉬운 오류 메시지</strong>로 변환하는 식입니다. 요약하면, <strong>BFF는 프론트엔드 친화적인 API 게이트웨이 역할</strong>을 하며, <strong>데이터 집계, 포맷 변환, 인증 처리</strong> 등을 전담합니다.</p>
<h2 id="bff의-장단점">BFF의 장단점</h2>
<p>BFF 패턴을 도입하면 얻을 수 있는 주요 이점은 다음과 같습니다.</p>
<h3 id="장점">장점</h3>
<ul>
<li><strong>클라이언트 맞춤형 API 제공</strong><ul>
<li>각 프론트엔드에 특화된 API를 설계하여 <strong>오버페칭/언더페칭을 줄이고 불필요한 데이터 전송이나 다중 호출을 최소화</strong>할 수 있습니다. 그 결과 API 호출 횟수가 감소하고 응답 지연이 줄어 UX를 향상할 수 있습니다.</li>
</ul>
</li>
<li><strong>프론트엔드와 백엔드의 분리</strong><ul>
<li>BFF 레이어가 양측을 분리시켜 줌으로써, <strong>백엔드 서비스 교체나 변경이 프론트엔드에 영향을 덜 주게 됩니다. 각 클라이언트 전용 API가 있으므로 특정 클라이언트를 위한 수정이 다른 클라이언트에 파급되지 않습니다. 또한 프론트엔드 팀이 요구에 맞게 BFF를 직접 조정할 수 있어 개발 속도가 향상됩니다.</strong></li>
</ul>
</li>
<li><strong>디바이스 최적화</strong><ul>
<li>서로 다른 디바이스의 능력과 제약에 맞게 최적화된 응답을 제공할 수 있습니다. 예를 들어 모바일 BFF는 저용량 이미지나 압축된 데이터 등 <strong>데이터 효율</strong>을 우선하고, 데스크톱 BFF는 한 번에 고해상도 이미지와 상세한 정보를 추가적으로 전달하여 <strong>콘텐츠 제공</strong>에 초점을 맞출 수 있습니다.</li>
</ul>
</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li><strong>중복 구현 및 코드 중복</strong><ul>
<li>클라이언트별로 BFF를 여러 개 운영하면 <strong>비슷한 로직이 중복</strong>될 수 있습니다. 예를 들어 공통으로 필요한 <strong>데이터 가공이나 인증, 캐싱 로직</strong>을 각 BFF마다 따로 구현해야 할 수 있어 <strong>코드 중복과 일관성 관리</strong> 문제가 생길 수 있습니다.</li>
</ul>
</li>
<li><strong>리소스 및 운영 부담</strong><ul>
<li>클라이언트마다 별도 백엔드를 둔다면, 그만큼 <strong>서버 자원과 인프라</strong>가 더 필요합니다. 여러 BFF 서비스를 각각 유지하려면 <strong>호스팅 비용</strong>도 증가합니다.</li>
</ul>
</li>
<li><strong>과설계 위험</strong><ul>
<li>모든 프로젝트에 BFF가 정답은 아닙니다. <strong>클라이언트가 하나뿐이거나</strong> 백엔드가 이미 충분히 단순한 경우, BFF를 추가하면 <strong>복잡성만 늘어나고 실익은 적을</strong> 수 있습니다</li>
</ul>
</li>
</ul>
<hr>
<h2 id="bff-적용-사례">BFF 적용 사례</h2>
<p>BFF 패턴의 효과를 이해하기 위해 <strong>프론트엔드 코드 관점</strong>에서 모놀리식 구조와 BFF 구조를 비교해보겠습니다. 예를 들어, 어떤 서비스에서 사용자 프로필 화면에 최근 주문 내역을 함께 표시해야 한다고 가정해보겠습니다.</p>
<h3 id="모놀리식-구조">모놀리식 구조</h3>
<p>BFF가 없던 기존 구조에서는 프론트엔드가 필요한 정보를 각기 다른 서비스 API들로부터 직접 받아와 <strong>클라이언트 측에서 데이터를 조합</strong>해야 했습니다. 사용자 정보와 주문 정보를 한 화면에 보여주려면, 프론트엔드 코드에서 <strong>두 개 이상의 API를 호출</strong>하고 그 결과를 합쳐서 사용해야 합니다. 아래는 이러한 상황에서의 예시 코드입니다:</p>
<pre><code class="language-jsx">// BFF 없이 두 개의 API를 호출하여 데이터 통합
useEffect(() =&gt; {
  async function loadData() {
    // 1. 사용자 정보 API 호출
    const userRes = await fetch(&#39;/api/user/123&#39;);
    const userData = await userRes.json();
    // 2. 주문 목록 API 호출
    const ordersRes = await fetch(`/api/orders?userId=123`);
    const ordersData = await ordersRes.json();
    // 3. 데이터 합성: 사용자 객체에 최근 주문 목록 추가
    userData.recentOrders = ordersData.slice(0, 5);
    setUserProfile(userData);
  }
  loadData();
}, []);</code></pre>
<p>위 코드에서 프론트엔드는 사용자 정보(<code>/api/user/123</code>)와 주문 목록(<code>/api/orders?userId=123</code>) 두 엔드포인트를 순차적으로 호출한 뒤, 응답 받은 데이터를 자바스크립트 단에서 합쳐서 사용하고 있습니다. 이렇듯 BFF가 없는 경우 <strong>프론트엔드에 상당한 비즈니스 로직</strong>이 포함되며, 필요한 데이터가 늘어날수록 이러한 통합 작업도 복잡해집니다.</p>
<h3 id="bff-구조-1">BFF 구조</h3>
<p>BFF를 도입한 후에는 프론트엔드가 복잡한 로직을 수행할 필요 없이 <strong>단일 BFF 엔드포인트 호출</strong>로 통합된 데이터를 받을 수 있습니다. 위와 동일한 기능을 BFF 패턴을 사용하여 구현한 예시는 다음과 같습니다.</p>
<pre><code class="language-jsx">// BFF를 통해 하나의 엔드포인트에서 통합 데이터 받기
useEffect(() =&gt; {
  async function loadData() {
    // BFF 서버가 통합된 사용자+주문 데이터를 제공
    const res = await fetch(&#39;/api/bff/user-dashboard/123&#39;);
    const dashboardData = await res.json();
    setUserProfile(dashboardData);
  }
  loadData();
}, []);</code></pre>
<p>위 코드에서는 프론트엔드가 BFF에 해당하는 <strong>단일 엔드포인트</strong>(<code>/api/bff/user-dashboard/123</code>)만 호출하면 됩니다. <strong>BFF 서버</strong>가 내부적으로 사용자 서비스와 주문 서비스 등 여러 곳에서 데이터를 가져와 대시보드에 필요한 형태로 가공한 뒤 응답했기 때문에, 프론트엔드에서는 추가적인 처리 없이 결과를 바로 활용할 수 있습니다. 이처럼 BFF를 활용하면 프론트엔드 코드가 훨씬 <strong>단순</strong>해지고 본연의 화면 구성에 집중할 수 있게 됩니다.</p>
<hr>
<h2 id="bff-의의">BFF 의의</h2>
<p>BFF의 의의는 프론트엔드와 백엔드의 <strong>역할 분리 및 협업 향상</strong> 측면에서도 강조됩니다. 프론트엔드 팀은 자신들의 BFF를 통해 백엔드 구현 세부사항에 얽매이지 않고 사용자 경험 개선에 집중할 수 있고, 백엔드 팀은 코어 서비스 로직에 집중하면서도 각 클라이언트별 요구사항을 BFF 레이어를 통해 유연하게 수용할 수 있습니다. 그 결과 <strong>개발 효율성</strong>과 <strong>시스템 품질</strong> 모두 향상되는 효과를 얻습니다.</p>
<p>또한 BFF는 <strong>마이크로서비스</strong>로 분산된 백엔드 시스템과 최종 사용자 사이를 효율적으로 <strong>이어주는 일종의 다리 역할</strong>을 합니다. 백엔드가 여러 마이크로서비스로 쪼개어져 있더라도, 사용자에게는 BFF를 통해 하나로 통합된 일관된 인터페이스를 제공할 수 있으므로 분산 시스템의 복잡성을 숨기고 안정적인 서비스를 제공할 수 있습니다. 오늘날 데이터 통합 문제를 해결하기 위해 GraphQL 등의 기술이 각광받고 있지만, BFF 패턴은 <strong>팀 경계와 서비스 책임을 명확히 반영</strong>하면서도 비교적 단순한 방식으로 이러한 문제를 해결한다는 점에서 여전히 유용한 접근입니다. 실제로 <strong>API Gateway</strong>와 BFF를 조합하여 사용하거나, 요구에 따라 <strong>REST, gRPC, GraphQL</strong> 등 다양한 API 스타일을 BFF 레이어에 적용하는 등 발전적인 활용도 이루어지고 있습니다.</p>
<p>마지막으로, BFF를 운용할 때의 <strong>유의사항</strong>도 짚고 넘어가야 합니다. BFF에 지나치게 많은 역할과 로직을 몰아넣으면 여러 BFF 간에 중복된 비즈니스 기능이 구현되거나 유지보수가 어려워지는 문제가 생길 수 있습니다. 따라서 <strong>공통 비즈니스 로직은 기존의 마이크로서비스들이 담당</strong>하고, BFF는 클라이언트별 <strong>프레젠테이션 계층</strong>의 조율에 집중하는 경량화된 레이어로 유지하는 것이 바람직합니다. 이러한 균형을 잘 맞춘다면 BFF는 앞으로도 다양한 클라이언트 환경에서 <strong>일관된 사용자 경험</strong>과 <strong>효율적인 개발 프로세스</strong>를 실현하는 데 기여하는 중요한 아키텍처 패턴으로서 의의를 가질 것입니다.</p>
<blockquote>
<p><strong>참고</strong>
<a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/backends-for-frontends">MS - Backends for Frontends pattern</a>
<a href="https://aws.amazon.com/ko/blogs/mobile/backends-for-frontends-pattern">AWS- Backends for Frontends Pattern</a>
<a href="https://medium.com/@shikha.ritu17/backend-for-frontend-bff-architecture-8af101d7ea38">Backend-for-Frontend (BFF) Architecture</a>
<a href="https://medium.com/mobilepeople/backend-for-frontend-pattern-why-you-need-to-know-it-46f94ce420b0">Backend for frontend (BFF) pattern— why do you need to know it?</a>
<a href="https://developers.soundcloud.com/blog/service-architecture-1">Service Architecture at SoundCloud — Part 1: Backends for Frontends</a>
<a href="https://tech.kakaopay.com/post/bff_webflux_coroutine/">WebFlux와 코루틴으로 BFF(Backend For Frontend) 구현하기</a>
<a href="https://velog.io/@k-svelte-master/bff-api-pattern">API 수정 요청? 이제 그만! BFF로 프론트엔드에 책임 떠넘기기 🙅‍♂️💼</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes Availablity & Network]]></title>
            <link>https://velog.io/@jong-kyung/Kubernetes-Availablity-Network</link>
            <guid>https://velog.io/@jong-kyung/Kubernetes-Availablity-Network</guid>
            <pubDate>Sun, 04 May 2025 03:44:06 GMT</pubDate>
            <description><![CDATA[<h2 id="k8s-auto-scaling">K8s Auto Scaling</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/70ffc7d9-bf01-4f87-9b85-44f14a22aeaa/image.png" alt="k8s auto scaling"></p>
<ul>
<li><code>Metrics Server</code><ul>
<li>Kubernetes 에 내장된 확장 파이프라인을 위한 <strong>컨테이너 지표(CPU/메모리 사용량)</strong> 수집 서버</li>
<li>Kubelet 의 지표를 수집하고 노출하여 API Server 에 전달</li>
<li>HPA, VPA 같은 자동 확장 사용 목적 (모니터링 솔루션 X)</li>
</ul>
</li>
</ul>
<pre><code class="language-bash"># Metrics Server 설치
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

# Metrics Server SSL 무시
kubectl patch deployment metrics-server -n kube-system --type=json \
  -p=&#39;[{&quot;op&quot;: &quot;add&quot;, &quot;path&quot;: &quot;/spec/template/spec/containers/0/args/-&quot;, &quot;value&quot;: &quot;--kubelet-insecure-tls&quot;}]&#39;

# Metrics Server 배포 확인
kubectl get pods -n kube-system -l k8s-app=metrics-server

# 쿠버네티스 리소스 자원 사용량 확인
kubectl top node
kubectl top pods -A

# CPU, Memory 내림차순
kubectl top pods -A --sort-by=cpu
kubectl top pods -A --sort-by=memory</code></pre>
<ol>
<li>Metrics Server 설치
 <img src="https://velog.velcdn.com/images/jong-kyung/post/8d1a794b-8e57-4a89-b926-079b462c862a/image.png" alt="metrics server 설치"></li>
</ol>
<ol start="2">
<li>쿠버네티스 리소스 자원 사용량 확인
 <img src="https://velog.velcdn.com/images/jong-kyung/post/034d06a8-dec8-4a1d-b2f2-f9a4ed4b44b8/image.png" alt="자원 사용량 확인"></li>
</ol>
<h3 id="hpa-horizontal-pod-autoscaling">HPA (Horizontal Pod Autoscaling)</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/1153dd0b-93e5-4a85-8769-b081d807f1a8/image.png" alt="HPA"></p>
<ul>
<li><strong>역할</strong><ul>
<li>어플리케이션의 부하(Load)에 따라 Pod의 수를 늘리거나 줄이는 ‘수평 확장(Scale-Out/In)’</li>
</ul>
</li>
<li><strong>동작 원리</strong><ol>
<li>Metrics API(CPU, Memory 사용률 또는 사용자 정의 지표)를 주기적으로 조회</li>
<li>실제 사용량이 목표값(<code>targetCPUUtilizationPercentage</code> 등)을 넘으면 Pod 수를 늘리고, 밑돌면 줄임</li>
<li>컨트롤러가 <code>Deployment</code>(혹은 <code>ReplicaSet</code>)의 <code>replicas</code> 필드를 조정</li>
</ol>
</li>
<li><strong>장단점</strong><ul>
<li>장점: 짧은 처리량 급증 대응, 애플리케이션 가용성 유지</li>
<li>단점: 시작하는 새 Pod가 준비되기까지 약간의 지연(콜드 스타트)이 발생</li>
</ul>
</li>
<li><strong>예시 YAML</strong></li>
</ul>
<pre><code class="language-json">apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: hpa-sample
spec:
  scaleTargetRef:          # Scale 타겟 지정
    apiVersion: apps/v1
    kind: Deployment
    name: my-app           # Deployment 이름
  minReplicas: 2           # 최소 Pod
  maxReplicas: 10          # 최대 Pod
  metrics:                 # Scale 기준 지표 설정
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 50  # CPU 사용률 50% 기준
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 70  # 메모리 사용률 70% 기준</code></pre>
<ul>
<li>HPA 구성</li>
</ul>
<pre><code class="language-bash">cat &lt;&lt; EOF &gt;&gt; hpa-nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hpa-nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: hpa-nginx
  template:
    metadata:
      labels:
        app: hpa-nginx
    spec:
      containers:
      - name: hpa-nginx
        image: nginx
        resources:
          requests:
            cpu: 50m
          limits:
            cpu: 100m
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: hpa-nginx
  labels:
    app: hpa-nginx
spec:
  ports:
  - port: 80
  selector:
    app: hpa-nginx
EOF

# hpa-nginx.yaml 파일 확인
cat hpa-nginx.yaml 

# Deployment 배포
kubectl apply -f hpa-nginx.yaml

kubectl get deploy,pod

# HPA 생성
kubectl autoscale deployment hpa-nginx --cpu-percent=50 --min=1 --max=10

# HPA 확인
kubectl get hpa

# HPA 상세 정보 확인
kubectl describe hpa</code></pre>
<ol>
<li><p>HPA 생성
 <img src="https://velog.velcdn.com/images/jong-kyung/post/4b5daabe-c750-4f68-ab42-fd3756076659/image.png" alt="hpa 생성"></p>
</li>
<li><p>HPA 확인
<img src="https://velog.velcdn.com/images/jong-kyung/post/0b336935-f714-4caf-9395-2e8942279c5a/image.png" alt="hpa 확인"></p>
</li>
<li><p>HPA 상세 정보
 <img src="https://velog.velcdn.com/images/jong-kyung/post/304f9d25-e409-478e-ae42-2dd7f84a4ec2/image.png" alt="hpa 상세정보"></p>
</li>
</ol>
<h3 id="vpa-vertical-pod-autoscaling">VPA (Vertical Pod Autoscaling)</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/96f484ab-2266-41fe-9b1d-ea596f838844/image.png" alt="vpa"></p>
<ul>
<li><strong>역할</strong><ul>
<li>Pod의 CPU/메모리 요청량(request)과 제한(limit)을 <strong>자동으로 조정</strong>하는 ‘수직 확장(Scale-Up/Down)’</li>
</ul>
</li>
<li><strong>동작 원리</strong><ol>
<li>각 Pod의 실제 사용량을 관측(Recommendation)</li>
<li>수직 확장이 필요하다고 판단되면 새 리소스 요청량을 제안</li>
<li><code>UpdatePolicy</code>에 따라 Pod 재생성(RollingUpdate)</li>
</ol>
</li>
<li><strong>장단점</strong><ul>
<li>장점: 메모리 누수나 CPU 버스트처럼 단일 Pod 리소스 한계 문제 해소</li>
<li>단점: 요청량 조정 시 Pod 재시작이 필요 → 가끔 가동 중단(짧은 서비스 지연)</li>
</ul>
</li>
<li>주의사항<ul>
<li>하나의 Deployment에 HPA, VPA를 동시에 사용할 수 없음</li>
</ul>
</li>
<li><strong>예시 YAML</strong></li>
</ul>
<pre><code class="language-bash">apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: my-app-vpa
spec:
  targetRef:                   # Scale 대상
    apiVersion: apps/v1
    kind: Deployment
    name: my-app               # Deployment 명칭
  updatePolicy:
    updateMode: &quot;Auto&quot;         # VPA Recommender 에 의해 자동 조정 활성화
  resourcePolicy:
    containerPolicies:
      - containerName: my-app-container # Container 명칭 &quot;*&quot; 사용 가능
        minAllowed:            # 컨테이너가 할당받을 수 있는 최소 리소스
          cpu: &quot;200m&quot;
          memory: &quot;512Mi&quot;
        maxAllowed:            # 컨테이너가 할당받을 수 있는 최대 리소스
          cpu: &quot;2&quot;
          memory: &quot;2Gi&quot;</code></pre>
<ul>
<li>VPA 테스트</li>
</ul>
<pre><code class="language-bash"># EKS Workshop 소스 사용
git clone https://github.com/kubernetes/autoscaler.git

# VPA 배포
cd autoscaler/vertical-pod-autoscaler/
./hack/vpa-up.sh

# VPA Controller 확인
kubectl get pods -n kube-system | grep vpa

# VPA 제거
./hack/vpa-down.sh</code></pre>
<ul>
<li>VPA Controller 확인</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/be6be5bb-6877-4854-86dc-46882441c423/image.png" alt="vpa contoller"></p>
<h3 id="ca-cluster-autoscaler">CA (Cluster Autoscaler)</h3>
<ul>
<li><strong>역할</strong><ul>
<li>클라우드 제공자(IaaS)의 <strong>인스턴스(노드)</strong>를 자동으로 추가/삭제하여 클러스터 전체 용량을 조절</li>
</ul>
</li>
<li><strong>동작 원리</strong><ol>
<li>스케줄러가 Pod를 어디에도 배치할 수 없을 때 → 부족한 리소스를 감지</li>
<li>신규 노드를 프로비저닝 요청 → <strong>워커 노드</strong> 풀에 추가</li>
<li>사용량이 줄어 더 이상 필요한 노드가 없으면 → 노드 종료 및 축소</li>
</ol>
</li>
<li><strong>장단점</strong><ul>
<li>장점: 클러스터 전체 비용 최적화, 워크로드 급증 대응</li>
<li>단점: 신규 노드 프로비저닝 시간(수십 초~수 분)</li>
</ul>
</li>
<li><strong>설정 포인트</strong><ul>
<li>최소/최대 노드 수(<code>minSize</code>, <code>maxSize</code>)</li>
<li>Scale-Down Delay: 빈 노드를 곧바로 삭제하지 않고 대기 (<code>-scale-down-delay-after-add</code>)</li>
</ul>
</li>
</ul>
<hr>
<h2 id="어플리케이션-변수-관리">어플리케이션 변수 관리</h2>
<h3 id="configmap">ConfigMap</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/1c554c7c-0260-4d94-98f5-505a77ba9a68/image.png" alt="configmap"></p>
<ul>
<li><p><strong>목적</strong></p>
<ul>
<li>Kubernetes 애플리케이션의 <strong>구성 파일</strong>이나 <strong>환경 설정</strong>을 설정값을 <strong>key–value</strong> 쌍 형태로 저장 및 관리</li>
</ul>
</li>
<li><p><strong>특징</strong></p>
<ul>
<li>애플리케이션의 설정 정보를 외부에서 관리하고 Pod 와 컨테이너에서 참조</li>
<li>Base64 인코딩 없이 평문으로 저장</li>
<li>크기 제한: 최대 1MiB</li>
<li>Versioning 지원은 별도 없음(수정 시 새 리비전으로 교체)</li>
</ul>
</li>
<li><p>주요 사용 용도</p>
<ol>
<li>애플리케이션 설정 관리<ul>
<li>애플리케이션 구성 정보 (DB URL, 변수 등)을 ConfigMap 에 저장하여 Pod 환경 변수나 파일로 사용</li>
</ul>
</li>
<li>애플리케이션 환경에 맞는 설정 값 변경<ul>
<li>애플리케이션을 재빌드 하지 않고 설정 값 변경</li>
<li>DEV, STG, PRD 배포 환경에 따라 각각 <strong>다른 파일 구성</strong>으로 관리 목적</li>
</ul>
</li>
</ol>
</li>
<li><p>기본 구성</p>
<pre><code class="language-yaml">  apiVersion: v1
  kind: ConfigMap
  metadata:
    name: my-app-config
  data:
    LOG_LEVEL: &quot;debug&quot;
    API_ENDPOINT: &quot;https://api.example.com&quot;
</code></pre>
</li>
<li><p>Pod에 주입 방법</p>
<ul>
<li><p><strong>환경변수로 주입</strong></p>
<pre><code class="language-yaml">  apiVersion: v1
  kind: ConfigMap
  metadata:
    name: mysql
  data:
    DBNAME: mydatabase
  ---
  apiVersion: v1
  kind: Pod
  metadata:
    name: nginx-configmap
  spec:
    containers:
    - image: nginx
      name: nginx-configmap # 컨테이너에서 사용할 변수 Key 값
      env:
      - name: DB
        valueFrom:
          configMapKeyRef:
            name: mysql  # 사용할 ConfigMap의 이름
            key: DBNAME  # ConfigMap 내의 키 -&gt; 값: value1</code></pre>
<p>  위의 yaml 파일을 통해 배포하면 다음과 같이 DBNAME의 변수에 mydatabase가 적용되어 있음을 확인할 수 있습니다.</p>
<p>  <img src="https://velog.velcdn.com/images/jong-kyung/post/a194d157-b9f2-4f32-b81c-e5cb34b0c3fb/image.png" alt="configmap env"></p>
</li>
</ul>
</li>
</ul>
<pre><code>- **볼륨으로 마운트**

    ```yaml
    # config-deploy.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: nginx-configmap-deploy        # Deployment 리소스 이름
    spec:
      replicas: 2                         # 생성할 Pod 개수
      selector:
        matchLabels:
          app: nginx-configmap            # Pod 선택용 레이블 셀렉터
      template:
        metadata:
          labels:
            app: nginx-configmap          # Pod 템플릿에 붙일 레이블
        spec:
          containers:
          - name: nginx                   # 컨테이너 이름
            image: nginx                  # 사용할 컨테이너 이미지
            ports:
            - containerPort: 80           # 컨테이너 내 열어줄 포트
            volumeMounts:
            - name: config-volume         # 아래 volumes.name 과 매칭
              mountPath: /etc/nginx/nginx.conf  # 컨테이너 내부에 마운트할 경로
              subPath: nginx.conf          # 볼륨 내 nginx.conf 파일만 마운트
          volumes:
          - name: config-volume           # 컨테이너에 마운트할 볼륨 식별자
            configMap:
              name: nginx-config           # 참조할 ConfigMap 이름

    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: nginx-service                # Service 리소스 이름
    spec:
      type: NodePort                      # 외부 접근을 위한 NodePort 방식
      selector:
        app: nginx-configmap              # 서비스가 트래픽을 전달할 Pod 레이블
      ports:
        - protocol: TCP
          port: 80                        # 서비스 포트
          targetPort: 80                  # Pod 컨테이너 포트
          nodePort: 31001                 # 클러스터 노드의 고정 노드포트

    ```

    ```yaml
    # configmap.yaml
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: nginx-config         # ConfigMap 리소스 이름
    data:
      nginx.conf: |              # 키가 곧 파일명이 되고, 아래 내용이 파일 내용으로 매핑
        events {}
        http {
            server {
                listen 80;
                location / {
                    return 200 &#39;Hello from nginx configmap!&#39;; 
                }
            }
        }
    ```

    위 두 Yaml 파일을 배포한 후 확인해보면 다음과 같이 볼륨 마운트가 되어있음을 확인할 수 있습니다.

    ![configmap volume](https://velog.velcdn.com/images/jong-kyung/post/d59bc902-0f39-4c6f-b477-54430fe3a2e7/image.png)</code></pre><h3 id="secret">Secret</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/1ae3197b-3248-4990-8e0f-8cd947a7cd54/image.png" alt="secret"></p>
<ul>
<li><p><strong>목적</strong></p>
<ul>
<li>비밀번호, 토큰, 인증서 등 민감 정보(sensitive data)를 저장</li>
<li>ConfigMap과 동일한 방식으로 주입 가능</li>
</ul>
</li>
<li><p><strong>특징</strong></p>
<ul>
<li>기본 저장 시 Base64 인코딩</li>
<li>etcd에 암호화 옵션(Etcd Encryption at Rest) 설정 가능</li>
<li>크기 제한: 최대 1MiB</li>
<li>Role-Based Access Control(RBAC)으로 접근 제어 가능</li>
</ul>
</li>
<li><p>기본 구성</p>
<pre><code class="language-yaml">  apiVersion: v1
  kind: Secret
  metadata:
    name: my-secret
  type: Opaque
  data:
    username: bXl1c2Vy  # base64로 인코딩된 값
    password: bXlwYXNzd29yZA==  # base64로 인코딩된 값</code></pre>
<pre><code class="language-yaml">  apiVersion: v1
  kind: Pod
  metadata:
    name: my-app
  spec:
    containers:
      - name: my-container
        image: my-image
        env:
          - name: DB_USER        # Container 에서 사용할 변수명
            valueFrom:
              secretKeyRef:
                name: my-secret  # 사용할 Secret의 이름
                key: username    # Secret 내의 키
          - name: DB_PASSWORD
            valueFrom:
              secretKeyRef:
                name: my-secret  # 사용할 Secret의 이름
                key: password    # Secret 내의 키</code></pre>
</li>
<li><p><strong>Pod 에 주입 방법</strong></p>
<ul>
<li><p><strong>환경변수로 주입</strong></p>
<pre><code class="language-yaml">  env:
    - name: DB_PASSWORD
      valueFrom:
        secretKeyRef:
          name: my-app-secret
          key: DB_PASSWORD</code></pre>
</li>
<li><p>볼륨으로 마운트</p>
<pre><code class="language-yaml">  volumes:
    - name: secret-volume
      secret:
        secretName: my-app-secret
  containers:
    - name: app
      image: my-app:latest
      volumeMounts:
        - name: secret-volume
          mountPath: /etc/secret
          readOnly: true
</code></pre>
</li>
</ul>
</li>
<li><p>사용 예제</p>
<pre><code class="language-yaml">  # secret.yaml
  apiVersion: v1
  kind: Secret
  metadata:
    name: secret-test          # Secret 이름
  type: Opaque                 # 일반 문자열 타입
  data:
    # Base64 인코딩된 값
    username: YWRtaW4=         
    password: cGFzc3dvcmQ=     </code></pre>
<pre><code class="language-yaml">  # secretPod.yaml
  apiVersion: v1
  kind: Pod
  metadata:
    name: secret-pod           # Pod 이름
  spec:
    containers:
      - name: nginx            # 컨테이너 이름
        image: nginx           # 사용할 이미지
        env:
          - name: DB_USER
            valueFrom:
              secretKeyRef:
                name: secret-test   # 참조할 Secret 이름
                key: username       # Secret 데이터 키
          - name: DB_PASS
            valueFrom:
              secretKeyRef:
                name: secret-test   # 참조할 Secret 이름
                key: password       # Secret 데이터 키</code></pre>
<ul>
<li><p>위 yaml 파일을 배포하면 다음과 같이 secret이 적용된 것을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/5edfc78e-3a09-4247-98ae-0379e1f543b6/image.png" alt="secret deploy"></p>
</li>
</ul>
</li>
</ul>
<h3 id="configmap-vs-secret">ConfigMap vs Secret</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>ConfigMap</th>
<th>Secret</th>
</tr>
</thead>
<tbody><tr>
<td>저장 데이터</td>
<td>평문(plain text)</td>
<td>Base64 인코딩 (etcd 암호화 옵션 가능)</td>
</tr>
<tr>
<td>용도</td>
<td>비민감 설정값</td>
<td>비밀번호, 토큰, 인증서 등 민감 정보</td>
</tr>
<tr>
<td>접근 제어</td>
<td>네임스페이스 단위 RBAC 가능</td>
<td>네임스페이스 단위 RBAC + 암호화 가능</td>
</tr>
<tr>
<td>생성 커맨드</td>
<td><code>kubectl create configmap</code></td>
<td><code>kubectl create secret</code></td>
</tr>
<tr>
<td>크기 제한</td>
<td>최대 1MiB</td>
<td>최대 1MiB</td>
</tr>
</tbody></table>
<hr>
<h2 id="k8s-network">K8s Network</h2>
<h3 id="service">Service</h3>
<ul>
<li>외부와 접하는 <strong>단일 엔드포인트</strong></li>
<li>서비스 뒷단의 애플리케이션으로 외부 트래픽을 전송</li>
</ul>
<h3 id="clusterip">ClusterIP</h3>
<ul>
<li><p><strong>역할</strong>: 클러스터 내부에서만 접근 가능한 가상 IP 제공</p>
</li>
<li><p><strong>특징</strong></p>
<ul>
<li>외부 노드에서 직접 접근 불가, 즉 <strong>클러스터 내부</strong>에서만 ClusterIP 로 접근 가능</li>
<li>클러스터 내부 서비스 간 통신(ex. 마이크로서비스 호출)에 사용</li>
<li>서비스 타입을 지정하지 않을 경우 <strong>기본값</strong></li>
</ul>
</li>
<li><p><strong>예시</strong></p>
<pre><code class="language-yaml">  # cluster-pod.yaml
  apiVersion: v1
  kind: Pod
  metadata:
    name: cluster-pod-1
    labels:
      app: cluster-pod
  spec:
    containers:
      - name: container
        image: traefik/whoami
  ---
  apiVersion: v1
  kind: Pod
  metadata:
    name: cluster-pod-2
    labels:
      app: cluster-pod
  spec:
    containers:
      - name: container
        image: traefik/whoami</code></pre>
<pre><code class="language-yaml">  # netshoot-pod.yaml
  apiVersion: v1
  kind: Pod
  metadata:
    name: netshoot-pod
  spec:
    containers:
      - name: netshoot-pod
        image: nicolaka/netshoot
        command: [&quot;tail&quot;]
        args: [&quot;-f&quot;, &quot;/dev/null&quot;]</code></pre>
<pre><code class="language-yaml">  # cluster-svc.yaml
  apiVersion: v1
  kind: Service
  metadata:
    name: cluster-svc
  spec:
    type: ClusterIP
    selector:
      app: cluster-pod
    ports:
      - name: cluster
        port: 8080
        targetPort: 80</code></pre>
<p>  위 yaml을 배포한 후 파드 대역, SVC 대역을 확인하면 다음과 같습니다.</p>
<p>  <img src="https://velog.velcdn.com/images/jong-kyung/post/bb7879a9-1adb-4fc0-8370-2c0f7d21fa86/image.png" alt="svc 대역폭"></p>
</li>
</ul>
<h3 id="nodeport">NodePort</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/a850f681-e9b8-4886-bf06-3420aa3defbc/image.png" alt="node port"></p>
<ul>
<li><p><strong>역할</strong>: 각 워커 노드의 <strong>고정 포트</strong>(30000–32767)로 Service 노출</p>
</li>
<li><p><strong>특징</strong></p>
<ul>
<li><code>NodeIP:NodePort</code> 로 외부(클러스터 외부)에서 접근 가능</li>
<li><strong>클러스터 외부</strong>에서 노드(Host) 의 IP:Port 를 통해 접근 가능</li>
<li>클라우드 LB 없이도 간단한 외부 노출 가능</li>
</ul>
</li>
<li><p><strong>예시</strong></p>
<pre><code class="language-yaml">  # nodeport-pod.yaml
  apiVersion: apps/v1
  kind: Deployment
  metadata:
    name: nodeport-deploy
  spec:
    replicas: 2
    selector:
      matchLabels:
        app: nodeport-deploy
    template:
      metadata:
        labels:
          app: nodeport-deploy
      spec:
        containers:
        - name: container
          image: traefik/whoami</code></pre>
<pre><code class="language-yaml">  # nodeport-svc.yaml
  apiVersion: v1
  kind: Service
  metadata:
    name: nodeport-svc
  spec:
    type: NodePort
    selector:
      app: nodeport-deploy
    ports:
      - name: nodeport-svc
        port: 80          # 서비스 포트 (클러스터 내부 사용)
        targetPort: 80    # 실제 컨테이너 포트
        nodePort: 31002   # 외부에서 접근할 포트</code></pre>
<p>  위 yaml을 배포하면 아래와 같이 엔드포인트를 확인할 수 있습니다.</p>
<p>  <img src="https://velog.velcdn.com/images/jong-kyung/post/1bab58b4-36b6-4dd1-9f9b-5975f0eb7abb/image.png" alt="nodeport deploy"></p>
</li>
</ul>
<h3 id="ingress">Ingress</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/a96a9bba-9cfb-4132-8965-6020514c9a2c/image.png" alt="ingress"></p>
<ul>
<li><p><strong>역할</strong></p>
<ul>
<li>클러스터 외부에서 퍼블릭 포트로 들어오는 트래픽을 모든 노드에 걸쳐 자동으로 분산·라우팅(Routing Mesh)</li>
<li>클러스터 외부에서 <strong>내부 서비스로 트래픽을 라우팅</strong> 하는 방법을 제공</li>
</ul>
</li>
<li><p><strong>특징</strong></p>
<ul>
<li><p>실제로 가장 많이 사용되는 방식</p>
</li>
<li><p>Ingress 를 사용하기 위해서는 Ingress Controller 가 필요</p>
</li>
<li><p>클러스터 내부 서비스 (ClusterIP, NodePort, LoadBalancer) 를 외부로 노출 (HTTP / HTTPS) - Web Proxy 역할</p>
</li>
<li><p><strong>호스트 기반 라우팅</strong></p>
<ul>
<li><p>호스트 이름 (도메인)을 기반으로 트래픽 라우팅 가능</p>
</li>
<li><p><code>api.example.com</code> / <code>www.example.com</code> 을 각각 다른 Service 리소스와 연결</p>
<pre><code class="language-yaml">rules:
- host: foo.example.com
  http: …  
- host: bar.example.com
  http: …  </code></pre>
</li>
</ul>
</li>
<li><p><strong>경로 기반 라우팅</strong></p>
<ul>
<li><p>요청 경로 기반으로 트래픽 라우팅 가능</p>
</li>
<li><p><code>/service1</code> , <code>/service2</code> 경로를 각각 다른 Service 리소스와 연결</p>
<pre><code class="language-yaml">- path: /api
  pathType: Prefix
  backend: { serviceName: api-svc, servicePort: 80 }
- path: /web
  pathType: Prefix
  backend: { serviceName: web-svc, servicePort: 80 }</code></pre>
</li>
</ul>
</li>
<li><p><strong>투명 로드밸런싱</strong></p>
<ul>
<li>클라이언트는 어느 노드로 들어가도 동일하게 서비스 이용</li>
</ul>
</li>
<li><p><strong>TLS 설정</strong></p>
<ul>
<li><p>TLS 인증서를 활용하여 HTTPS 구성 가능</p>
<pre><code class="language-yaml">tls:
- hosts:
  - example.com
  secretName: example-tls-secret</code></pre>
</li>
</ul>
</li>
<li><p>위 예시에서 <code>web</code> 서비스는 <code>ingress</code> 네트워크를 통해</p>
<p>  클러스터 내 모든 노드의 80번 포트를 리스닝하고,</p>
<p>  요청이 들어오면 실제 실행 중인 컨테이너로 자동 라우팅됩니다.</p>
</li>
</ul>
</li>
<li><p><strong>예시</strong></p>
<pre><code class="language-yaml">  apiVersion: networking.k8s.io/v1
  kind: Ingress
  metadata:
    name: example-ingress
    annotations:
      kubernetes.io/ingress.class: &quot;nginx&quot;
      nginx.ingress.kubernetes.io/rewrite-target: /$1
  spec:
    tls:
    - hosts:
      - example.com
      secretName: example-tls-secret
    rules:
    - host: example.com
      http:
        paths:
        - path: /foo(/|$)(.*)
          pathType: ImplementationSpecific
          backend:
            service:
              name: foo-service
              port:
                number: 80
        - path: /bar(/|$)(.*)
          pathType: ImplementationSpecific
          backend:
            service:
              name: bar-service
              port:
                number: 80
</code></pre>
</li>
<li><p>사용 예제</p>
<ol>
<li><p>Nginx Ingress Controller 설치</p>
<pre><code class="language-bash"># Nginx Ingress Controller 설치
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml

# Service 타입 변경
kubectl patch svc ingress-nginx-controller -n ingress-nginx -p \
&#39;{&quot;spec&quot;:{&quot;type&quot;:&quot;NodePort&quot;,&quot;ports&quot;:[{&quot;port&quot;:80,&quot;targetPort&quot;:80,&quot;nodePort&quot;:31000},{&quot;port&quot;:443,&quot;targetPort&quot;:443,&quot;nodePort&quot;:31001}]}}&#39;</code></pre>
<p>위 명령어를 통해 배포를 진행하면 다음과 같이 설치된 Ingress Controller를 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/44ad4e9c-cf97-474d-ae24-7625e7814a7c/image.png" alt="ingress controller"></p>
</li>
</ol>
</li>
</ul>
<pre><code>1. 서비스 생성

```yaml
apiVersion: v1
kind: Service
metadata:
  name: growth-service
spec:
  selector:
    app: growth
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: growth-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: growth
  template:
    metadata:
      labels:
        app: growth
    spec:
      containers:
        - name: nginx
          image: nginx
          ports:
            - containerPort: 80
          volumeMounts:
            - name: growth-html
              mountPath: /usr/share/nginx/html
      volumes:
        - name: growth-html
          configMap:
            name: growth-html
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: growth-html
data:
  index.html: |
    &lt;html&gt;
    &lt;body&gt;
      &lt;h1&gt;hello growth&lt;/h1&gt;
    &lt;/body&gt;
    &lt;/html&gt;
```

```yaml
apiVersion: v1
kind: Service
metadata:
  name: log-service
spec:
  selector:
    app: log
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: log-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: log
  template:
    metadata:
      labels:
        app: log
    spec:
      containers:
        - name: nginx
          image: nginx
          ports:
            - containerPort: 80
          volumeMounts:
            - name: log-html
              mountPath: /usr/share/nginx/html
      volumes:
        - name: log-html
          configMap:
            name: log-html
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: log-html
data:
  index.html: |
    &lt;html&gt;
    &lt;body&gt;
      &lt;h1&gt;hello log&lt;/h1&gt;
    &lt;/body&gt;
    &lt;/html&gt;

```

1. Ingress 배포

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: growth-log-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: / # /growth, /log 요청을 서비스로 전달할 때 접두사 제거. ex) /growth -&gt; growth-service &#39;/&#39;
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
      - path: /growth
        pathType: Prefix
        backend:
          service:
            name: growth-service
            port:
              number: 80
      - path: /log
        pathType: Prefix
        backend:
          service:
            name: log-service
            port:
              number: 80
```

위 yaml까지 배포한 후 확인해보면 ingress가 배포됨을 다음과 같이 확인할 수 있다.

![ingress deploy](https://velog.velcdn.com/images/jong-kyung/post/7cce4f0b-e8b4-4551-a10f-ffb814c20755/image.png)</code></pre><h2 id="k8s-storage">K8s Storage</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/56c629a5-c707-4b22-875d-38becc0033e7/image.png" alt="k8s storage"></p>
<h3 id="emptydir">emptyDir</h3>
<ul>
<li><strong>정의</strong><ul>
<li>Pod이 노드에 스케줄링될 때 생성되고, 해당 Pod이 삭제되면 함께 사라지는 임시 디렉터리입니다.</li>
</ul>
</li>
<li><strong>특징</strong><ul>
<li><strong>수명</strong>: Pod 생명 주기에 종속 (Pod 삭제 시 데이터 소멸)</li>
<li><strong>저장 매체</strong>: 기본은 노드 로컬 디스크(<code>emptyDir.medium: &quot;&quot;</code>), <code>medium: &quot;Memory&quot;</code>로 설정하면 tmpfs(메모리)에도 마운트 가능</li>
<li><strong>용도</strong>: 컨테이너 간 일시적 파일 공유, 캐시 저장, 임시 작업 디렉터리 등</li>
</ul>
</li>
</ul>
<h3 id="hostpath">hostPath</h3>
<ul>
<li><strong>정의</strong><ul>
<li><strong>호스트 노드</strong>의 특정 파일 또는 디렉터리를 그대로 Pod 안으로 마운트하는 방식입니다.</li>
</ul>
</li>
<li><strong>특징</strong><ul>
<li><strong>수명</strong>: Pod와 무관하게 호스트에 남아 있으며, 다른 Pod에서도 재사용 가능</li>
<li><strong>포터블성</strong>: 노드마다 경로 구조가 달라질 수 있어 클러스터 간 이식성(Portability)이 낮음</li>
<li><strong>용도</strong>: 호스트 로그 수집, 노드별 커스텀 설정 읽기, 디버깅용 쉘 접근 등</li>
</ul>
</li>
</ul>
<h3 id="persistent-volume-pv">Persistent Volume (PV)</h3>
<ul>
<li><strong>정의</strong><ul>
<li>클러스터 관리자가 미리 프로비저닝하거나, 동적 프로비저닝(StorageClass)으로 생성되는 <strong>클러스터 전체의 공유</strong> <strong>영구 스토리지</strong> 리소스입니다.</li>
</ul>
</li>
<li><strong>핵심 개념</strong><ul>
<li><strong>PersistentVolume (PV)</strong>: 물리적 스토리지(예: NFS, GCE PD, AWS EBS 등)에 대한 클러스터 레벨 추상</li>
<li><strong>PersistentVolumeClaim (PVC)</strong>: 사용자가 필요 용량·접근 모드·StorageClass를 선언하면, PVC에 바인딩되어 PV를 할당받음</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[쿠버네티스 소개 및 기본 명령어]]></title>
            <link>https://velog.io/@jong-kyung/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%86%8C%EA%B0%9C-%EB%B0%8F-%EA%B8%B0%EB%B3%B8-%EB%AA%85%EB%A0%B9%EC%96%B4</link>
            <guid>https://velog.io/@jong-kyung/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%86%8C%EA%B0%9C-%EB%B0%8F-%EA%B8%B0%EB%B3%B8-%EB%AA%85%EB%A0%B9%EC%96%B4</guid>
            <pubDate>Wed, 23 Apr 2025 11:38:52 GMT</pubDate>
            <description><![CDATA[<h2 id="kubernetes-소개">Kubernetes 소개</h2>
<p>쿠버네티스(Kubernetes, 줄여서 K8s)는 <strong>컨테이너화된 애플리케이션을 배포, 관리, 확장하는 데 필요한 대부분의 수동 프로세스를 <code>자동화</code>하는 오픈소스 컨테이너 오케스트레이션 플랫폼</strong>입니다. 구글이 내부로 사용하던 <a href="https://research.google/pubs/large-scale-cluster-management-at-google-with-borg/">Borg</a>에서 발전하여 2015년에 공개한 이후로 사실상 표준 컨테이너 오케스트레이션 도구로 사용되고 있습니다.</p>
<blockquote>
<p>쿠버네티스는 그리스어로 &quot;조타수&quot; 또는 &quot;항해사&quot;를 의미하는 Kybernetes에서 유래한 것처럼, 쿠버네티스는 컨테이너라는 바다에서 애플리케이션이라는 배를 안전하게 운항하도록 도와주는 역할을 합니다.
이에 쿠버네티스에서 사용되는 많은 도구들 또한 바다와 관련된 용어가 아이콘이 많습니다.</p>
</blockquote>
<p>쿠버네티스는 애플리케이션의 코드, 구성 및 종속성을 번들링하는 컨테이너를 관리하기 위한 플랫폼으로, 자체 리소스를 사용하여 격리된 프로세스로 실행될 수 있습니다. 애플리케이션마다 쿠버네티스 파드(Pod)로 분류되는 컨테이너가 하나 또는 다수 제공됩니다.</p>
<h3 id="kubernetes-이점">Kubernetes 이점</h3>
<ul>
<li><strong>서비스 디스커버리와 로드 밸런싱 :</strong> 쿠버네티스 <strong>서비스</strong>를 통해 컨테이너로 이루어진 애플리케이션을 DNS 이름이나 고유 IP로 노출하고, 다수의 인스턴스로 구성된 경우 자동으로 <strong>네트워크 트래픽을 로드밸런싱</strong>하여 안정적인 서비스 제공이 가능합니다</li>
<li><strong>자동화된 배포 및 롤백 :</strong> 선언적인 설정(YAML)만 하면 쿠버네티스가 <strong>애플리케이션의 원하는 상태(예: 신규 버전 이미지로 업데이트)를 자동 롤아웃</strong>하며, 문제가 발생하면 이전 버전으로 <strong>자동 롤백</strong>할 수도 있습니다</li>
<li><strong>자동화된 빈 패킹(bin packing):</strong> 쿠버네티스에 컨테이너가 필요로 하는 CPU/메모리 자원을 명시하면, 클러스터 내 노드들에 작업을 최적으로 배치하여 <strong>자원 활용률을 극대화</strong>합니다</li>
<li><strong>자동화된 복구(Self-healing) :</strong> 죽은 컨테이너를 감지하여 <strong>자동으로 재시작하거나 교체</strong>하고, 준비되지 않은 컨테이너에는 트래픽을 보내지 않는 등 <strong>자동 복구</strong> 기능을 제공합니다</li>
<li><strong>시크릿과 구성 관리</strong> 쿠버네티스를 사용하면 암호, OAuth 토큰 및 SSH 키와 같은 중요한 정보를 저장하고 관리할 수 있다. 컨테이너 이미지를 재구성하지 않고 스택 구성에 시크릿을 노출하지 않고도 시크릿 및 애플리케이션 구성을 배포 및 업데이트할 수 있습니다.</li>
</ul>
<h2 id="kubernetes-등장-배경">Kubernetes 등장 배경</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/250c6279-6d25-4ec9-9a56-38fef9303e7b/image.png" alt="배포의 발전 과정"></p>
<h3 id="전통적인-배포-방식">전통적인 배포 방식</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/ab58d354-b321-45ef-b398-e5975dd953df/image.png" alt="전통 배포"></p>
<p>가장 오래된 배포 방식은 <strong>물리적인 컴퓨터 한 대에 하나의 운영체제(OS)를 설치하고, 그 위에 여러 프로그램을 설치하는 방식</strong>이었습니다. 이 방식에는 다음과 같은 문제가 있었습니다.</p>
<ul>
<li>애플리케이션이 같은 컴퓨터 리소스를 공유하므로, 서로 영향을 주어 성능 이슈 발생</li>
<li>하드웨어 성능은 빠르게 발전했으나, 소프트웨어가 이를 따라가지 못해 서버의 성능을 활용하지 못 하는 비효율 발생</li>
</ul>
<h3 id="가상화-배포-방식">가상화 배포 방식</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/af1bd793-51b3-42b8-9cc9-5f59b72af675/image.png" alt="가상화 배포"></p>
<p>이러한 문제를 해결하기 위해 가상화 개념이 등장했습니다. <strong>가상머신(VM)을 기반으로 애플리케이션을 배포</strong>하는 방식입니다. 각 가상머신은 논리적으로 구분된 환경에서 실행되어 각기 다른 OS와 분리된 리소스를 가집니다.
그러나 VM이 많아질수록 성능과 실행 속도가 저하되고, 각 VM마다 게스트 OS를 설치해야 하는 부담이 있습니다.</p>
<h3 id="컨테이너-배포-방식">컨테이너 배포 방식</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/c454e78f-f4e5-43fe-8a30-cfc671f75516/image.png" alt="컨테이너 배포"></p>
<p><strong>VM의 한계를 극복하기 위해 컨테이너 기술</strong>이 발전했습니다. 컨테이너는 다음과 같은 특징을 가집니다.</p>
<ul>
<li>애플리케이션 간에 운영체제(OS)를 공유함으로써 리소스 효율성 증가</li>
<li>호스트 OS를 공유하므로 OS 설치 작업이 없어 설정이 빠르고 확장성이 좋음</li>
<li>VM보다 가벼워 마이그레이션, 백업, 전송이 빠름</li>
</ul>
<p>도커(Docker)는 이러한 컨테이너화 기술을 구현한 플랫폼으로, 애플리케이션 실행에 필요한 환경을 하나의 이미지로 모아 다양한 환경에서 일관되게 실행할 수 있게 했습니다.</p>
<h3 id="컨테이너-오케스트레이션의-필요성"><strong>컨테이너 오케스트레이션의 필요성</strong></h3>
<p>도커의 등장으로 컨테이너 기반 배포 방식이 보편화되었지만, 새로운 문제가 발생했습니다</p>
<ul>
<li>점점 더 많은 서비스가 컨테이너화되면서 관리해야 할 컨테이너와 서버 수가 급증</li>
<li>&quot;컨테이너를 어느 서버에 배포할지&quot;, &quot;리소스가 남는 서버는 어디인지&quot;, &quot;죽은 컨테이너를 다시 실행해야 하는&quot; 등의 수작업 증가</li>
</ul>
<p>이러한 배경에서 <strong>컨테이너 관리를 자동화할 도구의 필요성이 대두</strong>되었고, 이에 대한 해답으로 <strong>쿠버네티스가 등장</strong>하게 되었습니다.</p>
<h2 id="kubernetes-아키텍처">Kubernetes 아키텍처</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/56fb1310-84f7-4b23-b629-4cbe616765c6/image.png" alt="k8s 아키텍처"></p>
<p>쿠버네티스는 컨트롤 플레인 컴포넌트(마스터)와 노드 컴포넌트로 구성된 아키텍처를 가지고 있습니다.</p>
<h3 id="control-plane-components">Control Plane Components</h3>
<p>Kubernetes 관리 서버</p>
<ul>
<li><p>kube-apiserver</p>
<ul>
<li>Control Plane으로 전달되는 모든 요청을 받아 드리는 API 서버</li>
</ul>
</li>
<li><p>etcd</p>
<ul>
<li>key-value 스토어로, 클러스터 내 <strong>모든 메타정보 저장</strong></li>
<li>오직 API Server 와 통신, 다른 모듈은 API Server 를 통해 ETCD 접근</li>
</ul>
</li>
<li><p>kube-scheduler</p>
<ul>
<li>사용자의 요청에 따라 적절하게 컨테이너를 워커 노드에 배치하는 스케줄러</li>
<li>할당되지 않은 Pod 를 여러 조건 에 따라 적절한 Worker Node에 할당</li>
</ul>
</li>
<li><p>control-manager</p>
<ul>
<li><p><strong>현재 상태와 원하는 상태를 지속적으로 확인</strong>하며 특정 이벤트에 따라 특정 동작을 수행하는 컨트롤</p>
</li>
<li><p>쿠버네티스 리소스 상태 변화를 감지하고 클러스터에 적용</p>
</li>
<li><p>ETCD 의 Desired State 와 쿠버네티스의 Current State 비교</p>
</li>
<li><p><em>Kube-Controller-Manager*</em>
대부분의 쿠버네티스 오브젝트 상태 관리</p>
</li>
<li><p><em>Cloud-Controller-Manager*</em>
클라우드 플랫폼(AWS, GCP, Azure 등)에 특화된 리소스를 제어하는 클라우드 컨트롤러</p>
</li>
</ul>
</li>
</ul>
<h3 id="node-components">Node Components</h3>
<p>Control Plane과 통신하며 필요한 Pod와 볼륨을 생성하는 서버로, 실제 컨테이너가 동작합니다.</p>
<ul>
<li>kubelet<ul>
<li>Control Plane 명령에 따라 노드에 할당된 <strong>Pod 의 생명 주기(Life-Cycle)를 관리</strong></li>
<li>Pod 생성 / Node Pod에 이상이 없는 지 주기적으로 확인하면서 <strong>Control Plane (API Server) 에 현재 상태(Current State) 전달</strong></li>
<li>API Server 의 요청을 받아 Container Log 전달, 특정 명령 수행 등</li>
</ul>
</li>
<li>kube-proxy<ul>
<li>컨테이너의 네트워킹을 책임지는 프록시, 네트워크 규칙 유지 관리</li>
<li>Pod로 연결된 네트워크 관리 (Overlay Network)</li>
<li>Proxy 서버 → iptable 설정 방식 → IPVS 지원</li>
</ul>
</li>
<li>Container Runtime<ul>
<li>실제 컨테이너를 실행하는 컨테이너 실행 환경</li>
</ul>
</li>
</ul>
<h3 id="addons">Addons</h3>
<ul>
<li>CNI : Container Network Interface 는 k8s 네트워크 환경을 구성</li>
<li>DNS : 쿠버네티스 서비스를 위해 DNS 레코드를 제공해주며, Service Discovery 기능을 제공</li>
<li>CSI : Container Storage Interface</li>
</ul>
<h2 id="kubernetes-설치">Kubernetes 설치</h2>
<p>쿠버네티스의 배포 도구는 다양하게 존재하며, 운영환경/개발환경에 따라 선택하여 설치할 수 있습니다.</p>
<p>운영 환경에서는 주로 <code>kubeadm</code>, kubespray, kops가 사용되며, 개발 환경에서는 <code>minikube</code>, k3d, <code>kind</code>, getdeck, kwok이 사용됩니다.</p>
<h3 id="kind-설치">kind 설치</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/b1c759c0-56cc-4282-844c-bd33d1affb63/image.png" alt="kind"></p>
<p>kind(<strong>K</strong>ubernetes <strong>In</strong> <strong>D</strong>ocker)는 가볍고 빠르게 테스트하고 지울 수 있어 이를 사용하여 쿠버네티스 설치를 하겠습니다.</p>
<p>kind를 설치하기 전, 반드시 Docker가 설치되어 있어야합니다</p>
<p>설치는 MacOS를 기준으로 진행합니다.</p>
<pre><code class="language-bash"># Install Kind
**brew install kind**

# Install kubectl
**brew install kubernetes-cli**</code></pre>
<p>설치를 진행한 후 아래 명령어를 통해 설치가 정상적으로 되었는지 확인합니다.</p>
<pre><code class="language-bash">kind --version
kubectl version --client=true</code></pre>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/4a74870f-8384-4c77-b9f1-d95c946c2f6f/image.png" alt="kind 설치 확인"></p>
<p>kind 설치가 완료되었으면 아래 명령어를 통해 Cluster를 생성합니다.</p>
<pre><code class="language-bash"># Create a cluster with kind
**kind create cluster**</code></pre>
<p>정상적으로 Cluster가 생성되었는지 확인합니다.</p>
<pre><code class="language-bash"># 클러스터 배포 확인
kind get clusters # 현재 로컬 환경에 생성된 kind cluster 확인
kind get nodes # cluster 노드 확인
kubectl cluster-info # 현재 설정된 Kubernetes 클러스터의 기본 서비스 정보 확인

# 노드 정보 확인
kubectl get node -o wide # 현재 Kubernetes 클러스터의 노드 상태를 좀 더 상세하게(wide) 출력

# 파드 정보 확인
kubectl get pod -A # Kubernetes 클러스터 내의 모든 네임스페이스에서 실행 중인 모든 파드 목록을 조회</code></pre>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/12cce6f1-3f08-40a4-b718-718a7df76c4a/image.png" alt="cluster 생성 확인"></p>
<p>kind는 기본적으로 dokcer에 kubernetes 환경을 구성하는 도구로, 다음 명령어를 입력해보면 실제로 컨테이너가 배포되어 있는 것을 확인할 수 있습니다.</p>
<pre><code class="language-bash">docker ps
CONTAINER ID   IMAGE                  COMMAND                   CREATED          STATUS          PORTS                       NAMES
3850ec297c1a   kindest/node:v1.32.2   &quot;/usr/local/bin/entr…&quot;   31 minutes ago   Up 31 minutes   127.0.0.1:57230-&gt;6443/tcp   kind-control-plane</code></pre>
<h2 id="kubectl">Kubectl</h2>
<p><strong>Kubectl(kube</strong>rnetes + <strong>c</strong>on<strong>t</strong>ro<strong>l)</strong>은 쿠버네티스를 제어하는 명령을 API Server 로 전달합니다.</p>
<h3 id="kubernetes-정보-확인">Kubernetes 정보 확인</h3>
<p>아래 명령어를 통해 현재 쿠버네티스 클러스터에서 사용할 수 있는 <strong>API 리소스 목록</strong>을 출력하며, 리소스 약어를 확인할 수 있습니다.</p>
<pre><code class="language-bash">kubectl api-resources</code></pre>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/6a8c32cb-fb2b-429d-8c7e-9d22e8bc0882/image.png" alt="api 리소스 확인"></p>
<p><code>kubectl get &lt;RESOURCE&gt;</code> 를 통해 Kubernetes에서 <strong>리소스의 현재 상태를 조회</strong>할 수 있습니다.</p>
<pre><code class="language-bash"># 노드 조회
kubectl get node
kubectl get node -o wide

# 파드 조회
kubectl get pods
kubectl get pods -A

# 디플로이먼트 조회
kubectl get deployment
kubectl get deployment -A

# 네임스페이스 조회
kubectl get namespace</code></pre>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/1b4c6716-c48f-4716-bff2-2f026bd659d8/image.png" alt="리소스의 현재 상태 조회"></p>
<p><code>kubectl describe &lt;RESOURCE&gt; [NAME]</code> 를 통해 Kubernetes 리소스에 대한 <strong>자세한 설명과 상태 정보</strong>를 출력합니다.</p>
<pre><code class="language-bash"># 파드 상세 조회
kubectl describe pod -n kube-system coredns-668d6bf9bc-62k9k

# 디플로이먼트 조회
kubectl describe deployment -n kube-system coredns

# 네임스페이스 조회
kubectl describe namespace kube-system</code></pre>
<p><strong>Pod 상세 정보</strong></p>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/ab3c7a8f-fbcb-4f9a-b6b5-40d220025a1d/image.png" alt="Pod 상세 정보"></p>
<p><strong>Deployment 상세 정보</strong></p>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/e115139c-7ed9-4ac4-a22b-31c050808445/image.png" alt="deployment 상세 정보"></p>
<p><strong>Namespace 상세 정보</strong></p>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/b88ec7c6-81ea-4a58-a018-b135b71e20f4/image.png" alt="namespace 상세 정보"></p>
<h3 id="kubernetes-컨텍스트">Kubernetes <strong>컨텍스트</strong></h3>
<p><code>kubectl</code> 명령어를 실행할 때, <strong>어떤 클러스터에 연결할지</strong>, <strong>어떤 사용자 자격으로 접근할지</strong>, <strong>어떤 네임스페이스를 기본으로 사용할지</strong>를 지정하는 설정을 <strong>컨텍스트(Context)</strong>라고 합니다. </p>
<p>컨텍스트는 Kubernetes에 접근하기 위한 <strong>인증(Authentication)</strong> 과정의 일부로, 특히 <strong>여러 개의 클러스터(멀티 클러스터)를 운영할 때 매우 유용</strong>합니다.</p>
<table>
<thead>
<tr>
<th>구성 요소</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Cluster</td>
<td>연결할 Kubernetes 클러스터 정보</td>
</tr>
<tr>
<td>User</td>
<td>클러스터에 사용할 인증 방식 및 자격 증명</td>
</tr>
<tr>
<td>Namespace</td>
<td>기본적으로 사용할 네임스페이스</td>
</tr>
</tbody></table>
<p>컨텍스트는 아래 명령어를 통해 확인할 수 있습니다.</p>
<pre><code class="language-bash"># 현재 설정된 컨텍스트 확인
kubectl config current-context

# 모든 컨텍스트 확인
kubectl config get-contexts

# 다른 컨텍스트 전환
kubectl config use-context &lt;Context Name&gt;

# 컨텍스트 삭제
kubectl config delete-context &lt;Context Name&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/639853f1-02d8-49a1-8481-007bc5107aed/image.png" alt="컨텍스트 확인"></p>
<p>컨텍스트의 정보는 <code>~/.kube/config</code> 에서 확인할 수 있으며 이를 확인해보면 아래와 같은 구조로 되어있습니다.</p>
<pre><code class="language-yaml">apiVersion: v1
clusters:
- cluster: # API Server 정보
    certificate-authority-data: LS0tLS1CRUdJTiBDRVJUS# API Server 인증서
    server: https://127.0.0.1:57230 # API Server URL
  name: kind-kind # Cluster 명칭
contexts:
- context:
    cluster: kind-kind
    user: kind-kind   # Cluster 접근 유저
  name: kind-kind
current-context: kind-kind
kind: Config
preferences: {}
users:
- name: kind-kind # Cluster 접근 유저에 대한 
  user:
    client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS    # Client 인증서
    client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkF # Client Key</code></pre>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/c171f62e-07f9-4f2d-85a0-08235b2df5c4/image.png" alt="컨텍스트 정보 조회 결과"></p>
<h2 id="namespace">Namespace</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/5dd782d1-8186-44b2-9c9e-cf34023f1a18/image.png" alt="namespace"></p>
<p>Kubernetes에서 <strong>Namespace(네임스페이스)</strong>는 <strong>클러스터 내부를 논리적으로 분리하기</strong> 위한 단위로, 하나의 클러스터 안에서 <strong>리소스를 구분하고 효율적으로 관리할</strong> 수 있게 해줍니다. <code>kube-</code> 접두사는 Kubernetes 시스템에서 사용하기 위해 예약되어 있으므로, 해당 접두사로 네임스페이스를 생성할 수 없습니다.</p>
<p>또한, Kubernetes의 모든 객체(Object)가 네임스페이스에 속하는 것은 아니며, 일부 리소스는 클러스터 전체 범위에서 관리됩니다.</p>
<pre><code class="language-bash"># Namespace 에 속하는 리소스
kubectl api-resources --namespaced=true

# Namespace 에 속하지 않는 리소스
kubectl api-resources --namespaced=false</code></pre>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/23c40bf9-72b6-40b4-afda-2fd11fb30b00/image.png" alt="namespace 리소스 확인"></p>
<h3 id="namespace-활용"><strong>Namespace 활용</strong></h3>
<ol>
<li><p>네임 스페이스 생성</p>
<pre><code class="language-bash"> # 네임스페이스 조회
 kubectl get ns

 # 네임스페이스 생성
 kubectl create namespace k8s-test
 kubectl get ns</code></pre>
</li>
<li><p>파드 생성</p>
<pre><code class="language-bash"> # Default Namespace에 파드 생성
 kubectl run nginx --image=nginx:alpine

 kubectl get pods
 kubectl describe pod nginx
 kubectl describe pod nginx | grep Namespace

 # k8s-test 네임스페이스에 파드 생성
 kubectl run nginx --image=nginx:alpine -n k8s-test

 kubectl get pods -n k8s-test
 kubectl get pods -A

 # k8s-test 네임스페이스에 다시 한 번 파드 배포
 kubectl run nginx --image=nginx:alpine -n k8s-test
 Error from server (AlreadyExists): pods &quot;nginx&quot; already exists

 # 논리적으로 격리되어 있기 때문에 다른 네임스페이스에는 동일한 이름의 파드를 배포할 수 있지만,
 # 같은 네임스페이스에는 동일한 파드를 배포할 수 없습니다.</code></pre>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/46f25c29-5d3a-4d23-bd91-7a7a0ed1d50d/image.png" alt="네임스페이스 활용"></p>
<ol start="3">
<li><p>테스트 파드 삭제</p>
<pre><code class="language-bash"> # 테스트 파드 삭제
 kubectl delete pods nginx
 kubectl delete pods nginx -n k8s-test</code></pre>
</li>
</ol>
<h3 id="기본-namespace-변경"><strong>기본 Namespace 변경</strong></h3>
<pre><code class="language-bash"># 기본 네임스페이스 변경
## 현재 네임스페이스 정보 조회 (빈값: default)
kubectl config get-contexts
kubectl config view --minify
kubectl config view --minify --output &#39;jsonpath={..namespace}&#39;

## 네임스페이스 변경 
kubectl config set-context --current --namespace=k8s-test

kubectl config get-contexts
kubectl config view --minify
kubectl config view --minify --output &#39;jsonpath={..namespace}&#39;</code></pre>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/3c8dcc00-ec29-4ca2-89a2-e806ff2a74be/image.png" alt="기본 네임스페이스 변경"></p>
<h2 id="pod">Pod</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/ce8e9165-0cb9-4b5d-bda1-2791274b23c6/image.png" alt="pod"></p>
<p>Kubernetes에서 Pod(파드)는 <strong>가장 작은 배포 단위이자 실행 단위</strong>입니다. 하나 이상의 컨테이너로 구성되며, <strong>동일한 파드 내의 컨테이너들은 Storage와 Network를 공유</strong>합니다. 이들은 항상 함께 실행되며, <strong>Pod는 프로세스를 직접 실행하는 것이 아니라, 컨테이너를 실행하기 위한 환경</strong>을 제공합니다.</p>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/26474e1f-e924-462a-a648-72202849b3e8/image.png" alt="다양한 Pod 관리 방식"></p>
<p>일반적으로 Pod를 직접 생성하지 않고, <strong>Deployment와 같은 상위 리소스를 통해 생성하고 관리</strong>하는 것이 권장됩니다. Pod를 단독으로 사용하면 고가용성 및 자가 치유가 불가합니다. 따라서 아래와 같은 배포 방식을 사용합니다.</p>
<table>
<thead>
<tr>
<th>배포 방식</th>
<th>특징 설명</th>
<th>사용 사례</th>
</tr>
</thead>
<tbody><tr>
<td>Deployment</td>
<td>가장 일반적인 배포 방식, 다양한 배포 전략 사용 가능</td>
<td>웹 서버, API 서버 등</td>
</tr>
<tr>
<td>StatefulSets</td>
<td>파드의 실행 순서 보장, 고정된 네트워크 이름(Stable Hostname), 고정된 볼륨</td>
<td>DB, Kafka 등 상태 기반 서비스</td>
</tr>
<tr>
<td>DaemonSet</td>
<td>클러스터의 모든 노드에 하나씩 파드를 배포</td>
<td>로그 수집기, 모니터링 에이전트 등</td>
</tr>
<tr>
<td>Job / CronJob</td>
<td>일회성 또는 주기적인 배치 작업 수행</td>
<td>DB 백업, 배치 처리</td>
</tr>
</tbody></table>
<h3 id="pod-배포"><strong>Pod 배포</strong></h3>
<ol>
<li><p>pod.yaml 생성</p>
<pre><code class="language-yaml"> apiVersion: v1
 kind: Pod               # Pod 리소스 선언
 metadata:
   name: nginx           # Pod Name
 spec:
   containers:
   - name: nginx         # Container Name
     image: nginx:alpine # Container Image
     ports:              # Container Port
     - containerPort: 80</code></pre>
</li>
<li><p>pod 배포</p>
<pre><code class="language-bash"> # 현재 pod 상태 확인
 kubectl get pods -A

 # pod 배포
 kubectl apply -f pod.yaml

 kubectl get pods
 kubectl describe pods nginx

 # 삭제
 kubectl delete -f pod.yaml</code></pre>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/e7a700e4-148a-4479-bb68-253e49cde7ee/image.png" alt="yaml 이용 pod 배포"></p>
<ol start="3">
<li><p>Pod 로그 확인</p>
<pre><code class="language-yaml"> # 로그 조회
 kubectl logs nginx
 kubectl logs nginx -f # f : 로그 스트리밍 모드</code></pre>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/31ef87fc-bdb8-43af-b573-8295b4bf06ba/image.png" alt="pod 로그 확인"></p>
<h3 id="컨테이너-여러개인-pod-배포"><strong>컨테이너 여러개인 Pod 배포</strong></h3>
<ol>
<li><p>multi.yaml 생성</p>
<pre><code class="language-yaml"> apiVersion: v1
 kind: Pod
 metadata:
   name: k8s-multi-pods
 spec:
   containers:
   - name: nginx
     image: nginx
   - name: redis
     image: redis</code></pre>
</li>
<li><p>Pod 배포</p>
<pre><code class="language-bash"> # pod 배포
 kubectl apply -f multi.yaml

 kubectl get pods
 kubectl describe pods k8s-multi-pods</code></pre>
<p> 컨테이너가 여러개인 경우, <code>컨테이너 갯수 / 원하는 컨테이너 갯수</code> 로 아래와 같이 표시된다.</p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/cdac6794-6ebe-454a-a4fb-e8db96b23950/image.png" alt="state 표시"></p>
<p><strong>사이드카 패턴</strong></p>
<ol>
<li><p>sidecar.yaml 생성</p>
<pre><code class="language-yaml"> apiVersion: v1
 kind: Pod
 metadata:
   name: k8s-mart
 spec:
   volumes:
   - emptyDir: {}
     name: varlog
   containers:
   - name: k8s-mart
     image: busybox
     command:
     - /bin/sh
     - -c
     - &#39;i=1; while :;do echo -e &quot;$i: Price: $((RANDOM % 10000 + 1))&quot; &gt;&gt; /var/log/k8s-mart.log; i=$((i+1)); sleep 2; done&#39;
     volumeMounts:
     - mountPath: /var/log
       name: varlog
   - name: price
     image: busybox
     args: [/bin/sh, &quot;-c&quot;, &#39;tail -n+1 -f /var/log/k8s-mart.log&#39;]
     volumeMounts:
     - mountPath: /var/log
       name: varlog</code></pre>
</li>
<li><p>Pod 배포</p>
<pre><code class="language-bash"> # pod 배포
 kubectl apply -f sidecar.yaml

 kubectl get pods
 kubectl describe pods k8s-mart

 # price 컨테이너 로그 조회
 kubectl logs k8s-mart -c price -f # k8s-mart Pod에서 price라는 컨테이너의 로그를 출력</code></pre>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/9955c24e-e7ab-46e4-a985-994bff1e7201/image.png" alt="sidecar 로그 확인"></p>
<p><strong>배포된 Pod 삭제</strong></p>
<pre><code class="language-bash">kubectl delete -f &lt;file.yaml&gt;</code></pre>
<h2 id="deployment">Deployment</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/30d6c5d2-5faf-4d6d-8d64-b6ef35e13efb/image.png" alt="deployment"></p>
<p><strong>Deployment</strong>는 Kubernetes에서 <strong>파드와 레플리카셋을 선언적으로 업데이트</strong>할 수 있는 리소스로, 파드 배포에 있어 핵심적인 역할을 합니다.</p>
<p>Deployment는 내부적으로 <strong>ReplicaSet</strong>을 생성하고 관리하며, 이를 통해 명시된 <strong>파드 개수의 가용성을 자동으로 보장</strong>합니다. (예: 파드 2개 또는 3개 유지)</p>
<p>단, <strong>Deployment가 관리하는 ReplicaSet은 직접 수정하지 않는 것이 원칙</strong>입니다.</p>
<p>또한 Deployment는 <strong>롤링 업데이트, 롤백</strong> 등 다양한 배포 전략을 설정할 수 있어, 중단 없는 안정적인 배포를 지원합니다.</p>
<p>Deployment의 사양에는 실제로 배포될 파드의 정의를 담고 있는 <strong>PodTemplate</strong>이 포함되어 있으며, 이를 통해 어떤 컨테이너를 어떤 환경에서 실행할지를 선언합니다.</p>
<h3 id="deployment-사용"><strong>Deployment 사용</strong></h3>
<ol>
<li><p>deploy.yaml 생성</p>
<pre><code class="language-yaml"> apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: nginx-v1
 spec:
   selector:
     matchLabels:            # PodTemplate 에 선언된 Label 을 지정하여 Deployment 적용
       app: nginx
   replicas: 2               # Pod 개수
   template:                 # PodTemplate 시작
     metadata:
       labels:
         app: nginx
     spec:
       containers:
       - name: nginx
         image: nginx:latest
         env: # 환경변수 설정 key-value
         - name: APP_MODE
           value: &quot;v1&quot;
         ports:
         - containerPort: 80 # PodTemplate 종료</code></pre>
</li>
<li><p>Pod 배포</p>
<pre><code class="language-bash"> # pod 배포
 kubectl apply -f deploy.yaml

 # Pod, ReplicaSet, Deployment 조회
 kubectl get deploy,rs,pods</code></pre>
<p> 아래처럼 <strong>Deployment → ReplicaSet → Pod</strong> 순으로 출력되어, <strong>배포 구조 전체를 한눈에 파악</strong>할 수 있습니다.</p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/6845bf5a-d159-4f1d-b0f2-e9f58d084cee/image.png" alt="replicaset 배포"></p>
<ol start="3">
<li><p>Pod 업데이트</p>
<pre><code class="language-bash"> apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: nginx-v1
 spec:
   selector:
     matchLabels:            # PodTemplate 에 선언된 Label 을 지정하여 Deployment 적용
       app: nginx
   replicas: 2               # Pod 개수
   template:                 # PodTemplate 시작
     metadata:
       labels:
         app: nginx
     spec:
       containers:
       - name: nginx
         image: nginx:latest
         env:
         **- name: APP_MODE
           value: &quot;v2&quot;       # v1 -&gt; v2 변경**
         ports:
         - containerPort: 80 # PodTemplate 종료</code></pre>
</li>
</ol>
<blockquote>
<p><strong>기본 롤링 업데이트 재배포 흐름</strong>
신규 pod 배포 → 배포 완료 → 기존 pod 1대 제거 → 제거 완료 → 신규 Pod 배포 → 배포 완료 → 기존 Pod 제거 → 제거 완료 → N번 반복</p>
</blockquote>
<h3 id="guaranteed-qos"><strong>Guaranteed QoS</strong></h3>
<p>QoS(Quality of Service)는 <strong>노드의 리소스가 부족해질 때, 어떤 파드를 우선적으로 유지하고 어떤 파드를 먼저 제거할지를 결정하는 기준</strong>입니다. 이는 <strong>파드가 요청한 CPU/메모리 리소스의 설정 방식</strong>에 따라 자동으로 결정됩니다. (BestEffort → Bustable → Guaranteed)</p>
<table>
<thead>
<tr>
<th>QoS</th>
<th>조건</th>
<th>우선 순위</th>
</tr>
</thead>
<tbody><tr>
<td>Guaranteed</td>
<td>requests와 limits이 동일한 경우</td>
<td>상</td>
</tr>
<tr>
<td>Burstable</td>
<td>requests와 limits이 다른 경우</td>
<td>중</td>
</tr>
<tr>
<td>BestEffort</td>
<td>requests와 limits이 설정되지 않은 경우</td>
<td>하</td>
</tr>
</tbody></table>
<p><strong>QoS 확인</strong></p>
<ol>
<li><p>quaranteed.yaml 작성</p>
<pre><code class="language-bash"> # Request, Limit 존재. 값 동일
 apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: qos-guaranteed
 spec:
   replicas: 1
   selector:
     matchLabels:
       app: qos-guaranteed
   template:
     metadata:
       labels:
         app: qos-guaranteed
     spec:
       containers:
         - name: nginx
           image: nginx:alpine
           resources:
             requests:
               memory: &quot;128Mi&quot;
               cpu: &quot;100m&quot;
             limits:
               memory: &quot;128Mi&quot;
               cpu: &quot;100m&quot;</code></pre>
</li>
<li><p>pod 배포 및 QoS 확인</p>
<pre><code class="language-bash">
 # 배포
 kubectl apply -f guaranteed.yaml
 kubectl get pods

 GUARANTEED=$(kubectl get pods --no-headers -o custom-columns=&quot;:metadata.name&quot; | grep qos-guaranteed)

 # QoS 확인
 kubectl get pod ${GUARANTEED} -o jsonpath=&#39;{.status.qosClass}&#39;
 kubectl describe pod ${GUARANTEED}</code></pre>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/6ff2d55d-4e2c-4bcc-b24d-55b39e7ce4fe/image.png" alt="QoS 확인"></p>
<h2 id="rbac">RBAC</h2>
<p>Role Base Access Control의 줄임말로 쿠버네티스 리소스에 대한 접근<strong>(인가 Authorization)</strong>을 관리합니다.</p>
<h3 id="주요-리소스">주요 리소스</h3>
<table>
<thead>
<tr>
<th>객체</th>
<th>설명</th>
<th>적용 범위</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Role</strong></td>
<td>특정 네임스페이스 내에서의 권한 규칙 모음</td>
<td>네임스페이스</td>
</tr>
<tr>
<td><strong>ClusterRole</strong></td>
<td>클러스터 전역 또는 네임스페이스별로 재사용 가능한 권한 규칙 모음</td>
<td>클러스터 전체</td>
</tr>
<tr>
<td><strong>RoleBinding</strong></td>
<td>Role을 특정 Subject(User, Group SA)에 바인딩</td>
<td>네임스페이스</td>
</tr>
<tr>
<td><strong>ClusterRoleBinding</strong></td>
<td>ClusterRole을 특정 Subject(User, Group SA)에 바인딩</td>
<td>클러스터 전체</td>
</tr>
</tbody></table>
<h3 id="service-account">Service Account</h3>
<p>파드 내부에서 실행되는 프로세스는 자신에게 할당된 서비스 어카운트의 식별자를 사용해 클러스터 API 서버에 인증할 수 있습니다. 파드를 배포할 때 별도로 서비스 어카운트를 지정하지 않으면 기본적으로 <code>default</code> 서비스 어카운트가 사용됩니다. 이후 해당 파드는 Role 또는 ClusterRole과 연계된 권한을 네임스페이스 단위로 부여받아 동작하게 됩니다.</p>
<h3 id="role--clusterrole">Role / ClusterRole</h3>
<p>Service Account 혹은 User 에 부여하기 위한 <strong>권한입니다.</strong></p>
<ul>
<li>Role : 네임스페이스에 종속된 권한</li>
<li>ClusterRole : 클러스터 전체 권한</li>
</ul>
<h3 id="rolebinding--clusterrolebinding">RoleBinding / ClusterRoleBinding</h3>
<p>선언된 Role, ClusterRole 을 User, Service Account 등과 연결하는 리소스입니다.</p>
<ul>
<li><strong>RoleBinding</strong> : Role 과 User, SA 등 Subject 간 연결</li>
<li><strong>ClusterRoleBinding</strong> : ClusterRole 과 User, SA 등 Subject 간 연결</li>
</ul>
<blockquote>
<p><strong>참고</strong>
<a href="https://kubernetes.io/ko/">운영 수준의 컨테이너 오케스트레이션</a>
<a href="https://dgjinsu.tistory.com/67">[k8s] 쿠버네티스의 등장 배경과 쿠버네티스란?</a>
<a href="https://subicura.com/2019/05/19/kubernetes-basic-1.html">쿠버네티스 시작하기 - Kubernetes란 무엇인가?</a>
<a href="https://kubernetes.io/docs/concepts/overview/components/">Kubernetes Components
</a><a href="https://stacksimplify.com/azure-aks/azure-kubernetes-service-namespaces-imperative/">Kubernetes Namespaces - Imperative using kubectl</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker 개념 및 주요 명령어]]></title>
            <link>https://velog.io/@jong-kyung/Docker-%EA%B0%9C%EB%85%90-%EB%B0%8F-%EC%A3%BC%EC%9A%94-%EB%AA%85%EB%A0%B9%EC%96%B4</link>
            <guid>https://velog.io/@jong-kyung/Docker-%EA%B0%9C%EB%85%90-%EB%B0%8F-%EC%A3%BC%EC%9A%94-%EB%AA%85%EB%A0%B9%EC%96%B4</guid>
            <pubDate>Fri, 18 Apr 2025 15:07:33 GMT</pubDate>
            <description><![CDATA[<h2 id="docker란">Docker란?</h2>
<p>Docker는 컨테이너 기반의 가상화 플랫폼으로, 애플리케이션을 격리된 환경에서 실행할 수 있게 해줍니다. 이를 통해 애플리케이션을 다양한 환경에서 일관되게 실행하고 배포할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/1db2b394-6db3-4888-bcd3-ff1a05bb2da4/image.png" alt="Docker 개념"></p>
<h2 id="1-컨테이너-container">1. 컨테이너 (Container)</h2>
<ul>
<li>Docker 컨테이너는 애플리케이션 및 그 의존성(라이브러리, 환경 설정 파일 등)을 포함하는 격리된 실행 환경입니다. 각각의 컨테이너는 독립적으로 실행되며, 호스트 시스템의 자원을 공유합니다.</li>
</ul>
<h2 id="2-이미지-image">2. 이미지 (Image)</h2>
<ul>
<li>Docker 이미지는 애플리케이션을 실행하기 위한 파일 시스템 스냅샷입니다. 애플리케이션, 라이브러리, 설정 파일, 의존성 등이 포함되어 있으며, 컨테이너는 이미지를 기반으로 실행됩니다.</li>
<li>이미지는 읽기 전용이고, 컨테이너는 이미지를 기반으로 실행되면서 읽기/쓰기가 가능합니다.</li>
</ul>
<h2 id="3-dockerfile">3. Dockerfile</h2>
<ul>
<li>Dockerfile은 이미지를 만들기 위한 설정 파일입니다. 이 파일 안에는 이미지 빌드를 위한 단계별 명령어들이 포함됩니다. 예를 들어, 베이스 이미지 선택, 애플리케이션 복사, 패키지 설치 등이 포함될 수 있습니다.</li>
<li><code>FROM</code> : 베이스 이미지 지정 (필수)</li>
<li><code>WORKDIR</code> : 작업 디렉토리 지정 (cd 명령어)</li>
<li><code>COPY</code> : 호스트에서 컨테이너로 파일 및 디렉토리 복사</li>
<li><code>RUN</code> : 쉘 명령어 수행</li>
<li><code>ENV</code> : 환경 변수 설정</li>
<li><code>ARG</code> : 빌드 시 인자 설정 (빌드 후 제거)</li>
<li><code>CMD</code> : 컨테이너 시작 시 실행할 기본 명령어</li>
<li><code>ENTRYPOINT</code> : CMD 와 비슷하나, 고정 명령</li>
</ul>
<h3 id="dockerignore">.dockerignore</h3>
<ul>
<li><code>.gitignore</code> 처럼 docker build 시 Container 내부에 복사하지 않도록 지정하는 파일</li>
<li><strong>dockerfile 과 같은 경로</strong>에 위치</li>
<li>민감 정보 유출 방지</li>
<li>불필요한 파일을 제외하여 빌드 속도 상승</li>
</ul>
<h2 id="4-docker-hub">4. Docker Hub</h2>
<ul>
<li>Docker Hub는 Docker 이미지의 레지스트리입니다. 사용자는 여기서 이미지를 다운로드하거나, 자신이 만든 이미지를 업로드할 수 있습니다.</li>
</ul>
<h2 id="5-도커-네트워크">5. 도커 네트워크</h2>
<p>도커의 기본 네트워크 모드는 <strong>Bridge이고 이외에</strong> Host, None 이 추가적으로 존재합니다. 외에도  macvlan, ipvlan, overlay와 같은 네트워크 플러그인을 사용할 수 있습니다.</p>
<pre><code class="language-bash">docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
5d4ec64ed746   bridge    bridge    local
71da3212e9dc   host      host      local
90a304de3e67   none      null      local

docker info | grep Network
  Network: bridge host ipvlan macvlan null overlay</code></pre>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/b0860b8f-25a8-48cb-9998-241bce8393b9/image.png" alt="도커 네트워크"></p>
<h3 id="bridge-모드">Bridge 모드</h3>
<p>도커에서 기본적으로 쓸 수 있는 네트워크입니다.</p>
<ul>
<li>컨테이너 기본 생성 시 자동으로 docker0 브리지를 사용하게 됩니다.</li>
<li>기본 172.17.0.0/16 대역을 컨테이너가 사용하며, 대역 변경 설정이 가능합니다.</li>
<li>도커 컨테이너 간 상호 통신을 허용하며, 호스트 컨테이너 간 통신 또한 허용합니다</li>
</ul>
<h3 id="host-모드">Host 모드</h3>
<p>호스트의 환경을 그대로 사용하는 것을 의미합니다.</p>
<ul>
<li>애플리케이션 별도 포트 포워딩 없이 바로 서비스 가능합니다.</li>
<li>컨테이너의 호스트 이름도 호스트 머신의 이름과 동일합니다.</li>
</ul>
<h3 id="none-모드"><strong>None 모드</strong></h3>
<p>아무런 네트워크를 쓰지 않는 것을 의미합니다.</p>
<ul>
<li>외부와의 연결이 단절됩니다.</li>
<li>컨테이너 내부에 lo 인터페이스만 존재합니다.</li>
</ul>
<h2 id="6-멀티스테이지-빌드">6. 멀티스테이지 빌드</h2>
<ul>
<li><code>멀티 스테이지 빌드</code><ul>
<li>빌드 만을 위한 파일과 어플리케이션 실행을 위한 파일을 분리하여 실행 컨테이너 크기 축소</li>
</ul>
</li>
</ul>
<h2 id="7-멀티-플랫폼--아키텍처">7. 멀티 플랫폼 / 아키텍처</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/ce78ee21-83d2-4a47-96a0-1baf31c5d68d/image.png" alt=""></p>
<ul>
<li>하나의 빌드 이미지, 태그를 통해 각기 다른 CPU 아키텍처에서 사용할 수 있는 이미지를 생성하는 것</li>
</ul>
<h2 id="8-docker-compose">8. Docker Compose</h2>
<ul>
<li>Docker Compose는 <strong>여러 개의 Docker 컨테이너</strong>를 정의하고, 동시에 실행할 수 있도록 도와주는 도구입니다. 여러 서비스로 구성된 애플리케이션을 쉽게 배포하고 관리할 수 있습니다.</li>
<li><code>linux/amd64</code>, <code>linux/arm64</code>, <code>windows/amd64</code></li>
</ul>
<h2 id="docker-cli">Docker CLI</h2>
<h3 id="basic">Basic</h3>
<p><strong>버전 확인</strong></p>
<ul>
<li>Docker의 현재 버전 확인</li>
</ul>
<pre><code class="language-bash">docker --version</code></pre>
<h3 id="image">Image</h3>
<p><strong>이미지 빌드</strong></p>
<ul>
<li>Dockefile을 사용해 이미지를 빌드</li>
</ul>
<pre><code class="language-bash">docker build -t &lt;이미지이름&gt;:&lt;태그&gt; .(위치)</code></pre>
<p><strong>이미지 다운로드</strong></p>
<ul>
<li>Docker Hub에서 이미지 다운로드</li>
</ul>
<pre><code class="language-bash">docker pull &lt;이미지이름&gt;</code></pre>
<p><strong>이미지 목록 확인</strong></p>
<pre><code class="language-bash">docker images</code></pre>
<p><strong>이미지 제거</strong></p>
<pre><code class="language-bash">docker rmi &lt;이미지이름&gt;</code></pre>
<p>이미지 존재 여부 확인</p>
<pre><code class="language-bash">docker manifest inspect &lt;이미지이름&gt;:&lt;이미지태그&gt;</code></pre>
<h3 id="container">Container</h3>
<hr>
<p><strong>컨테이너 생성</strong></p>
<pre><code class="language-bash">docker create &lt;이미지이름&gt;:&lt;태그&gt;</code></pre>
<p><strong>이미지 다운 및 컨테이너 생성</strong></p>
<ul>
<li>이미지를 기반으로 컨테이너를 생성하고 실행</li>
<li><code>docker run -d -p 8080:80 nginx</code> (nginx 이미지로 컨테이너 실행, 80번 포트를 8080번 포트로 매핑)</li>
</ul>
<pre><code class="language-bash">docker run &lt;옵션&gt; &lt;이미지이름&gt;
# 필수로 알아야 하는 옵션
-d        : [Detach] 백그라운드에서 컨테이너 프로세스 실행 (nohup &amp;)
-i        : [Interactive] 표준입력 연결 (입력) (-it 는 세트)
-t        : [tty] 가상 터미널 생성 - 상호작용 (출력) (-it 는 세트)
--name    : 컨테이너 이름 지정
-p        : [Publish] 호스트 포트와 컨테이너 포트 매핑
--restart : 재시작 정책 [always 항상, on-failure 에러 코드 0이 아닌 경우, unless-stopped 수동으로 중지하지 않는 한]
-v        : [volume] 호스트 볼륨 마운트

# 알아두면 좋은 옵션
-u        : [user] 컨테이너 내부 실행 유저
--rm      : 컨테이너 종료 시 자동 삭제, 임시 사용 [restart 와는 동시 사용 불가]

## 참고
# --restart 와 --rm 은 같이 사용할 수 없다.
# -d 와 -it 는 함께 사용할 수 없다. (docker exec -it 활용)</code></pre>
<p><strong>컨테이너 목록 조회</strong></p>
<ul>
<li>실행중인 컨테이너 목록 조회</li>
<li><code>-a</code> 옵션으로 모든 컨테이너 조회</li>
</ul>
<pre><code class="language-bash">docker ps &lt;옵션&gt;</code></pre>
<p><strong>컨테이너 실행 중지</strong></p>
<pre><code class="language-bash">docker stop &lt;컨테이너ID&gt;</code></pre>
<p><strong>컨테이너 삭제</strong></p>
<pre><code class="language-bash">docker rm &lt;컨테이너ID&gt;
docker rm $(docekr ps -aq) # 전체 삭제</code></pre>
<p><strong>컨테이너 로그 확인</strong></p>
<pre><code class="language-bash">docker logs &lt;컨테이너ID&gt;</code></pre>
<p><strong>실행중인 컨테이너에 명령어 실행</strong></p>
<ul>
<li><code>docker exec -it &lt;컨테이너ID&gt; bash</code> (컨테이너 내에서 bash 쉘 실행)</li>
</ul>
<pre><code class="language-bash">docker exec -it &lt;컨테이너ID&gt; &lt;명령어&gt;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[WebRTC 톺아보기]]></title>
            <link>https://velog.io/@jong-kyung/WebRTC-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@jong-kyung/WebRTC-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sun, 06 Apr 2025 06:57:45 GMT</pubDate>
            <description><![CDATA[<h2 id="기본-개념">기본 개념</h2>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/4f96e5fd-ea85-4748-8522-6c8dc6bb7b14/image.png" alt="webRTC 통신"></p>
<p>WebRTC(Web Real-Time Communication)는 웹 애플리케이션이 <strong>플러그인 없이도</strong> 브라우저들 간에 실시간으로 오디오, 비디오 등의 <strong>미디어를 캡처하고 스트리밍</strong>하며, 그외 임의의 데이터까지 교환할 수 있도록 해주는 기술입니다. 이러한 일련의 표준을 통해 별도의 소프트웨어 설치 없이 <strong>피어 투 피어(P2P)</strong> 방식으로 데이터 공유나 화상 회의를 수행할 수 있게 됩니다.</p>
<p>즉, 브라우저 내장 기능만으로 사용자들 끼리 직접 영상 통화, 음성 채팅, 파일 전송 등을 할 수 있도록 해주는 기술입니다.</p>
<h2 id="주요-구성-요소">주요 구성 요소</h2>
<h3 id="mediastream">MediaStream</h3>
<p>카메라 영상이나 마이크 음성 등의 <strong>미디어 스트림을 나타내는 객체</strong>입니다. 일반적으로 <code>navigator.mediaDevices.getUserMedia()</code> 함수를 통해 사용자의 카메라/마이크 접근 허가 이후 Media Stream을 얻습니다. 이렇게 얻은 Media Stream은 비디오 요소에 출력하거나 P2P 연결로 전달할 수 있습니다.</p>
<pre><code class="language-jsx">navigator.mediaDevices.getUserMedia({ video: true, audio: true })
  .then(stream =&gt; {
    const videoElement = document.getElementById(&#39;localVideo&#39;);
    videoElement.srcObject = stream;    // MediaStream을 video 요소에 연결
  })
  .catch(error =&gt; {
    console.error(&#39;카메라/마이크 접근 실패:&#39;, error);
  });</code></pre>
<h3 id="rtcpeerconnection">RTCPeerConnection</h3>
<p>두 피어(peer) 간의 <strong>P2P 연결을 대표하는 객체입니다.</strong> WebRTC에서 <strong>네트워크 연결의 생성과 관리</strong>를 담당하며, 이 객체에 MediaStream 트랙이나 데이터 채널을 추가하여 <strong>실제 미디어/데이터 교환</strong>을 수행합니다. 또한 이 객체는 <strong>대역폭 관리, 네트워크 장애 대응, ICE 후보 처리</strong> 등을 내부적으로 수행하여 안정적인 연결을 유지합니다.</p>
<h3 id="rtcdatachannel">RTCDataChannel</h3>
<p>WebRTC 연결상에서 <strong>임의의 데이터(P2P 데이터)</strong>를 교환할 수 있게 해주는 <strong>데이터 통신 채널</strong>입니다. RTCDataChaanel은 RTCPeerConnection을 통해 생성되며, 전송에는 내부적으로 <strong>SCTP 프로토콜</strong>이 사용되어 신뢰성 있는 데이터 전송을 제공합니다. </p>
<p>일반 WebSocket처럼 <code>send</code>와 <code>onmessage</code> 등을 이용해 손쉽게 P2P 데이터 통신을 구현할 수 있습니다.</p>
<p>위 세 가지 API를 조합함으로써 미디어 스트림과 임의의 데이터의 직접 통신을 구현할 수 있습니다.</p>
<p>WebRTC의 <strong>미디어 캡처</strong> (MediaStream 획득) → <strong>피어 연결</strong> (RTCPeerConnection 생성 및 활용) → <strong>데이터 교환 채널</strong> (RTCDataChannel 사용)로 이어지는 구성이 P2P 실시간 통신의 기반입니다.</p>
<h2 id="webrtc-내부-프로토콜-스택">WebRTC 내부 프로토콜 스택</h2>
<h3 id="ice-interactive-connectivity-establishment">ICE (Interactive Connectivity Establishment)</h3>
<p>두 피어 간 <strong>네트워크 경로를 찾아내고 연결을 성립하기 위한 프레임워크</strong>입니다.</p>
<p>인터넷 환경에서는 방화벽이나 NAT 라우터 때문에 단순히 IP만 알아서는 통신이 안되는 경우가 많기 떄문에, ICE는 연결을 뚫기 위해 필요한 여러 기법(STUN/TURN)을 종합적으로 시도합니다.</p>
<p>즉, <strong>직접 연결이 막혀있을 경우 대안을 찾고, 가능한 최선의 경로를 찾아주는 역할</strong>을 ICE가 담당합니다.</p>
<h3 id="stun-session-traversal-utilities-for-nat">STUN (Session Traversal Utilities for NAT)</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/044d8165-4da0-4700-8aa6-9f9c4178cb77/image.png" alt="STUN"></p>
<p><strong>자신의 공인 IP주소와 포트 정보를 알아내기 위한 프로토콜</strong>입니다. NAT 뒤에 있는 피어는 직접 접속이 어려우므로, STUN 서버에게 요청을 보내서 <strong>Public IP와 포트</strong>를 응답받습니다. 또한 STUN 응답으로 해당 피어가 NAT 뒤에서 <strong>직접 접속 가능한지 여부</strong>도 알 수 있습니다.</p>
<p>이를 통해 각 피어는 <strong>상대방에게 전달할 자신의 공개 주소(ICE 후보)</strong>를 확보합니다.</p>
<h3 id="turn-traversal-using-relays-around-nat">TURN (Traversal Using Relays around NAT)</h3>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/7e33e89d-3cf0-426b-a63b-8549897c0e36/image.png" alt="TURN"></p>
<p><strong>릴레이 서버를 통한 우회 연결</strong>을 위한 프로토콜입니다. 일부 <strong>NAT</strong> 환경에서는 STUN으로 받은 주소로도 직접 통신이 불가능한데, 이 경우 <strong>TURN 서버를 경유</strong>하여 통신합니다. 피어는 TURN 서버와 연결을 맺고, <strong>모든 데이터를 TURN 서버로 보내 중계</strong>하도록 합니다. 성능상 <strong>오버헤드</strong>가 있기 때문에 <strong>직접 통신이 불가능한 최후의 수단</strong>으로 사용됩니다</p>
<h3 id="sdp-session-description-protocol">SDP (Session Description Protocol)</h3>
<p>WebRTC에서 <strong>미디어 세션의 각종 정보를 표현하는 서식/포맷</strong>입니다. 예를 들어 두 피어가 주고받을 <strong>코덱, 해상도, 암호화 방식, 미디어 종류</strong> 등의 정보를 문자열로 기술한 것입니다. SDP 자체는 <strong>프로토콜이라기보다 데이터 포맷</strong>이며, 이것을 이용해 <strong>Offer/Answer</strong> 형식으로 피어 간 <strong>협상(negotiation)</strong>을 진행합니다. 한 마디로, <strong>서로 어떤 환경과 조건으로 통신할지 묘사한 메타데이터</strong>가 SDP라고 할 수 있습니다.</p>
<p>이 외에도 WebRTC는 내부적으로 <strong>RTP/RTCP</strong>(실시간 전송 프로토콜 및 제어 프로토콜)를 통해 오디오/비디오 패킷을 교환하고, <strong>SCTP</strong>(Stream Control Transmission Protocol)를 통해 데이터 채널의 패킷을 전송합니다.  이러한 세부 프로토콜을 직접 다루기보다는, RTCPeerConnection API를 사용하면 브라우저가 <strong>ICE/STUN/TURN을 통한 경로 설정</strong>부터 <strong>SDP 협상</strong>과 <strong>패킷 전송</strong>까지 <strong>모두 내부적으로 처리</strong>해줍니다.</p>
<h2 id="피어간-연결-흐름">피어간 연결 흐름</h2>
<p>WebRTC를 통해 두 피어를 연결하려면 <strong>사전에 약속된 신호 교환(시그널링)</strong> 과정을 거쳐 <strong>서로를 찾고 연결 협상</strong>을 해야 합니다. WebRTC 자체는 시그널링 방법을 규정하지 않으며, 이를 위해 별도의 신호 서버(웹 소켓 서버 등)를 활용합니다. 아래는 <strong>WebRTC 연결 수립 과정</strong>의 흐름을 순서대로 나타낸 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/jong-kyung/post/d43f051f-1449-4d33-8023-1d37c0705088/image.png" alt="Peer connection"></p>
<h3 id="1-미디어-획득-및-rtcpeerconnection-생성">1. 미디어 획득 및 RTCPeerConnection 생성</h3>
<p>생성을 시작하는 쪽(Peer A)은 필요 시 <code>getUSerMedia()</code> 등으로 로컬 미디어 스트림을 얻고, new <code>RTCPeerConnection()</code> 을 호출하여 <strong>피어 연결 객체</strong>를 생성합니다. 이 때 피어 연결 설정에 <strong>STUN/TURN</strong> 서버 정보를 포함하여 NAT 통과 설정도 함께 합니다.</p>
<h3 id="2-미디어-트랙-추가">2. 미디어 트랙 추가</h3>
<p>MediaStream의 각 트랙(track)을 <code>RTCPeerConnection.addTrack()</code> 등을 통해 peer A의 RTCPeerConnection에 추가합니다. 이렇게 하면 해당 미디어를 상대에게 보낼 준비가 됩니다.</p>
<p>이때 필요시, 데이터 채널도 <code>createDataChannel()</code> 로 미리 열 수 있습니다.</p>
<h3 id="3-offer-생성">3. Offer 생성</h3>
<p>peer A는 <code>RTCPeerConnection.createOffer()</code> 를 호출하여 SDP 제안서(offer)를 생성합니다. 이 SDP에는 자신이 보내고자 하는 미디어 형식, 코덱, 네트워크 후보(ICE candidates) 등이 포함됩니다. 이어 RTCPeerConnection의 <code>setLocalDescript(offer)</code> 를 호출하여 peer A의 로컬 SDP로 설정합니다.</p>
<h3 id="4-시그널링을-통한-offer-전송">4. 시그널링을 통한 Offer 전송</h3>
<p>peer A는 신호 서버를 통해 SDP Offer 내용을 peer B에게 전달합니다. </p>
<h3 id="5-offer-수신-및-answer-생성">5. Offer 수신 및 Answer 생성</h3>
<p>상대 피어(peer B)는 신호 서버로부터 peer A의 Offer(SDP)를 받아서 자신의 <code>setRemoteDescription(answer)</code> 로 이를 <strong>원격 SDP</strong>로 설정합니다. 그런 다음 peer B측에서도 <code>createAnswer()</code> 를 호출하여 <strong>SDP 응답(answer)</strong>을 생성합니다. 생성된 answer SDP를 <strong>setLocalDescription(answer)</strong>로 설정하여 peer B의 로컬 SDP로 적용합니다.</p>
<h3 id="6-answer-전송">6. Answer 전송</h3>
<p>peer B는 신호 서버를 통해 이 answer SDP를 peer A에게 전달합니다.</p>
<h3 id="7-answer-수신">7. Answer 수신</h3>
<p>peer A는 peer B로부터 answer SDP를 건네 받으면 <code>setRemoteDescription(answer)</code> 로 이를 <strong>원격 SDP</strong>로 설정합니다. 이로써 양측의 SDP 협상(offer/answer 교환)이 완료되어, 서로의 미디어 및 연결 조건을 이해하게 됩니다.</p>
<h3 id="8-ice-후보-교환">8. ICE 후보 교환</h3>
<p>양 피어는 각자의 RTCPeerConnection에서 <strong>ICE 후보지(네트워크 주소)</strong>들을 지속적으로 수집하며, 이는 <code>icecandidate</code> 이벤트로 콜백을 받습니다. 이 이벤트가 발생할 때마다 서로 신호 서버를 통해 상대에게 <strong>ICE 후보 정보를 전달</strong>합니다. 상대 피어는 받은 ICE 후보를 <code>addIceCandidate()</code>로 추가합니다. 이러한 방식으로 SDP 교환 이후에도 <strong>추가 후보들을 계속 교환</strong>하여, 최적의 통신 경로를 찾습니다</p>
<h3 id="9-피어-연결-수립">9. 피어 연결 수립</h3>
<p>서로 전달받은 ICE 후보들 중에서 <strong>직접 통신 가능한 쌍을 찾아</strong> 네트워크 검증을 마치면, RTCPeerConnection의 <code>iceConnectionState</code>가 &quot;connected&quot; 또는 &quot;completed&quot;로 바뀌고 <strong>P2P 연결이 확립</strong>됩니다. 이 과정에서 양측 브라우저는 <strong>DTLS 핸드셰이크</strong>를 실행하여 <strong>암호화 채널을 설정</strong>하며, 이후 <strong>실시간 미디어/데이터 전송</strong>이 시작됩니다. 한쪽 피어에서 <code>ontrack</code> 이벤트를 통해 <strong>상대방의 MediaStream 트랙</strong>이 도착했다는 사실을 알려주고, 이를 비디오 요소에 출력하면 화상 통신이 구현됩니다.</p>
<h3 id="예시-코드">예시 코드</h3>
<p><strong>Peer A</strong></p>
<pre><code class="language-jsx">// 1. RTCPeerConnection 생성 (ICE 서버 정보 포함 가능)
const configuration = {
  iceServers: [
    { urls: &#39;stun:stun.example.com&#39; },             // STUN 서버
    { urls: &#39;turn:turn.example.com&#39;, credential: &#39;pwd&#39;, username: &#39;user&#39; }  // TURN 서버 (예시)
  ]
};
const pc = new RTCPeerConnection(configuration);

// 2. (선택) 데이터 채널 생성
const dataChannel = pc.createDataChannel(&quot;chat&quot;);
dataChannel.onopen = () =&gt; console.log(&quot;데이터채널 열린됨&quot;);
dataChannel.onmessage = (e) =&gt; console.log(&quot;상대 메세지:&quot;, e.data);

// 3. 로컬 미디어 스트림 획득 후 PeerConnection에 추가
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
  .then(stream =&gt; {
    stream.getTracks().forEach(track =&gt; pc.addTrack(track, stream));
    // 로컬 비디오 출력 (자기 화면)
    document.getElementById(&#39;localVideo&#39;).srcObject = stream;
    // 4. Offer 생성 및 LocalDescription 설정
    return pc.createOffer();
  })
  .then(offer =&gt; {
    return pc.setLocalDescription(offer);
  })
  .then(() =&gt; {
    // 5. 신호 서버를 통해 Offer SDP 전송
    signalingSend({ type: &#39;offer&#39;, sdp: pc.localDescription });
  })
  .catch(err =&gt; console.error(err));

// ICE 후보 발생 시 상대에게 전송
pc.onicecandidate = (event) =&gt; {
  if (event.candidate) {
    signalingSend({ type: &#39;ice-candidate&#39;, candidate: event.candidate });
  }
};

// 원격 트랙 수신 시 (상대 영상 스트림 수신)
pc.ontrack = (event) =&gt; {
  const [remoteStream] = event.streams;
  document.getElementById(&#39;remoteVideo&#39;).srcObject = remoteStream; // 원격 비디오 출력
};</code></pre>
<p><strong>Peer B</strong></p>
<pre><code class="language-jsx">const pc = new RTCPeerConnection(configuration);

// (선택) 데이터 채널 수신 대기
pc.ondatachannel = (event) =&gt; {
  const dataChannel = event.channel;
  dataChannel.onmessage = (e) =&gt; console.log(&quot;상대 메세지:&quot;, e.data);
};

// 원격 Offer 수신 및 설정
signaling.on(&#39;offer&#39;, (message) =&gt; {
  pc.setRemoteDescription(message.sdp)
    .then(() =&gt; {
      // (옵션) 필요하면 이 시점에 getUserMedia로 로컬 스트림 획득 후 pc.addTrack
      // 6. Answer 생성
      return pc.createAnswer();
    })
    .then(answer =&gt; {
      // 7. LocalDescription 설정 (Answer)
      return pc.setLocalDescription(answer);
    })
    .then(() =&gt; {
      // 8. Answer SDP를 신호 서버 통해 응답 전송
      signalingSend({ type: &#39;answer&#39;, sdp: pc.localDescription });
    })
    .catch(err =&gt; console.error(err));
});

// ICE 후보 처리
pc.onicecandidate = (event) =&gt; {
  if (event.candidate) {
    signalingSend({ type: &#39;ice-candidate&#39;, candidate: event.candidate });
  }
};
signaling.on(&#39;ice-candidate&#39;, (message) =&gt; {
  pc.addIceCandidate(message.candidate);
});

// 원격 트랙 수신 처리
pc.ontrack = (event) =&gt; {
  const remoteStream = event.streams[0];
  document.getElementById(&#39;remoteVideo&#39;).srcObject = remoteStream;
};
</code></pre>
<p>양쪽 피어 모두 <code>RTCPeerConnection</code> 객체를 만들고, <strong>로컬 미디어 스트림을 연결</strong>하며, Offer/Answer 생성 및 교환을 통해 <strong>SDP 협상</strong>을 마칩니다. 그 다음 <strong>ICE 후보들도 상호 교환</strong>하여 P2P 경로를 확정지으면, <code>ontrack</code> 이벤트를 통해 <strong>상대방의 스트림을 수신</strong>하게 됩니다. 데이터 채널의 경우 한쪽에서 <code>createDataChannel()</code>로 만들면, 상대방에는 <code>ondatachannel</code> 이벤트가 발생하므로 이를 통해 <strong>P2P 데이터 통신 채널</strong>도 설정됩니다. 실제로 WebRTC API를 이용한 P2P 연결은 위 코드와 같은 일련의 <strong>비동기 promise 체인</strong>으로 이루어지며, 브라우저가 많은 부분을 자동 처리해주므로 비교적 적은 코드로 구현할 수 있습니다.</p>
<blockquote>
<p><strong>참고</strong>
<a href="https://developer.mozilla.org/ko/docs/Web/API/WebRTC_API">MDN - WebRTC API</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection">MDN - RTCPeerConnection</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Protocols">MDN - WebRTC Protocols</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Signaling_and_video_calling">MDN - Signaling and video Calling</a>
<a href="https://web.dev/articles/webrtc-basics">web.dev - Get started with WebRTC</a>
<a href="https://lovejaco.github.io/posts/webrtc-connectivity-and-nat-traversal/">WebRTC 연결성 및 NAT 통과 기법</a></p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>