<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>따끈따끈한 신입 프론트엔드 개발자</title>
        <link>https://velog.io/</link>
        <description>프론트엔드 입문 개발자입니다.</description>
        <lastBuildDate>Wed, 03 Dec 2025 07:42:49 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>따끈따끈한 신입 프론트엔드 개발자</title>
            <url>https://velog.velcdn.com/images/lee_1124/profile/26ff5f05-ea47-4d35-87a6-bba22dc4522c/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 따끈따끈한 신입 프론트엔드 개발자. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/lee_1124" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[프론트엔드 짧은 간단 지식 정리 - 디자인 패턴]]></title>
            <link>https://velog.io/@lee_1124/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A7%A7%EC%9D%80-%EA%B0%84%EB%8B%A8-%EC%A7%80%EC%8B%9D-%EC%A0%95%EB%A6%AC-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@lee_1124/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A7%A7%EC%9D%80-%EA%B0%84%EB%8B%A8-%EC%A7%80%EC%8B%9D-%EC%A0%95%EB%A6%AC-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Wed, 03 Dec 2025 07:42:49 GMT</pubDate>
            <description><![CDATA[<h2 id="1-디자인-패턴이란-🎯">1. 디자인 패턴이란? 🎯</h2>
<p><strong>디자인 패턴(Design Pattern)</strong>은 소프트웨어 개발 과정에서 반복적으로 발생하는 문제들을 해결하기 위한 <strong>검증된 솔루션</strong>입니다.</p>
<h3 id="🔍-디자인-패턴의-의미">🔍 디자인 패턴의 의미</h3>
<blockquote>
<p>💡 &quot;이런 상황에서는 이런 패턴을 사용하면 좋다&quot;는 일종의 <strong>방향성을 제시</strong>하는 것입니다.</p>
</blockquote>
<p>디자인 패턴은 다음과 같은 이점을 제공합니다:</p>
<ul>
<li><strong>경험 기반의 증명된 해결책</strong> 제공</li>
<li><strong>코드의 재사용성과 유지보수성</strong> 향상</li>
<li><strong>개발자 간 의사소통</strong> 효율화</li>
<li><strong>코드 구조의 표준화</strong></li>
</ul>
<h3 id="🌟-주요-디자인-패턴-종류">🌟 주요 디자인 패턴 종류</h3>
<p>JavaScript/프론트엔드에서 자주 사용되는 패턴들:</p>
<ul>
<li><strong>모듈 패턴</strong> - 코드를 모듈화하여 캡슐화</li>
<li><strong>싱글톤 패턴</strong> - 단 하나의 인스턴스만 생성</li>
<li><strong>팩토리 패턴</strong> - 객체 생성을 추상화</li>
<li><strong>믹스인 패턴</strong> - 객체 간 기능 공유</li>
<li><strong>위임/상속 패턴</strong> - 프로토타입 기반 코드 재사용</li>
</ul>
<hr>
<h2 id="2-모듈-패턴-📦">2. 모듈 패턴 📦</h2>
<p><strong>모듈 패턴(Module Pattern)</strong>은 코드를 논리적 단위로 분리하고, <strong>캡슐화와 은닉화</strong>를 통해 보안성을 높이는 패턴입니다.</p>
<h3 id="💡-방법-1-object-literal-기본">💡 방법 1: Object Literal (기본)</h3>
<p>가장 간단한 모듈 생성 방법으로, 객체 리터럴을 사용합니다.</p>
<pre><code class="language-javascript">// Object Literal 방식
const calculator = {
    result: 0,

    add(num) {
        this.result += num;
        return this;
    },

    subtract(num) {
        this.result -= num;
        return this;
    },

    getResult() {
        return this.result;
    }
};

// 사용 예시
calculator.add(10).subtract(3);
console.log(calculator.getResult()); // 7

// ⚠️ 문제점: 내부 데이터에 직접 접근 가능
calculator.result = 999; // 보안 취약! 🚨
console.log(calculator.getResult()); // 999</code></pre>
<p><strong>장점</strong>: 간편하고 직관적<br><strong>단점</strong>: 모든 프로퍼티가 public이라 보안에 취약</p>
<h3 id="💡-방법-2-iife--클로저-개선된-모듈-패턴">💡 방법 2: IIFE + 클로저 (개선된 모듈 패턴)</h3>
<p><strong>즉시실행함수(IIFE)</strong>와 <strong>클로저</strong>를 활용하여 private 공간을 만듭니다.</p>
<pre><code class="language-javascript">// IIFE + 클로저를 활용한 모듈 패턴
const secureCalculator = (function() {
    // Private 변수 (외부에서 접근 불가) 🔒
    let result = 0;

    // Private 함수
    function validate(num) {
        if (typeof num !== &#39;number&#39;) {
            throw new Error(&#39;숫자만 입력 가능합니다!&#39;);
        }
    }

    // Public API 반환
    return {
        add(num) {
            validate(num);
            result += num;
            return this;
        },

        subtract(num) {
            validate(num);
            result -= num;
            return this;
        },

        multiply(num) {
            validate(num);
            result *= num;
            return this;
        },

        divide(num) {
            validate(num);
            if (num === 0) throw new Error(&#39;0으로 나눌 수 없습니다!&#39;);
            result /= num;
            return this;
        },

        getResult() {
            return result;
        },

        reset() {
            result = 0;
            return this;
        }
    };
})();

// 사용 예시
secureCalculator.add(10).multiply(2).subtract(5);
console.log(secureCalculator.getResult()); // 15

// ✅ 보안 강화: private 변수에 직접 접근 불가
console.log(secureCalculator.result); // undefined
secureCalculator.result = 999; // 영향 없음!
console.log(secureCalculator.getResult()); // 여전히 15</code></pre>
<h3 id="🎯-모듈-패턴의-핵심">🎯 모듈 패턴의 핵심</h3>
<blockquote>
<p><strong>Public/Private 개념</strong>으로 객체를 나누는 <strong>캡슐화 및 은닉화</strong>가 핵심입니다.</p>
</blockquote>
<table>
<thead>
<tr>
<th>특징</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>캡슐화</strong></td>
<td>관련된 데이터와 메서드를 하나의 단위로 묶음</td>
</tr>
<tr>
<td><strong>은닉화</strong></td>
<td>내부 구현을 숨기고 필요한 부분만 노출</td>
</tr>
<tr>
<td><strong>네임스페이스</strong></td>
<td>전역 변수 오염 방지</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-싱글톤-패턴-👑">3. 싱글톤 패턴 👑</h2>
<p><strong>싱글톤 패턴(Singleton Pattern)</strong>은 <strong>한 클래스에서 인스턴스를 단 1개만 생성</strong>하도록 제한하는 패턴입니다.</p>
<h3 id="🔍-싱글톤이-필요한-경우">🔍 싱글톤이 필요한 경우</h3>
<ul>
<li>애플리케이션 전역에서 <strong>하나의 상태만 관리</strong>해야 할 때</li>
<li><strong>데이터베이스 연결</strong>, <strong>로거</strong>, <strong>설정 관리자</strong> 등</li>
</ul>
<h3 id="💡-싱글톤-구현-예시">💡 싱글톤 구현 예시</h3>
<pre><code class="language-javascript">// 싱글톤 패턴 구현
const Database = (function() {
    let instance; // private 변수로 인스턴스 저장

    function createInstance() {
        // 실제 데이터베이스 객체
        return {
            connection: null,

            connect() {
                if (!this.connection) {
                    this.connection = &#39;데이터베이스 연결됨 🔗&#39;;
                    console.log(this.connection);
                }
            },

            query(sql) {
                if (!this.connection) {
                    throw new Error(&#39;먼저 연결해주세요!&#39;);
                }
                console.log(`쿼리 실행: ${sql}`);
            },

            disconnect() {
                this.connection = null;
                console.log(&#39;연결 종료 ❌&#39;);
            }
        };
    }

    return {
        getInstance() {
            if (!instance) {
                instance = createInstance();
                console.log(&#39;✨ 새 인스턴스 생성&#39;);
            } else {
                console.log(&#39;♻️ 기존 인스턴스 반환&#39;);
            }
            return instance;
        }
    };
})();

// 사용 예시
const db1 = Database.getInstance(); // ✨ 새 인스턴스 생성
db1.connect(); // 데이터베이스 연결됨 🔗

const db2 = Database.getInstance(); // ♻️ 기존 인스턴스 반환

console.log(db1 === db2); // true (같은 인스턴스!)</code></pre>
<h3 id="🎯-싱글톤의-장단점">🎯 싱글톤의 장단점</h3>
<p><strong>장점</strong>:</p>
<ul>
<li>메모리 효율성 (인스턴스가 하나만 존재)</li>
<li>전역 상태 관리 용이</li>
<li>리소스 공유 최적화</li>
</ul>
<p><strong>단점</strong>:</p>
<ul>
<li>전역 상태로 인한 테스트 어려움</li>
<li>의존성 증가</li>
<li>멀티스레드 환경에서 주의 필요</li>
</ul>
<hr>
<h2 id="4-팩토리-패턴-🏭">4. 팩토리 패턴 🏭</h2>
<p><strong>팩토리 패턴(Factory Pattern)</strong>은 <strong>비슷한 객체를 공장에서 찍어내듯 반복적으로 생성</strong>할 수 있도록 하는 패턴입니다.</p>
<h3 id="🔍-팩토리-패턴의-특징">🔍 팩토리 패턴의 특징</h3>
<blockquote>
<p><strong>new 키워드</strong>를 사용한 생성자 함수가 아니라, <strong>일반 함수에서 객체를 반환</strong>하는 것을 팩토리 함수라고 합니다.</p>
</blockquote>
<h3 id="💡-예시-1-사용자-생성-팩토리">💡 예시 1: 사용자 생성 팩토리</h3>
<pre><code class="language-javascript">// 팩토리 함수
function createUser(name, role) {
    // 공통 속성
    const user = {
        name: name,
        role: role,
        createdAt: new Date(),

        // 공통 메서드
        getInfo() {
            return `${this.name} (${this.role})`;
        }
    };

    // 역할별 특수 메서드 추가
    if (role === &#39;admin&#39;) {
        user.deleteUser = function(userId) {
            console.log(`관리자 ${this.name}가 사용자 ${userId}를 삭제했습니다.`);
        };
    } else if (role === &#39;editor&#39;) {
        user.editContent = function(contentId) {
            console.log(`편집자 ${this.name}가 콘텐츠 ${contentId}를 수정했습니다.`);
        };
    }

    return user;
}

// 사용 예시
const admin = createUser(&#39;Alice&#39;, &#39;admin&#39;);
const editor = createUser(&#39;Bob&#39;, &#39;editor&#39;);
const viewer = createUser(&#39;Charlie&#39;, &#39;viewer&#39;);

console.log(admin.getInfo()); // &quot;Alice (admin)&quot;
admin.deleteUser(123); // &quot;관리자 Alice가 사용자 123을 삭제했습니다.&quot;

console.log(editor.getInfo()); // &quot;Bob (editor)&quot;
editor.editContent(456); // &quot;편집자 Bob가 콘텐츠 456을 수정했습니다.&quot;</code></pre>
<h3 id="💡-예시-2-ui-컴포넌트-팩토리">💡 예시 2: UI 컴포넌트 팩토리</h3>
<pre><code class="language-javascript">// UI 컴포넌트 팩토리
function createButton(type, text) {
    const button = {
        type: type,
        text: text,

        render() {
            return `&lt;button class=&quot;btn-${this.type}&quot;&gt;${this.text}&lt;/button&gt;`;
        },

        onClick(handler) {
            console.log(`${this.text} 버튼 클릭 이벤트 등록`);
            this.clickHandler = handler;
        }
    };

    // 타입별 스타일 설정
    switch(type) {
        case &#39;primary&#39;:
            button.style = &#39;background: blue; color: white;&#39;;
            break;
        case &#39;danger&#39;:
            button.style = &#39;background: red; color: white;&#39;;
            break;
        case &#39;success&#39;:
            button.style = &#39;background: green; color: white;&#39;;
            break;
        default:
            button.style = &#39;background: gray; color: black;&#39;;
    }

    return button;
}

// 사용 예시
const submitBtn = createButton(&#39;primary&#39;, &#39;제출&#39;);
const deleteBtn = createButton(&#39;danger&#39;, &#39;삭제&#39;);
const saveBtn = createButton(&#39;success&#39;, &#39;저장&#39;);

console.log(submitBtn.render());
// &lt;button class=&quot;btn-primary&quot;&gt;제출&lt;/button&gt;

deleteBtn.onClick(() =&gt; console.log(&#39;삭제 확인&#39;));
// &quot;삭제 버튼 클릭 이벤트 등록&quot;</code></pre>
<h3 id="🎯-팩토리-패턴의-장점">🎯 팩토리 패턴의 장점</h3>
<ul>
<li><strong>객체 생성 로직을 한 곳에 집중</strong></li>
<li><strong>생성자 대신 명확한 함수명</strong> 사용 (예: <code>createUser</code>, <code>createButton</code>)</li>
<li><strong>조건부 객체 생성이 쉬움</strong></li>
<li><strong>코드 재사용성 향상</strong></li>
</ul>
<hr>
<h2 id="5-믹스인-패턴-🎨">5. 믹스인 패턴 🎨</h2>
<p><strong>믹스인 패턴(Mixin Pattern)</strong>은 <strong>한 객체의 프로퍼티를 다른 객체에 복사해 사용</strong>하는 패턴으로, 코드를 재사용하는 효과를 냅니다.</p>
<h3 id="🔍-믹스인의-목적">🔍 믹스인의 목적</h3>
<blockquote>
<p>기존 객체의 기능을 <strong>그대로 보존</strong>하면서 다른 객체에 추가할 때 사용합니다.</p>
</blockquote>
<h3 id="💡-예시-1-기본-믹스인-구현">💡 예시 1: 기본 믹스인 구현</h3>
<pre><code class="language-javascript">// 믹스인 함수
function mixin(target, source) {
    for (let key in source) {
        if (source.hasOwnProperty(key)) {
            target[key] = source[key];
        }
    }
    return target;
}

// 공통 기능들
const canEat = {
    eat(food) {
        console.log(`${this.name}이(가) ${food}를 먹습니다. 🍽️`);
    }
};

const canWalk = {
    walk() {
        console.log(`${this.name}이(가) 걷습니다. 🚶`);
    }
};

const canSwim = {
    swim() {
        console.log(`${this.name}이(가) 수영합니다. 🏊`);
    }
};

// 사용 예시
const person = { name: &#39;철수&#39; };
mixin(person, canEat);
mixin(person, canWalk);

person.eat(&#39;사과&#39;); // &quot;철수이(가) 사과를 먹습니다. 🍽️&quot;
person.walk(); // &quot;철수이(가) 걷습니다. 🚶&quot;

const duck = { name: &#39;오리&#39; };
mixin(duck, canEat);
mixin(duck, canWalk);
mixin(duck, canSwim);

duck.eat(&#39;빵&#39;); // &quot;오리이(가) 빵을 먹습니다. 🍽️&quot;
duck.swim(); // &quot;오리이(가) 수영합니다. 🏊&quot;</code></pre>
<h3 id="💡-예시-2-objectassign을-활용한-믹스인">💡 예시 2: Object.assign을 활용한 믹스인</h3>
<pre><code class="language-javascript">// Object.assign을 사용한 믹스인
const eventEmitterMixin = {
    on(event, handler) {
        if (!this._events) this._events = {};
        if (!this._events[event]) this._events[event] = [];
        this._events[event].push(handler);
    },

    emit(event, ...args) {
        if (!this._events || !this._events[event]) return;
        this._events[event].forEach(handler =&gt; handler(...args));
    },

    off(event, handler) {
        if (!this._events || !this._events[event]) return;
        this._events[event] = this._events[event].filter(h =&gt; h !== handler);
    }
};

const loggingMixin = {
    log(message) {
        console.log(`[${new Date().toISOString()}] ${message}`);
    },

    error(message) {
        console.error(`[ERROR] ${message}`);
    }
};

// 여러 믹스인을 한 번에 적용
class Component {
    constructor(name) {
        this.name = name;
    }
}

Object.assign(Component.prototype, eventEmitterMixin, loggingMixin);

// 사용 예시
const myComponent = new Component(&#39;MyComponent&#39;);

myComponent.on(&#39;dataLoaded&#39;, (data) =&gt; {
    myComponent.log(`데이터 로드됨: ${data}`);
});

myComponent.emit(&#39;dataLoaded&#39;, &#39;사용자 목록&#39;); 
// &quot;[2024-01-15T12:00:00.000Z] 데이터 로드됨: 사용자 목록&quot;

myComponent.error(&#39;데이터 로드 실패!&#39;);
// &quot;[ERROR] 데이터 로드 실패!&quot;</code></pre>
<h3 id="🎯-믹스인-패턴의-특징">🎯 믹스인 패턴의 특징</h3>
<p><strong>장점</strong>:</p>
<ul>
<li>다중 상속과 유사한 효과</li>
<li>코드 재사용성 극대화</li>
<li>유연한 기능 조합</li>
</ul>
<p><strong>단점</strong>:</p>
<ul>
<li>메서드 이름 충돌 가능성</li>
<li>출처 추적이 어려울 수 있음</li>
</ul>
<hr>
<h2 id="6-위임상속-패턴-🔗">6. 위임/상속 패턴 🔗</h2>
<p><strong>Behavior Delegation (위임) / Inheritance (상속)</strong>은 부모 프로토타입에 저장되어있는 변수나 메소드를 자식 쪽에서 위임받아 사용하는 패턴입니다.</p>
<h3 id="🔍-프로토타입-체인의-이해">🔍 프로토타입 체인의 이해</h3>
<blockquote>
<p>하위 클래스(객체)에서는 <strong>상위 프로토타입에 있는 변수나 메소드들을 위임받아</strong> 언제든지 사용할 수 있습니다.</p>
</blockquote>
<h3 id="💡-예시-1-프로토타입-상속">💡 예시 1: 프로토타입 상속</h3>
<pre><code class="language-javascript">// 상위 프로토타입 (부모)
function Animal(name) {
    this.name = name;
}

Animal.prototype.eat = function(food) {
    console.log(`${this.name}이(가) ${food}를 먹습니다.`);
};

Animal.prototype.sleep = function() {
    console.log(`${this.name}이(가) 잠을 잡니다. 💤`);
};

// 하위 프로토타입 (자식)
function Dog(name, breed) {
    Animal.call(this, name); // 부모 생성자 호출
    this.breed = breed;
}

// 프로토타입 체인 연결
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// Dog만의 메서드 추가
Dog.prototype.bark = function() {
    console.log(`${this.name}: 멍멍! 🐕`);
};

// 사용 예시
const myDog = new Dog(&#39;바둑이&#39;, &#39;진돗개&#39;);

myDog.eat(&#39;사료&#39;); // &quot;바둑이이(가) 사료를 먹습니다.&quot; (Animal에서 상속)
myDog.sleep(); // &quot;바둑이이(가) 잠을 잡니다. 💤&quot; (Animal에서 상속)
myDog.bark(); // &quot;바둑이: 멍멍! 🐕&quot; (Dog 자체 메서드)

console.log(myDog.breed); // &quot;진돗개&quot;</code></pre>
<h3 id="💡-예시-2-es6-class-문법으로-상속">💡 예시 2: ES6 Class 문법으로 상속</h3>
<pre><code class="language-javascript">// 부모 클래스
class Vehicle {
    constructor(name, speed) {
        this.name = name;
        this.speed = speed;
    }

    move() {
        console.log(`${this.name}이(가) ${this.speed}km/h로 이동합니다. 🚗`);
    }

    stop() {
        console.log(`${this.name}이(가) 정지합니다. 🛑`);
    }
}

// 자식 클래스
class Car extends Vehicle {
    constructor(name, speed, brand) {
        super(name, speed); // 부모 생성자 호출
        this.brand = brand;
    }

    // 메서드 오버라이딩
    move() {
        console.log(`${this.brand} ${this.name}이(가) 도로 위를 달립니다! 🏎️`);
        super.move(); // 부모 메서드 호출
    }

    // 자식만의 메서드
    honk() {
        console.log(`${this.name}: 빵빵! 📯`);
    }
}

class Airplane extends Vehicle {
    constructor(name, speed, altitude) {
        super(name, speed);
        this.altitude = altitude;
    }

    fly() {
        console.log(`${this.name}이(가) ${this.altitude}m 상공을 비행합니다. ✈️`);
    }
}

// 사용 예시
const myCar = new Car(&#39;소나타&#39;, 180, &#39;현대&#39;);
myCar.move();
// &quot;현대 소나타이(가) 도로 위를 달립니다! 🏎️&quot;
// &quot;소나타이(가) 180km/h로 이동합니다. 🚗&quot;

myCar.honk(); // &quot;소나타: 빵빵! 📯&quot;
myCar.stop(); // &quot;소나타이(가) 정지합니다. 🛑&quot;

const myPlane = new Airplane(&#39;보잉747&#39;, 900, 10000);
myPlane.move(); // &quot;보잉747이(가) 900km/h로 이동합니다. 🚗&quot;
myPlane.fly(); // &quot;보잉747이(가) 10000m 상공을 비행합니다. ✈️&quot;</code></pre>
<h3 id="💡-예시-3-delegation-패턴-위임">💡 예시 3: Delegation 패턴 (위임)</h3>
<pre><code class="language-javascript">// 위임 방식: 상속 대신 객체 간 연결
const AnimalBehavior = {
    init(name) {
        this.name = name;
    },

    eat(food) {
        console.log(`${this.name}이(가) ${food}를 먹습니다.`);
    }
};

const DogBehavior = Object.create(AnimalBehavior);

DogBehavior.setup = function(name, breed) {
    this.init(name);
    this.breed = breed;
};

DogBehavior.bark = function() {
    console.log(`${this.name}: 멍멍!`);
};

// 사용 예시
const dog = Object.create(DogBehavior);
dog.setup(&#39;바둑이&#39;, &#39;진돗개&#39;);

dog.eat(&#39;간식&#39;); // AnimalBehavior에 위임
dog.bark(); // DogBehavior 자체 메서드</code></pre>
<h3 id="🎯-상속-vs-위임">🎯 상속 vs 위임</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>상속 (Inheritance)</th>
<th>위임 (Delegation)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>관계</strong></td>
<td>&quot;is-a&quot; 관계</td>
<td>&quot;has-a&quot; 관계</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>
<hr>
<h2 id="7-실전-활용-예시-⚡">7. 실전 활용 예시 ⚡</h2>
<h3 id="🎯-패턴-조합-싱글톤--팩토리">🎯 패턴 조합: 싱글톤 + 팩토리</h3>
<pre><code class="language-javascript">// 앱 설정 관리자 (싱글톤 + 팩토리)
const ConfigManager = (function() {
    let instance;

    function createConfig() {
        let config = {
            apiUrl: &#39;https://api.example.com&#39;,
            timeout: 5000,
            theme: &#39;light&#39;
        };

        return {
            get(key) {
                return config[key];
            },

            set(key, value) {
                config[key] = value;
                console.log(`설정 변경: ${key} = ${value}`);
            },

            getAll() {
                return { ...config };
            }
        };
    }

    return {
        getInstance() {
            if (!instance) {
                instance = createConfig();
            }
            return instance;
        }
    };
})();

// 사용
const config1 = ConfigManager.getInstance();
const config2 = ConfigManager.getInstance();

console.log(config1 === config2); // true

config1.set(&#39;theme&#39;, &#39;dark&#39;);
console.log(config2.get(&#39;theme&#39;)); // &#39;dark&#39; (같은 인스턴스!)</code></pre>
<h3 id="🎯-패턴-조합-모듈--믹스인">🎯 패턴 조합: 모듈 + 믹스인</h3>
<pre><code class="language-javascript">// 기능별 믹스인
const validationMixin = {
    validate(data, rules) {
        for (let field in rules) {
            if (!rules[field](data[field])) {
                return { valid: false, field };
            }
        }
        return { valid: true };
    }
};

const ajaxMixin = {
    async request(url, options = {}) {
        try {
            const response = await fetch(url, options);
            return await response.json();
        } catch (error) {
            this.handleError(error);
        }
    }
};

// 모듈 패턴으로 전체 구조 생성
const UserModule = (function() {
    let users = [];

    const module = {
        addUser(user) {
            const result = this.validate(user, {
                name: (val) =&gt; val &amp;&amp; val.length &gt; 0,
                email: (val) =&gt; /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)
            });

            if (result.valid) {
                users.push(user);
                console.log(&#39;✅ 사용자 추가 성공&#39;);
            } else {
                console.log(`❌ 유효성 검사 실패: ${result.field}`);
            }
        },

        async fetchUsers() {
            const data = await this.request(&#39;/api/users&#39;);
            users = data;
            return users;
        },

        handleError(error) {
            console.error(&#39;에러 발생:&#39;, error.message);
        },

        getUsers() {
            return [...users];
        }
    };

    // 믹스인 적용
    Object.assign(module, validationMixin, ajaxMixin);

    return module;
})();

// 사용
UserModule.addUser({ name: &#39;철수&#39;, email: &#39;chulsoo@example.com&#39; });
// ✅ 사용자 추가 성공

UserModule.addUser({ name: &#39;&#39;, email: &#39;invalid&#39; });
// ❌ 유효성 검사 실패: name</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 짧은 간단 지식 정리 - HTTP Method]]></title>
            <link>https://velog.io/@lee_1124/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A7%A7%EC%9D%80-%EA%B0%84%EB%8B%A8-%EC%A7%80%EC%8B%9D-%EC%A0%95%EB%A6%AC-HTTP-Method</link>
            <guid>https://velog.io/@lee_1124/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A7%A7%EC%9D%80-%EA%B0%84%EB%8B%A8-%EC%A7%80%EC%8B%9D-%EC%A0%95%EB%A6%AC-HTTP-Method</guid>
            <pubDate>Wed, 03 Dec 2025 06:52:22 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-http-method란">📌 HTTP Method란?</h2>
<p><strong>HTTP Method</strong>는 클라이언트와 서버가 통신하기 위해 사용하는 메소드입니다.</p>
<p>서버에게 &quot;이 리소스를 어떻게 처리해주세요&quot;라고 요청하는 방식을 정의하는 것으로, <strong>RESTful API</strong>의 핵심 개념입니다.</p>
<h3 id="주요-http-method">주요 HTTP Method</h3>
<table>
<thead>
<tr>
<th>Method</th>
<th>용도</th>
<th>멱등성</th>
<th>안전성</th>
<th>Body</th>
</tr>
</thead>
<tbody><tr>
<td><strong>GET</strong></td>
<td>조회</td>
<td>✅</td>
<td>✅</td>
<td>❌</td>
</tr>
<tr>
<td><strong>POST</strong></td>
<td>생성</td>
<td>❌</td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td><strong>PUT</strong></td>
<td>전체 수정</td>
<td>✅</td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td><strong>PATCH</strong></td>
<td>부분 수정</td>
<td>❌</td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td><strong>DELETE</strong></td>
<td>삭제</td>
<td>✅</td>
<td>❌</td>
<td>❌</td>
</tr>
</tbody></table>
<hr>
<h2 id="1️⃣-get---데이터-조회">1️⃣ GET - 데이터 조회</h2>
<h3 id="개념">개념</h3>
<p><strong>GET</strong>은 서버로부터 데이터를 조회하기 위한 용도로 사용합니다.</p>
<pre><code class="language-http">GET /users/123 HTTP/1.1
Host: api.example.com</code></pre>
<h3 id="특징">특징</h3>
<h4 id="✅-데이터-전송-방식">✅ 데이터 전송 방식</h4>
<p><strong>URL에 Query String 형식으로 데이터를 전송</strong>합니다.</p>
<pre><code>❌ Body 사용 안 함
✅ URL 사용

예시:
https://api.example.com/search?keyword=nextjs&amp;page=1&amp;limit=10
                              ↑
                         Query String</code></pre><h4 id="📊-상세-특징">📊 상세 특징</h4>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>데이터 위치</strong></td>
<td>URL (Query String)</td>
</tr>
<tr>
<td><strong>Body 사용</strong></td>
<td>❌ 사용 안 함</td>
</tr>
<tr>
<td><strong>길이 제한</strong></td>
<td>⚠️ URL 최대 길이 고려 필요 (브라우저마다 다름)</td>
</tr>
<tr>
<td><strong>보안</strong></td>
<td>⚠️ URL에 노출되어 취약</td>
</tr>
<tr>
<td><strong>캐싱</strong></td>
<td>✅ 가능 (성능 최적화)</td>
</tr>
<tr>
<td><strong>브라우저 기록</strong></td>
<td>✅ 남음</td>
</tr>
<tr>
<td><strong>북마크</strong></td>
<td>✅ 가능</td>
</tr>
<tr>
<td><strong>멱등성</strong></td>
<td>✅ 있음</td>
</tr>
<tr>
<td><strong>안전성</strong></td>
<td>✅ 서버 상태 변경 안 함</td>
</tr>
</tbody></table>
<h3 id="사용-예시">사용 예시</h3>
<h4 id="기본-조회">기본 조회</h4>
<pre><code class="language-http">GET /api/users HTTP/1.1
Host: example.com

→ 모든 사용자 목록 조회</code></pre>
<h4 id="특정-리소스-조회">특정 리소스 조회</h4>
<pre><code class="language-http">GET /api/users/123 HTTP/1.1
Host: example.com

→ ID가 123인 사용자 조회</code></pre>
<h4 id="query-string-사용">Query String 사용</h4>
<pre><code class="language-http">GET /api/products?category=electronics&amp;price_max=50000&amp;sort=latest HTTP/1.1
Host: example.com

→ 필터링 + 정렬된 제품 목록 조회</code></pre>
<h3 id="코드-예시">코드 예시</h3>
<h4 id="javascript-fetch-api">JavaScript (Fetch API)</h4>
<pre><code class="language-javascript">// 기본 GET 요청
fetch(&#39;https://api.example.com/users/123&#39;)
  .then(response =&gt; response.json())
  .then(data =&gt; console.log(data));

// Query String 포함
const params = new URLSearchParams({
  keyword: &#39;nextjs&#39;,
  page: 1,
  limit: 10
});

fetch(`https://api.example.com/search?${params}`)
  .then(response =&gt; response.json())
  .then(data =&gt; console.log(data));</code></pre>
<h4 id="axios">Axios</h4>
<pre><code class="language-javascript">// 기본 GET 요청
axios.get(&#39;https://api.example.com/users/123&#39;)
  .then(response =&gt; console.log(response.data));

// Query String (params 옵션)
axios.get(&#39;https://api.example.com/search&#39;, {
  params: {
    keyword: &#39;nextjs&#39;,
    page: 1,
    limit: 10
  }
}).then(response =&gt; console.log(response.data));</code></pre>
<h3 id="주의사항">주의사항</h3>
<h4 id="⚠️-url-길이-제한">⚠️ URL 길이 제한</h4>
<p>브라우저마다 URL 최대 길이가 다릅니다:</p>
<table>
<thead>
<tr>
<th>브라우저</th>
<th>최대 길이</th>
</tr>
</thead>
<tbody><tr>
<td>Chrome</td>
<td>~8,192자</td>
</tr>
<tr>
<td>Firefox</td>
<td>~65,536자</td>
</tr>
<tr>
<td>IE</td>
<td>~2,083자</td>
</tr>
<tr>
<td>Safari</td>
<td>~80,000자</td>
</tr>
</tbody></table>
<pre><code>❌ 나쁜 예 (긴 데이터)
GET /api/search?data=매우매우매우...긴데이터... (수천 자)

✅ 좋은 예
POST /api/search (데이터는 Body에)</code></pre><h4 id="⚠️-보안-취약점">⚠️ 보안 취약점</h4>
<pre><code>❌ 민감 정보를 GET으로 전송하지 마세요!

GET /api/login?username=admin&amp;password=1234
                              ↑
                      URL에 노출 (위험!)

✅ 민감 정보는 POST + Body + HTTPS</code></pre><h3 id="캐싱의-이점">캐싱의 이점</h3>
<pre><code>첫 번째 요청:
Client → [GET /api/products] → Server
       ← [데이터 + 캐시 헤더] ←

두 번째 요청 (같은 URL):
Client → [캐시 확인] → ⚡ 캐시에서 즉시 응답
           (서버 요청 없음!)

→ 빠른 응답 속도
→ 서버 부하 감소
→ 네트워크 비용 절감</code></pre><hr>
<h2 id="2️⃣-post---데이터-생성">2️⃣ POST - 데이터 생성</h2>
<h3 id="개념-1">개념</h3>
<p><strong>POST</strong>는 리소스 데이터를 생성하는 목적으로 사용합니다.</p>
<pre><code class="language-http">POST /api/users HTTP/1.1
Host: example.com
Content-Type: application/json

{
  &quot;name&quot;: &quot;홍길동&quot;,
  &quot;email&quot;: &quot;hong@example.com&quot;
}</code></pre>
<h3 id="특징-1">특징</h3>
<h4 id="✅-데이터-전송-방식-1">✅ 데이터 전송 방식</h4>
<p><strong>HTTP 메시지의 Body에 데이터를 담아서 전송</strong>합니다.</p>
<pre><code>✅ Body 사용
❌ URL에 데이터 노출 안 함

예시:
POST /api/users HTTP/1.1
Content-Type: application/json

{
  &quot;name&quot;: &quot;홍길동&quot;,
  &quot;age&quot;: 30
}</code></pre><h4 id="📊-상세-특징-1">📊 상세 특징</h4>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>데이터 위치</strong></td>
<td>HTTP Message Body</td>
</tr>
<tr>
<td><strong>Body 사용</strong></td>
<td>✅ 필수</td>
</tr>
<tr>
<td><strong>길이 제한</strong></td>
<td>✅ 제한 없음 (대용량 가능)</td>
</tr>
<tr>
<td><strong>보안</strong></td>
<td>✅ URL에 노출 안 됨 (HTTPS 권장)</td>
</tr>
<tr>
<td><strong>캐싱</strong></td>
<td>❌ 불가능</td>
</tr>
<tr>
<td><strong>브라우저 기록</strong></td>
<td>❌ 남지 않음</td>
</tr>
<tr>
<td><strong>북마크</strong></td>
<td>❌ 불가능</td>
</tr>
<tr>
<td><strong>멱등성</strong></td>
<td>❌ 없음</td>
</tr>
<tr>
<td><strong>안전성</strong></td>
<td>❌ 서버 상태 변경</td>
</tr>
</tbody></table>
<h3 id="content-type">Content-Type</h3>
<p>POST 요청 시 <strong>Content-Type 헤더</strong>에 데이터 타입을 명시해야 합니다.</p>
<table>
<thead>
<tr>
<th>Content-Type</th>
<th>용도</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>application/json</code></td>
<td>JSON 데이터</td>
<td>API 요청 (가장 일반적)</td>
</tr>
<tr>
<td><code>application/x-www-form-urlencoded</code></td>
<td>폼 데이터</td>
<td>HTML 폼 제출</td>
</tr>
<tr>
<td><code>multipart/form-data</code></td>
<td>파일 업로드</td>
<td>이미지, 동영상 등</td>
</tr>
<tr>
<td><code>text/plain</code></td>
<td>텍스트</td>
<td>단순 텍스트</td>
</tr>
</tbody></table>
<h3 id="사용-예시-1">사용 예시</h3>
<h4 id="json-데이터-전송">JSON 데이터 전송</h4>
<pre><code class="language-http">POST /api/users HTTP/1.1
Host: example.com
Content-Type: application/json
Content-Length: 67

{
  &quot;name&quot;: &quot;홍길동&quot;,
  &quot;email&quot;: &quot;hong@example.com&quot;,
  &quot;age&quot;: 30
}</code></pre>
<h4 id="폼-데이터-전송">폼 데이터 전송</h4>
<pre><code class="language-http">POST /api/users HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded

name=%ED%99%8D%EA%B8%B8%EB%8F%99&amp;email=hong@example.com&amp;age=30</code></pre>
<h4 id="파일-업로드">파일 업로드</h4>
<pre><code class="language-http">POST /api/upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name=&quot;file&quot;; filename=&quot;image.jpg&quot;
Content-Type: image/jpeg

[바이너리 데이터]
------WebKitFormBoundary--</code></pre>
<h3 id="코드-예시-1">코드 예시</h3>
<h4 id="javascript-fetch-api-1">JavaScript (Fetch API)</h4>
<pre><code class="language-javascript">// JSON 데이터 전송
fetch(&#39;https://api.example.com/users&#39;, {
  method: &#39;POST&#39;,
  headers: {
    &#39;Content-Type&#39;: &#39;application/json&#39;,
  },
  body: JSON.stringify({
    name: &#39;홍길동&#39;,
    email: &#39;hong@example.com&#39;,
    age: 30
  })
})
  .then(response =&gt; response.json())
  .then(data =&gt; console.log(data));

// 파일 업로드
const formData = new FormData();
formData.append(&#39;file&#39;, fileInput.files[0]);
formData.append(&#39;title&#39;, &#39;제목&#39;);

fetch(&#39;https://api.example.com/upload&#39;, {
  method: &#39;POST&#39;,
  body: formData // Content-Type 자동 설정
})
  .then(response =&gt; response.json())
  .then(data =&gt; console.log(data));</code></pre>
<h4 id="axios-1">Axios</h4>
<pre><code class="language-javascript">// JSON 데이터 전송
axios.post(&#39;https://api.example.com/users&#39;, {
  name: &#39;홍길동&#39;,
  email: &#39;hong@example.com&#39;,
  age: 30
})
  .then(response =&gt; console.log(response.data));

// 파일 업로드
const formData = new FormData();
formData.append(&#39;file&#39;, fileInput.files[0]);

axios.post(&#39;https://api.example.com/upload&#39;, formData, {
  headers: {
    &#39;Content-Type&#39;: &#39;multipart/form-data&#39;
  }
})
  .then(response =&gt; console.log(response.data));</code></pre>
<h3 id="대용량-데이터-전송">대용량 데이터 전송</h3>
<pre><code>GET의 한계:
URL 길이 제한 → 대량 데이터 전송 불가

POST의 장점:
Body 사용 → 제한 없음 ✅

예시:
- 대용량 JSON 데이터
- 이미지/동영상 파일
- 복잡한 검색 조건
- 대량의 배열 데이터</code></pre><hr>
<h2 id="3️⃣-put---전체-수정">3️⃣ PUT - 전체 수정</h2>
<h3 id="개념-2">개념</h3>
<p><strong>PUT</strong>은 데이터를 추가하거나 기존 데이터 전체를 바꾸는 덮어쓰기 용도로 사용합니다.</p>
<pre><code class="language-http">PUT /api/users/123 HTTP/1.1
Host: example.com
Content-Type: application/json

{
  &quot;name&quot;: &quot;홍길동&quot;,
  &quot;email&quot;: &quot;hong@example.com&quot;,
  &quot;age&quot;: 31,
  &quot;address&quot;: &quot;서울시 강남구&quot;
}</code></pre>
<h3 id="특징-2">특징</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>용도</strong></td>
<td>리소스 전체 교체/생성</td>
</tr>
<tr>
<td><strong>멱등성</strong></td>
<td>✅ 있음 (여러 번 요청해도 결과 동일)</td>
</tr>
<tr>
<td><strong>부분 수정</strong></td>
<td>❌ 불가능 (전체만 가능)</td>
</tr>
<tr>
<td><strong>누락 필드</strong></td>
<td>⚠️ null 또는 기본값으로 변경</td>
</tr>
</tbody></table>
<h3 id="동작-방식">동작 방식</h3>
<pre><code>기존 데이터:
{
  &quot;name&quot;: &quot;홍길동&quot;,
  &quot;email&quot;: &quot;hong@example.com&quot;,
  &quot;age&quot;: 30,
  &quot;address&quot;: &quot;서울시&quot;
}

PUT 요청 (일부 필드만 전송):
{
  &quot;name&quot;: &quot;김철수&quot;,
  &quot;email&quot;: &quot;kim@example.com&quot;
}

결과 (전체 교체):
{
  &quot;name&quot;: &quot;김철수&quot;,
  &quot;email&quot;: &quot;kim@example.com&quot;,
  &quot;age&quot;: null,        ← ⚠️ 누락된 필드
  &quot;address&quot;: null     ← ⚠️ null로 변경됨
}</code></pre><h3 id="사용-예시-2">사용 예시</h3>
<h4 id="사용자-정보-전체-수정">사용자 정보 전체 수정</h4>
<pre><code class="language-http">PUT /api/users/123 HTTP/1.1
Host: example.com
Content-Type: application/json

{
  &quot;name&quot;: &quot;홍길동&quot;,
  &quot;email&quot;: &quot;hong@example.com&quot;,
  &quot;age&quot;: 31,
  &quot;address&quot;: &quot;서울시 강남구&quot;,
  &quot;phone&quot;: &quot;010-1234-5678&quot;
}</code></pre>
<h4 id="리소스-생성-없으면">리소스 생성 (없으면)</h4>
<pre><code class="language-http">PUT /api/users/999 HTTP/1.1
Host: example.com
Content-Type: application/json

{
  &quot;name&quot;: &quot;이영희&quot;,
  &quot;email&quot;: &quot;lee@example.com&quot;
}

→ ID 999가 없으면 생성, 있으면 전체 교체</code></pre>
<h3 id="코드-예시-2">코드 예시</h3>
<pre><code class="language-javascript">// Fetch API
fetch(&#39;https://api.example.com/users/123&#39;, {
  method: &#39;PUT&#39;,
  headers: {
    &#39;Content-Type&#39;: &#39;application/json&#39;,
  },
  body: JSON.stringify({
    name: &#39;홍길동&#39;,
    email: &#39;hong@example.com&#39;,
    age: 31,
    address: &#39;서울시 강남구&#39;
  })
})
  .then(response =&gt; response.json())
  .then(data =&gt; console.log(data));

// Axios
axios.put(&#39;https://api.example.com/users/123&#39;, {
  name: &#39;홍길동&#39;,
  email: &#39;hong@example.com&#39;,
  age: 31,
  address: &#39;서울시 강남구&#39;
})
  .then(response =&gt; console.log(response.data));</code></pre>
<hr>
<h2 id="4️⃣-patch---부분-수정">4️⃣ PATCH - 부분 수정</h2>
<h3 id="개념-3">개념</h3>
<p><strong>PATCH</strong>는 데이터의 일부만 수정하기 위한 용도로 사용합니다.</p>
<pre><code class="language-http">PATCH /api/users/123 HTTP/1.1
Host: example.com
Content-Type: application/json

{
  &quot;age&quot;: 31
}</code></pre>
<h3 id="특징-3">특징</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>용도</strong></td>
<td>리소스 일부만 수정</td>
</tr>
<tr>
<td><strong>멱등성</strong></td>
<td>❌ 없음 (구현에 따라 다름)</td>
</tr>
<tr>
<td><strong>부분 수정</strong></td>
<td>✅ 가능</td>
</tr>
<tr>
<td><strong>누락 필드</strong></td>
<td>✅ 기존 값 유지</td>
</tr>
</tbody></table>
<h3 id="put-vs-patch-비교">PUT vs PATCH 비교</h3>
<pre><code>기존 데이터:
{
  &quot;name&quot;: &quot;홍길동&quot;,
  &quot;email&quot;: &quot;hong@example.com&quot;,
  &quot;age&quot;: 30,
  &quot;address&quot;: &quot;서울시&quot;
}

┌─────────────────────────────────────────┐
│ PUT 요청 (전체 교체)                      │
├─────────────────────────────────────────┤
│ 요청:                                    │
│ { &quot;age&quot;: 31 }                           │
│                                         │
│ 결과:                                    │
│ {                                       │
│   &quot;age&quot;: 31,                            │
│   &quot;name&quot;: null,     ← ⚠️ 삭제됨         │
│   &quot;email&quot;: null,    ← ⚠️ 삭제됨         │
│   &quot;address&quot;: null   ← ⚠️ 삭제됨         │
│ }                                       │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│ PATCH 요청 (부분 수정)                    │
├─────────────────────────────────────────┤
│ 요청:                                    │
│ { &quot;age&quot;: 31 }                           │
│                                         │
│ 결과:                                    │
│ {                                       │
│   &quot;name&quot;: &quot;홍길동&quot;,     ← ✅ 유지       │
│   &quot;email&quot;: &quot;hong@...&quot;,  ← ✅ 유지       │
│   &quot;age&quot;: 31,            ← ✅ 수정       │
│   &quot;address&quot;: &quot;서울시&quot;   ← ✅ 유지       │
│ }                                       │
└─────────────────────────────────────────┘</code></pre><h3 id="사용-예시-3">사용 예시</h3>
<h4 id="나이만-수정">나이만 수정</h4>
<pre><code class="language-http">PATCH /api/users/123 HTTP/1.1
Host: example.com
Content-Type: application/json

{
  &quot;age&quot;: 31
}

→ 나이만 31로 변경, 나머지 필드는 그대로 유지</code></pre>
<h4 id="여러-필드-수정">여러 필드 수정</h4>
<pre><code class="language-http">PATCH /api/users/123 HTTP/1.1
Host: example.com
Content-Type: application/json

{
  &quot;age&quot;: 31,
  &quot;address&quot;: &quot;서울시 강남구&quot;
}

→ 나이와 주소만 변경, name과 email은 유지</code></pre>
<h3 id="코드-예시-3">코드 예시</h3>
<pre><code class="language-javascript">// Fetch API
fetch(&#39;https://api.example.com/users/123&#39;, {
  method: &#39;PATCH&#39;,
  headers: {
    &#39;Content-Type&#39;: &#39;application/json&#39;,
  },
  body: JSON.stringify({
    age: 31 // 나이만 수정
  })
})
  .then(response =&gt; response.json())
  .then(data =&gt; console.log(data));

// Axios
axios.patch(&#39;https://api.example.com/users/123&#39;, {
  age: 31
})
  .then(response =&gt; console.log(response.data));</code></pre>
<h3 id="실전-사용-케이스">실전 사용 케이스</h3>
<pre><code class="language-javascript">// ✅ 좋은 예: 상태만 변경
PATCH /api/orders/456
{
  &quot;status&quot;: &quot;shipped&quot;
}

// ✅ 좋은 예: 조회수 증가
PATCH /api/posts/789
{
  &quot;views&quot;: 101
}

// ❌ 나쁜 예: 모든 필드 전송 (PUT을 쓰세요)
PATCH /api/users/123
{
  &quot;name&quot;: &quot;홍길동&quot;,
  &quot;email&quot;: &quot;hong@example.com&quot;,
  &quot;age&quot;: 31,
  &quot;address&quot;: &quot;서울시&quot;,
  &quot;phone&quot;: &quot;010-1234-5678&quot;
}</code></pre>
<hr>
<h2 id="5️⃣-delete---삭제">5️⃣ DELETE - 삭제</h2>
<h3 id="개념-4">개념</h3>
<p><strong>DELETE</strong>는 특정 리소스의 삭제를 요청하는 데 사용합니다.</p>
<pre><code class="language-http">DELETE /api/users/123 HTTP/1.1
Host: example.com</code></pre>
<h3 id="특징-4">특징</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>용도</strong></td>
<td>리소스 삭제</td>
</tr>
<tr>
<td><strong>멱등성</strong></td>
<td>✅ 있음</td>
</tr>
<tr>
<td><strong>Body</strong></td>
<td>❌ 일반적으로 사용 안 함</td>
</tr>
<tr>
<td><strong>응답</strong></td>
<td>204 No Content 또는 200 OK</td>
</tr>
</tbody></table>
<h3 id="사용-예시-4">사용 예시</h3>
<h4 id="단일-리소스-삭제">단일 리소스 삭제</h4>
<pre><code class="language-http">DELETE /api/users/123 HTTP/1.1
Host: example.com

→ ID가 123인 사용자 삭제</code></pre>
<h4 id="응답-예시">응답 예시</h4>
<pre><code class="language-http">HTTP/1.1 204 No Content</code></pre>
<p>또는</p>
<pre><code class="language-http">HTTP/1.1 200 OK
Content-Type: application/json

{
  &quot;message&quot;: &quot;사용자가 성공적으로 삭제되었습니다.&quot;,
  &quot;deletedId&quot;: 123
}</code></pre>
<h3 id="코드-예시-4">코드 예시</h3>
<pre><code class="language-javascript">// Fetch API
fetch(&#39;https://api.example.com/users/123&#39;, {
  method: &#39;DELETE&#39;
})
  .then(response =&gt; {
    if (response.ok) {
      console.log(&#39;삭제 성공&#39;);
    }
  });

// Axios
axios.delete(&#39;https://api.example.com/users/123&#39;)
  .then(response =&gt; console.log(&#39;삭제 성공&#39;))
  .catch(error =&gt; console.error(&#39;삭제 실패:&#39;, error));</code></pre>
<h3 id="멱등성">멱등성</h3>
<pre><code>첫 번째 DELETE 요청:
DELETE /api/users/123 → ✅ 삭제 성공 (200 OK)

두 번째 DELETE 요청:
DELETE /api/users/123 → ✅ 이미 없음 (404 Not Found)

세 번째 DELETE 요청:
DELETE /api/users/123 → ✅ 여전히 없음 (404 Not Found)

→ 결과는 항상 동일: &quot;리소스가 없는 상태&quot;
→ 멱등성을 만족 ✅</code></pre><hr>
<h2 id="🎯-get-vs-post-심층-비교">🎯 GET vs POST 심층 비교</h2>
<h3 id="멱등성-idempotent">멱등성 (Idempotent)</h3>
<p><strong>동일한 연산을 여러 번 수행해도 동일한 결과가 나오는 성질</strong></p>
<h4 id="get의-멱등성">GET의 멱등성</h4>
<pre><code>첫 번째 요청:
GET /api/users/123 → { name: &quot;홍길동&quot;, age: 30 }

두 번째 요청:
GET /api/users/123 → { name: &quot;홍길동&quot;, age: 30 }

백 번째 요청:
GET /api/users/123 → { name: &quot;홍길동&quot;, age: 30 }

→ 항상 동일한 결과 ✅
→ 서버 상태를 변경하지 않음</code></pre><h4 id="post의-비멱등성">POST의 비멱등성</h4>
<pre><code>첫 번째 요청:
POST /api/orders → { orderId: 1, total: 50000 }

두 번째 요청:
POST /api/orders → { orderId: 2, total: 50000 }

세 번째 요청:
POST /api/orders → { orderId: 3, total: 50000 }

→ 요청마다 새로운 주문 생성 ❌
→ 결과가 매번 다름</code></pre><h3 id="안전성-safe">안전성 (Safe)</h3>
<p><strong>서버의 상태를 변경하지 않는 성질</strong></p>
<pre><code>GET    → ✅ 안전함 (조회만 함)
POST   → ❌ 안전하지 않음 (생성)
PUT    → ❌ 안전하지 않음 (수정)
PATCH  → ❌ 안전하지 않음 (수정)
DELETE → ❌ 안전하지 않음 (삭제)</code></pre><h3 id="http-method별-멱등성과-안전성">HTTP Method별 멱등성과 안전성</h3>
<table>
<thead>
<tr>
<th>Method</th>
<th>멱등성</th>
<th>안전성</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>GET</strong></td>
<td>✅</td>
<td>✅</td>
<td>조회만 하므로 둘 다 만족</td>
</tr>
<tr>
<td><strong>POST</strong></td>
<td>❌</td>
<td>❌</td>
<td>매번 새 리소스 생성</td>
</tr>
<tr>
<td><strong>PUT</strong></td>
<td>✅</td>
<td>❌</td>
<td>같은 데이터로 여러 번 요청해도 결과 동일</td>
</tr>
<tr>
<td><strong>PATCH</strong></td>
<td>⚠️</td>
<td>❌</td>
<td>구현에 따라 다름</td>
</tr>
<tr>
<td><strong>DELETE</strong></td>
<td>✅</td>
<td>❌</td>
<td>여러 번 삭제해도 &quot;없는 상태&quot;로 동일</td>
</tr>
</tbody></table>
<h3 id="상세-비교표">상세 비교표</h3>
<table>
<thead>
<tr>
<th>비교 항목</th>
<th>GET</th>
<th>POST</th>
</tr>
</thead>
<tbody><tr>
<td><strong>용도</strong></td>
<td>조회 (Read)</td>
<td>생성 (Create)</td>
</tr>
<tr>
<td><strong>데이터 위치</strong></td>
<td>URL (Query String)</td>
<td>Body</td>
</tr>
<tr>
<td><strong>데이터 노출</strong></td>
<td>⚠️ URL에 노출</td>
<td>✅ Body에 숨김</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>
<tr>
<td><strong>멱등성</strong></td>
<td>✅ 있음</td>
<td>❌ 없음</td>
</tr>
<tr>
<td><strong>안전성</strong></td>
<td>✅ 안전</td>
<td>❌ 안전하지 않음</td>
</tr>
</tbody></table>
<h3 id="실전-사용-가이드">실전 사용 가이드</h3>
<h4 id="get을-사용해야-할-때">GET을 사용해야 할 때</h4>
<pre><code class="language-javascript">✅ 게시글 목록 조회
GET /api/posts

✅ 사용자 정보 조회
GET /api/users/123

✅ 검색 (간단한 쿼리)
GET /api/search?q=nextjs

✅ 필터링
GET /api/products?category=electronics&amp;price_max=50000</code></pre>
<h4 id="post를-사용해야-할-때">POST를 사용해야 할 때</h4>
<pre><code class="language-javascript">✅ 회원가입
POST /api/users

✅ 로그인
POST /api/auth/login

✅ 주문 생성
POST /api/orders

✅ 댓글 작성
POST /api/posts/123/comments

✅ 복잡한 검색 (많은 조건)
POST /api/search
{
  &quot;filters&quot;: { ... },
  &quot;sort&quot;: { ... },
  &quot;pagination&quot;: { ... }
}

✅ 파일 업로드
POST /api/upload</code></pre>
<hr>
<h2 id="📊-전체-http-method-비교">📊 전체 HTTP Method 비교</h2>
<h3 id="빠른-참조표">빠른 참조표</h3>
<table>
<thead>
<tr>
<th>Method</th>
<th>용도</th>
<th>멱등성</th>
<th>안전성</th>
<th>캐싱</th>
<th>Body</th>
</tr>
</thead>
<tbody><tr>
<td><strong>GET</strong></td>
<td>조회</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>❌</td>
</tr>
<tr>
<td><strong>POST</strong></td>
<td>생성</td>
<td>❌</td>
<td>❌</td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td><strong>PUT</strong></td>
<td>전체 수정/생성</td>
<td>✅</td>
<td>❌</td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td><strong>PATCH</strong></td>
<td>부분 수정</td>
<td>⚠️</td>
<td>❌</td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td><strong>DELETE</strong></td>
<td>삭제</td>
<td>✅</td>
<td>❌</td>
<td>❌</td>
<td>❌*</td>
</tr>
</tbody></table>
<p>*DELETE는 Body를 사용할 수 있지만 일반적으로 사용하지 않음</p>
<h3 id="crud-매핑">CRUD 매핑</h3>
<table>
<thead>
<tr>
<th>CRUD</th>
<th>HTTP Method</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Create</strong></td>
<td>POST</td>
<td><code>POST /api/users</code></td>
</tr>
<tr>
<td><strong>Read</strong></td>
<td>GET</td>
<td><code>GET /api/users/123</code></td>
</tr>
<tr>
<td><strong>Update</strong></td>
<td>PUT / PATCH</td>
<td><code>PUT /api/users/123</code></td>
</tr>
<tr>
<td><strong>Delete</strong></td>
<td>DELETE</td>
<td><code>DELETE /api/users/123</code></td>
</tr>
</tbody></table>
<h3 id="restful-api-설계-예시">RESTful API 설계 예시</h3>
<pre><code>리소스: 사용자 (Users)

┌────────────────────────────────────────────────┐
│ 목록 조회                                       │
│ GET /api/users                                 │
│ → 모든 사용자 목록 반환                          │
├────────────────────────────────────────────────┤
│ 상세 조회                                       │
│ GET /api/users/123                             │
│ → ID가 123인 사용자 정보 반환                    │
├────────────────────────────────────────────────┤
│ 생성                                           │
│ POST /api/users                                │
│ → 새로운 사용자 생성                            │
├────────────────────────────────────────────────┤
│ 전체 수정                                       │
│ PUT /api/users/123                             │
│ → ID가 123인 사용자 정보 전체 교체              │
├────────────────────────────────────────────────┤
│ 부분 수정                                       │
│ PATCH /api/users/123                           │
│ → ID가 123인 사용자 정보 일부 수정              │
├────────────────────────────────────────────────┤
│ 삭제                                           │
│ DELETE /api/users/123                          │
│ → ID가 123인 사용자 삭제                        │
└────────────────────────────────────────────────┘</code></pre><hr>
<h2 id="🎨-실전-시나리오">🎨 실전 시나리오</h2>
<h3 id="시나리오-1-전자상거래">시나리오 1: 전자상거래</h3>
<pre><code class="language-javascript">// 1. 상품 목록 조회
GET /api/products?category=electronics&amp;page=1

// 2. 상품 상세 조회
GET /api/products/789

// 3. 장바구니에 추가
POST /api/cart
{
  &quot;productId&quot;: 789,
  &quot;quantity&quot;: 2
}

// 4. 장바구니 수량 변경
PATCH /api/cart/items/123
{
  &quot;quantity&quot;: 3
}

// 5. 주문 생성
POST /api/orders
{
  &quot;items&quot;: [...],
  &quot;shippingAddress&quot;: {...}
}

// 6. 주문 취소
DELETE /api/orders/456</code></pre>
<h3 id="시나리오-2-소셜-미디어">시나리오 2: 소셜 미디어</h3>
<pre><code class="language-javascript">// 1. 피드 조회
GET /api/feed?page=1&amp;limit=20

// 2. 게시글 작성
POST /api/posts
{
  &quot;content&quot;: &quot;안녕하세요!&quot;,
  &quot;images&quot;: [...]
}

// 3. 게시글 수정 (전체)
PUT /api/posts/123
{
  &quot;content&quot;: &quot;수정된 내용&quot;,
  &quot;images&quot;: [...]
}

// 4. 좋아요 추가
POST /api/posts/123/like

// 5. 댓글 작성
POST /api/posts/123/comments
{
  &quot;content&quot;: &quot;좋은 글이네요!&quot;
}

// 6. 게시글 삭제
DELETE /api/posts/123</code></pre>
<h3 id="시나리오-3-사용자-관리">시나리오 3: 사용자 관리</h3>
<pre><code class="language-javascript">// 1. 회원가입
POST /api/auth/register
{
  &quot;email&quot;: &quot;user@example.com&quot;,
  &quot;password&quot;: &quot;password123&quot;,
  &quot;name&quot;: &quot;홍길동&quot;
}

// 2. 로그인
POST /api/auth/login
{
  &quot;email&quot;: &quot;user@example.com&quot;,
  &quot;password&quot;: &quot;password123&quot;
}

// 3. 프로필 조회
GET /api/users/me

// 4. 프로필 이미지만 변경
PATCH /api/users/me
{
  &quot;profileImage&quot;: &quot;https://...&quot;
}

// 5. 비밀번호 변경
PATCH /api/users/me/password
{
  &quot;currentPassword&quot;: &quot;old&quot;,
  &quot;newPassword&quot;: &quot;new&quot;
}

// 6. 회원탈퇴
DELETE /api/users/me</code></pre>
<hr>
<h2 id="💡-best-practices">💡 Best Practices</h2>
<h3 id="1-올바른-http-method-선택">1. 올바른 HTTP Method 선택</h3>
<pre><code>✅ 좋은 예:
GET    /api/users        → 목록 조회
GET    /api/users/123    → 상세 조회
POST   /api/users        → 생성
PUT    /api/users/123    → 전체 수정
PATCH  /api/users/123    → 부분 수정
DELETE /api/users/123    → 삭제

❌ 나쁜 예:
GET /api/createUser      → POST를 사용하세요
GET /api/deleteUser/123  → DELETE를 사용하세요
POST /api/getUsers       → GET을 사용하세요</code></pre><h3 id="2-url-설계">2. URL 설계</h3>
<pre><code>✅ 명사 사용 (리소스 중심)
/api/users
/api/products
/api/orders

❌ 동사 사용 (행동 중심)
/api/getUsers
/api/createProduct
/api/deleteOrder</code></pre><h3 id="3-상태-코드-사용">3. 상태 코드 사용</h3>
<table>
<thead>
<tr>
<th>Method</th>
<th>성공 시</th>
<th>실패 시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>GET</strong></td>
<td>200 OK</td>
<td>404 Not Found</td>
</tr>
<tr>
<td><strong>POST</strong></td>
<td>201 Created</td>
<td>400 Bad Request</td>
</tr>
<tr>
<td><strong>PUT</strong></td>
<td>200 OK</td>
<td>404 Not Found</td>
</tr>
<tr>
<td><strong>PATCH</strong></td>
<td>200 OK</td>
<td>404 Not Found</td>
</tr>
<tr>
<td><strong>DELETE</strong></td>
<td>204 No Content</td>
<td>404 Not Found</td>
</tr>
</tbody></table>
<h3 id="4-보안">4. 보안</h3>
<pre><code>✅ HTTPS 사용
✅ 인증 토큰은 Header에
✅ 민감 정보는 POST/PUT/PATCH의 Body에
✅ CORS 설정
✅ Rate Limiting

❌ GET으로 민감 정보 전송
❌ 인증 없이 수정/삭제 허용</code></pre><hr>
<h2 id="❓-자주-묻는-질문">❓ 자주 묻는 질문</h2>
<h3 id="q1-get과-post-중-어떤-것을-사용해야-할까요">Q1. GET과 POST 중 어떤 것을 사용해야 할까요?</h3>
<pre><code>✅ GET을 사용하세요:
- 데이터 조회
- 검색 (간단한 쿼리)
- 필터링
- 페이지네이션
- 캐싱이 필요한 경우

✅ POST를 사용하세요:
- 데이터 생성
- 로그인/회원가입
- 파일 업로드
- 복잡한 검색 조건
- 민감한 정보 전송</code></pre><h3 id="q2-put과-patch의-차이는-무엇인가요">Q2. PUT과 PATCH의 차이는 무엇인가요?</h3>
<pre><code>PUT:
- 리소스 전체 교체
- 누락된 필드는 null
- 멱등성 있음

PATCH:
- 리소스 일부만 수정
- 누락된 필드는 유지
- 멱등성 구현에 따라 다름

예시:
{ name: &quot;홍길동&quot;, age: 30, email: &quot;hong@example.com&quot; }

PUT { age: 31 }
→ { age: 31, name: null, email: null }

PATCH { age: 31 }
→ { name: &quot;홍길동&quot;, age: 31, email: &quot;hong@example.com&quot; }</code></pre><h3 id="q3-delete-요청에-body를-사용할-수-있나요">Q3. DELETE 요청에 Body를 사용할 수 있나요?</h3>
<pre><code>기술적으로는 가능하지만, 권장하지 않습니다.

✅ 권장:
DELETE /api/users/123

❌ 비권장:
DELETE /api/users
Body: { id: 123 }

이유:
- RESTful 관례에 맞지 않음
- 일부 프록시/클라이언트가 무시할 수 있음
- URL로 충분히 표현 가능</code></pre><h3 id="q4-멱등성이-왜-중요한가요">Q4. 멱등성이 왜 중요한가요?</h3>
<pre><code>멱등성이 있으면:
✅ 네트워크 오류 시 안전하게 재시도 가능
✅ 캐싱 전략 수립 가능
✅ 예측 가능한 API 동작

예시:
PUT /api/users/123 { name: &quot;홍길동&quot; }
→ 네트워크 오류로 응답 못 받음
→ 다시 요청해도 안전 ✅ (결과 동일)

POST /api/orders { ... }
→ 네트워크 오류로 응답 못 받음
→ 다시 요청하면 중복 주문 발생 ⚠️</code></pre><hr>
<h2 id="🎓-요약">🎓 요약</h2>
<h3 id="http-method-한눈에-보기">HTTP Method 한눈에 보기</h3>
<pre><code>GET     → 🔍 조회 (안전, 멱등, 캐시)
POST    → ➕ 생성 (비안전, 비멱등, 노캐시)
PUT     → 🔄 전체 수정 (비안전, 멱등, 노캐시)
PATCH   → ✏️ 부분 수정 (비안전, 비멱등*, 노캐시)
DELETE  → 🗑️ 삭제 (비안전, 멱등, 노캐시)

*PATCH의 멱등성은 구현에 따라 다름</code></pre><h3 id="선택-가이드">선택 가이드</h3>
<pre><code>데이터를 조회하고 싶다
→ GET

데이터를 생성하고 싶다
→ POST

데이터 전체를 바꾸고 싶다
→ PUT

데이터 일부만 바꾸고 싶다
→ PATCH

데이터를 삭제하고 싶다
→ DELETE</code></pre><h3 id="핵심-원칙">핵심 원칙</h3>
<ol>
<li><strong>의미에 맞는 Method 사용</strong>: GET은 조회, POST는 생성</li>
<li><strong>RESTful 설계</strong>: 명사 기반 URL + HTTP Method</li>
<li><strong>멱등성 고려</strong>: 안전한 재시도 가능하도록</li>
<li><strong>보안</strong>: HTTPS + 적절한 인증/인가</li>
<li><strong>표준 상태 코드</strong>: 일관된 응답</li>
</ol>
<hr>
<h2 id="🔗-추가-리소스">🔗 추가 리소스</h2>
<ul>
<li><a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Methods">MDN: HTTP Request Methods</a></li>
<li><a href="https://restfulapi.net/">REST API Tutorial</a></li>
<li><a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Status">HTTP 상태 코드</a></li>
</ul>
<hr>
<blockquote>
<p>💡 <strong>TIP</strong>: API를 설계할 때는 항상 &quot;이 요청이 서버의 상태를 변경하는가?&quot;를 먼저 생각하세요. 변경하지 않으면 GET, 변경한다면 POST/PUT/PATCH/DELETE를 사용하면 됩니다!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 짧은 간단 지식 정리 - HTTP와 HTTPS]]></title>
            <link>https://velog.io/@lee_1124/HTTP%EC%99%80-HTTPS</link>
            <guid>https://velog.io/@lee_1124/HTTP%EC%99%80-HTTPS</guid>
            <pubDate>Wed, 03 Dec 2025 06:34:56 GMT</pubDate>
            <description><![CDATA[<h2 id="🌐-http-hypertext-transfer-protocol">🌐 HTTP (HyperText Transfer Protocol)</h2>
<h3 id="개념">개념</h3>
<p><strong>HTTP</strong>는 서버와 클라이언트 모델에서 데이터를 주고받기 위한 프로토콜입니다.</p>
<ul>
<li>인터넷에서 웹 브라우저와 서버 간의 통신 규약</li>
<li>1989년 팀 버너스리가 처음 설계</li>
<li>기본 포트: <strong>80번</strong></li>
<li>연결 방식: 비연결성(Connectionless), 무상태(Stateless)</li>
</ul>
<h3 id="특징">특징</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>프로토콜</strong></td>
<td>응용 계층(Application Layer) 프로토콜</td>
</tr>
<tr>
<td><strong>전송 방식</strong></td>
<td>평문(Plain Text) 전송</td>
</tr>
<tr>
<td><strong>보안</strong></td>
<td>암호화 없음</td>
</tr>
<tr>
<td><strong>속도</strong></td>
<td>상대적으로 빠름</td>
</tr>
<tr>
<td><strong>비용</strong></td>
<td>추가 비용 없음</td>
</tr>
</tbody></table>
<h3 id="구조">구조</h3>
<pre><code>클라이언트 ──── HTTP 요청 ───→ 서버
            (평문 데이터)
클라이언트 ←─── HTTP 응답 ──── 서버
            (평문 데이터)</code></pre><hr>
<h2 id="🔒-https-hypertext-transfer-protocol-secure">🔒 HTTPS (HyperText Transfer Protocol Secure)</h2>
<h3 id="개념-1">개념</h3>
<p><strong>HTTPS</strong>는 HTTP에 <strong>SSL/TLS</strong>라는 암호화 기반 인터넷 보안 프로토콜을 추가한 것입니다.</p>
<ul>
<li>HTTP + SSL/TLS = HTTPS</li>
<li>기본 포트: <strong>443번</strong></li>
<li>데이터 암호화, 인증, 무결성 보장</li>
</ul>
<h3 id="ssl-vs-tls">SSL vs TLS</h3>
<table>
<thead>
<tr>
<th>프로토콜</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>SSL</strong></td>
<td>Secure Sockets Layer (구버전)</td>
</tr>
<tr>
<td><strong>TLS</strong></td>
<td>Transport Layer Security (현재 표준)</td>
</tr>
</tbody></table>
<blockquote>
<p>💡 현재는 TLS를 사용하지만, 관습적으로 SSL이라는 용어를 함께 사용합니다.</p>
</blockquote>
<h3 id="특징-1">특징</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>보안</strong></td>
<td>데이터 암호화, 인증, 무결성 검증</td>
</tr>
<tr>
<td><strong>속도</strong></td>
<td>암호화/복호화로 인한 약간의 오버헤드</td>
</tr>
<tr>
<td><strong>비용</strong></td>
<td>SSL/TLS 인증서 발급 및 유지 비용</td>
</tr>
<tr>
<td><strong>SEO</strong></td>
<td>구글 검색 순위 가산점</td>
</tr>
</tbody></table>
<h3 id="구조-1">구조</h3>
<pre><code>클라이언트 ──── HTTPS 요청 ───→ 서버
         (암호화된 데이터 🔐)
클라이언트 ←─── HTTPS 응답 ──── 서버
         (암호화된 데이터 🔐)</code></pre><hr>
<h2 id="🆚-http-vs-https-차이점">🆚 HTTP vs HTTPS 차이점</h2>
<h3 id="1-보안성-🔐">1. 보안성 🔐</h3>
<h4 id="http">HTTP</h4>
<ul>
<li>❌ 암호화되지 않은 평문 전송</li>
<li>❌ 중간자 공격(MITM)에 취약</li>
<li>❌ 데이터 도청 가능</li>
<li>❌ 데이터 변조 가능</li>
</ul>
<pre><code>사용자 → [ID: admin, PW: 1234] → 서버
        ↓ (누구나 볼 수 있음!)
     해커 👀</code></pre><h4 id="https">HTTPS</h4>
<ul>
<li>✅ 데이터 암호화</li>
<li>✅ 안전한 데이터 전송</li>
<li>✅ 데이터 무결성 보장</li>
<li>✅ 서버 신원 인증</li>
</ul>
<pre><code>사용자 → [🔐 암호화된 데이터] → 서버
        ↓ (해독 불가능!)
     해커 ❓❓❓</code></pre><h3 id="2-seo-검색엔진-최적화-📈">2. SEO (검색엔진 최적화) 📈</h3>
<h4 id="구글의-https-우대-정책">구글의 HTTPS 우대 정책</h4>
<ul>
<li>✅ <strong>2014년</strong>: 구글이 HTTPS를 검색 순위 결정 요소로 공식 발표</li>
<li>✅ <strong>가산점 부여</strong>: HTTPS 사이트에 순위 가산점</li>
<li>✅ <strong>Chrome 경고</strong>: HTTP 사이트를 &quot;안전하지 않음&quot;으로 표시</li>
</ul>
<pre><code>🔒 https://example.com    ← 우선 노출
⚠️  http://example.com     ← 불이익</code></pre><h3 id="3-성능-⚡">3. 성능 ⚡</h3>
<h4 id="과거">과거</h4>
<pre><code>HTTP  → 빠름 ✅
HTTPS → 느림 (암호화/복호화 오버헤드) ❌</code></pre><h4 id="현재-2025년">현재 (2025년)</h4>
<pre><code>HTTP  → 빠름 ✅
HTTPS → 거의 동일 (하드웨어 발전 + HTTP/2, HTTP/3) ✅</code></pre><p><strong>성능 차이가 미미한 이유</strong>:</p>
<ul>
<li>현대 프로세서의 AES-NI 명령어 세트</li>
<li>HTTP/2, HTTP/3의 성능 최적화</li>
<li>TLS 1.3의 핸드셰이크 간소화</li>
</ul>
<h3 id="4-비용-💰">4. 비용 💰</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>HTTP</th>
<th>HTTPS</th>
</tr>
</thead>
<tbody><tr>
<td>인증서 비용</td>
<td>무료</td>
<td>무료~유료</td>
</tr>
<tr>
<td>유지보수</td>
<td>간단</td>
<td>인증서 갱신 필요</td>
</tr>
</tbody></table>
<p><strong>HTTPS 인증서 옵션</strong>:</p>
<ul>
<li>🆓 <strong>Let&#39;s Encrypt</strong>: 무료 인증서 (90일 갱신)</li>
<li>💵 <strong>상용 인증서</strong>: Comodo, DigiCert 등 (연간 수만원~수십만원)</li>
<li>💼 <strong>EV 인증서</strong>: 회사명 표시 (연간 수백만원)</li>
</ul>
<hr>
<h2 id="🔑-https-암호화-방식">🔑 HTTPS 암호화 방식</h2>
<p>HTTPS는 <strong>대칭키</strong>와 <strong>비대칭키</strong> 암호화 방식을 <strong>하이브리드</strong>로 사용합니다.</p>
<h3 id="대칭키-암호화-symmetric-key">대칭키 암호화 (Symmetric Key)</h3>
<p><strong>동일한 키</strong>로 암호화와 복호화를 수행합니다.</p>
<pre><code>평문 ──[세션키로 암호화]──→ 암호문
암호문 ──[같은 세션키로 복호화]──→ 평문</code></pre><p><strong>특징</strong>:</p>
<ul>
<li>⚡ <strong>빠른 속도</strong>: 연산이 간단</li>
<li>🔐 <strong>데이터 암호화</strong>: 실제 통신 데이터 암호화에 사용</li>
<li>⚠️ <strong>키 공유 문제</strong>: 키를 안전하게 교환하는 것이 어려움</li>
</ul>
<p><strong>예시</strong>: AES, DES, 3DES</p>
<h3 id="비대칭키-암호화-asymmetric-key">비대칭키 암호화 (Asymmetric Key)</h3>
<p><strong>공개키</strong>와 <strong>개인키</strong> 쌍을 사용합니다.</p>
<pre><code>평문 ──[공개키로 암호화]──→ 암호문 ──[개인키로 복호화]──→ 평문
평문 ──[개인키로 암호화]──→ 암호문 ──[공개키로 복호화]──→ 평문</code></pre><p><strong>핵심 원리</strong>:</p>
<ul>
<li>🔓 <strong>공개키</strong>: 누구나 볼 수 있는 키 (암호화용)</li>
<li>🔐 <strong>개인키</strong>: 소유자만 갖고 있는 키 (복호화용)</li>
<li>🔄 <strong>상호 변환</strong>: 공개키로 암호화 → 개인키로 복호화 / 개인키로 암호화 → 공개키로 복호화</li>
</ul>
<p><strong>특징</strong>:</p>
<ul>
<li>🐌 <strong>느린 속도</strong>: 연산이 복잡</li>
<li>🔑 <strong>키 교환</strong>: 세션키를 안전하게 전달하는데 사용</li>
<li>✅ <strong>안전성</strong>: 공개키가 노출되어도 안전</li>
</ul>
<p><strong>예시</strong>: RSA, ECC</p>
<h3 id="하이브리드-방식">하이브리드 방식</h3>
<p>HTTPS는 두 방식의 장점을 결합합니다:</p>
<pre><code>1단계: 비대칭키로 세션키 교환 (느리지만 안전)
   ↓
2단계: 대칭키(세션키)로 데이터 암호화 (빠르고 효율적)</code></pre><table>
<thead>
<tr>
<th>단계</th>
<th>암호화 방식</th>
<th>용도</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><strong>핸드셰이크</strong></td>
<td>비대칭키</td>
<td>세션키 교환</td>
<td>느리지만 안전</td>
</tr>
<tr>
<td><strong>데이터 전송</strong></td>
<td>대칭키</td>
<td>실제 데이터 암호화</td>
<td>빠르고 효율적</td>
</tr>
</tbody></table>
<hr>
<h2 id="🔐-https-세션키-공유-과정-ssltls-handshake">🔐 HTTPS 세션키 공유 과정 (SSL/TLS Handshake)</h2>
<h3 id="전체-흐름도">전체 흐름도</h3>
<pre><code>클라이언트                               서버
    |                                    |
    |────① Client Hello─────────────────→|
    |                                    |
    |←───② Server Hello + 인증서─────────|
    |                                    |
    |────③ 세션키 생성 및 암호화─────────→|
    |     (서버 공개키로 암호화)           |
    |                                    |
    |←───④ 완료 (세션키 복호화 완료)──────|
    |                                    |
    |═══⑤ 암호화된 데이터 통신 시작═══════|</code></pre><h3 id="상세-단계별-설명">상세 단계별 설명</h3>
<h4 id="🔹-step-1-클라이언트-→-서버-최초-연결-시도">🔹 Step 1: 클라이언트 → 서버 (최초 연결 시도)</h4>
<pre><code>클라이언트: &quot;안녕하세요! HTTPS 연결을 시작하고 싶습니다.&quot;
           &quot;제가 지원하는 암호화 방식: [TLS 1.3, AES-256, ...]&quot;</code></pre><p><strong>전송 정보</strong>:</p>
<ul>
<li>지원하는 TLS 버전</li>
<li>지원하는 암호화 알고리즘 목록</li>
<li>무작위 데이터 (랜덤 바이트)</li>
</ul>
<hr>
<h4 id="🔹-step-2-서버-→-클라이언트-인증서-전달">🔹 Step 2: 서버 → 클라이언트 (인증서 전달)</h4>
<pre><code>서버: &quot;안녕하세요! 여기 제 신분증(인증서)입니다.&quot;</code></pre><p><strong>서버가 전달하는 것</strong>:</p>
<ul>
<li>✅ <strong>SSL/TLS 인증서</strong><ul>
<li>서버의 공개키 포함</li>
<li>CA(인증기관)의 개인키로 암호화됨</li>
<li>서버 정보 (도메인, 유효기간 등)</li>
</ul>
</li>
</ul>
<h5 id="📜-인증서는-어떻게-만들어졌을까">📜 인증서는 어떻게 만들어졌을까?</h5>
<p><strong>사전 준비 과정</strong>:</p>
<pre><code>1. 서버 관리자
   └─→ 개인키/공개키 쌍 생성
   └─→ 공개키 + 서버정보를 CA에 제출
   └─→ 💰 비용 지불

2. CA (Certificate Authority)
   └─→ 서버 신원 확인
   └─→ 인증서 생성 (서버 공개키 포함)
   └─→ CA의 개인키로 인증서 암호화 🔐
   └─→ 서버에게 인증서 발급

3. 서버
   └─→ 발급받은 인증서 저장
   └─→ 클라이언트 요청 시 전달 준비 완료</code></pre><p><strong>주요 CA 기업</strong>:</p>
<ul>
<li>Let&#39;s Encrypt (무료)</li>
<li>DigiCert</li>
<li>Comodo</li>
<li>GlobalSign</li>
</ul>
<hr>
<h4 id="🔹-step-3-클라이언트의-인증서-검증">🔹 Step 3: 클라이언트의 인증서 검증</h4>
<pre><code>클라이언트: &quot;이 인증서가 진짜인지 확인해볼게요!&quot;</code></pre><p><strong>검증 과정</strong>:</p>
<pre><code>1. 브라우저에 내장된 CA 공개키 확인
   └─→ 주요 CA들의 공개키는 브라우저에 사전 설치됨

2. CA 공개키로 인증서 복호화 시도
   └─→ 성공 = 진짜 CA가 발급한 인증서 ✅
   └─→ 실패 = 위조 인증서 ❌

3. 인증서 내용 확인
   ├─→ 도메인 일치 여부
   ├─→ 유효기간 확인
   └─→ 폐기 여부 확인 (CRL, OCSP)</code></pre><p><strong>검증 결과</strong>:</p>
<ul>
<li>✅ <strong>성공</strong>: 인증서에서 서버의 공개키 추출</li>
<li>❌ <strong>실패</strong>: &quot;안전하지 않은 연결&quot; 경고 표시</li>
</ul>
<hr>
<h4 id="🔹-step-4-클라이언트-→-서버-세션키-전달">🔹 Step 4: 클라이언트 → 서버 (세션키 전달)</h4>
<pre><code>클라이언트: &quot;이제 우리가 사용할 비밀번호(세션키)를 
           안전하게 보내드릴게요!&quot;</code></pre><p><strong>과정</strong>:</p>
<pre><code>1. 세션키 생성
   └─→ 무작위 대칭키 생성 (예: AES-256 키)

2. 클라이언트에 세션키 저장
   └─→ 메모리에 보관

3. 서버 공개키로 세션키 암호화 🔐
   └─→ 인증서에서 추출한 서버 공개키 사용
   └─→ 암호화된 세션키 생성

4. 암호화된 세션키를 서버로 전송
   └─→ 중간에 누가 가로채도 복호화 불가능!</code></pre><p><strong>비유</strong>:</p>
<pre><code>세션키 = &quot;secret123&quot;

암호화 전:  secret123  (해커가 볼 수 있음 ❌)
       ↓ [서버 공개키로 암호화]
암호화 후:  8j#kL@9x...  (해커가 못 봄 ✅)</code></pre><hr>
<h4 id="🔹-step-5-서버의-세션키-복호화">🔹 Step 5: 서버의 세션키 복호화</h4>
<pre><code>서버: &quot;받았습니다! 제 개인키로 열어볼게요.&quot;</code></pre><p><strong>과정</strong>:</p>
<pre><code>1. 암호화된 세션키 수신
   └─→ 8j#kL@9x... (암호화된 데이터)

2. 서버의 개인키로 복호화 🔓
   └─→ 오직 서버만 갖고 있는 개인키 사용
   └─→ 복호화 성공: &quot;secret123&quot;

3. 서버에 세션키 저장
   └─→ 메모리에 보관</code></pre><p><strong>핵심</strong>:</p>
<ul>
<li>✅ <strong>서버만</strong> 개인키를 갖고 있음</li>
<li>✅ 따라서 <strong>서버만</strong> 세션키를 복호화 가능</li>
<li>✅ 중간에 누가 가로채도 무용지물</li>
</ul>
<hr>
<h4 id="🔹-step-6-암호화된-통신-시작">🔹 Step 6: 암호화된 통신 시작</h4>
<pre><code>클라이언트: &quot;이제 우리만의 비밀번호로 얘기해요!&quot;
서버: &quot;좋습니다! 보안 통신 시작!&quot;</code></pre><p><strong>이제 양쪽 모두 세션키를 가짐</strong>:</p>
<pre><code>클라이언트                          서버
    |                              |
[세션키: secret123]      [세션키: secret123]
    |                              |
    |═══ 암호화된 데이터 전송 ═══════|
    |     (대칭키 암호화)            |
    |                              |</code></pre><p><strong>실제 데이터 전송</strong>:</p>
<pre><code>1. 클라이언트 → 서버
   평문: &quot;안녕하세요&quot;
   ↓ [세션키로 암호화]
   전송: &quot;9k#mL@2p...&quot;

2. 서버
   수신: &quot;9k#mL@2p...&quot;
   ↓ [세션키로 복호화]
   평문: &quot;안녕하세요&quot;</code></pre><hr>
<h3 id="🎯-핵심-포인트-정리">🎯 핵심 포인트 정리</h3>
<h4 id="암호화복호화-규칙">암호화/복호화 규칙</h4>
<pre><code>✅ 공개키로 암호화 → 개인키로 복호화
✅ 개인키로 암호화 → 공개키로 복호화

❌ 공개키로 암호화 → 공개키로 복호화 (불가능!)
❌ 개인키로 암호화 → 개인키로 복호화 (불가능!)</code></pre><h4 id="세션키-교환의-보안성">세션키 교환의 보안성</h4>
<table>
<thead>
<tr>
<th>단계</th>
<th>암호화</th>
<th>위험도</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>세션키 전송</td>
<td>서버 공개키</td>
<td>안전 ✅</td>
<td>서버의 개인키만이 복호화 가능</td>
</tr>
<tr>
<td>실제 데이터 전송</td>
<td>세션키 (대칭키)</td>
<td>안전 ✅</td>
<td>세션키는 안전하게 교환됨</td>
</tr>
</tbody></table>
<h4 id="각-키의-역할">각 키의 역할</h4>
<table>
<thead>
<tr>
<th>키</th>
<th>소유자</th>
<th>공개 여부</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><strong>서버 공개키</strong></td>
<td>서버</td>
<td>공개 ✅</td>
<td>세션키 암호화</td>
</tr>
<tr>
<td><strong>서버 개인키</strong></td>
<td>서버</td>
<td>비공개 🔐</td>
<td>세션키 복호화</td>
</tr>
<tr>
<td><strong>CA 공개키</strong></td>
<td>브라우저</td>
<td>공개 ✅</td>
<td>인증서 검증</td>
</tr>
<tr>
<td><strong>CA 개인키</strong></td>
<td>CA</td>
<td>비공개 🔐</td>
<td>인증서 발급</td>
</tr>
<tr>
<td><strong>세션키</strong></td>
<td>양쪽</td>
<td>비공개 🔐</td>
<td>데이터 암호화/복호화</td>
</tr>
</tbody></table>
<hr>
<h3 id="🎨-전체-과정-시각화">🎨 전체 과정 시각화</h3>
<pre><code>준비 단계 (서버)
┌─────────────────────────────────────────┐
│ 서버: 개인키/공개키 생성                  │
│   └─→ CA에 공개키 + 정보 제출            │
│   └─→ CA가 개인키로 암호화한 인증서 발급  │
└─────────────────────────────────────────┘

연결 단계 (클라이언트 ↔ 서버)
┌─────────────────────────────────────────┐
│ ① 클라이언트: 연결 요청                   │
│    &quot;안녕하세요!&quot;                         │
│                                         │
│ ② 서버: 인증서 전달                       │
│    [CA 개인키로 암호화된 인증서]          │
│     └─ 서버 공개키 포함                  │
│                                         │
│ ③ 클라이언트: 인증서 검증                 │
│    [CA 공개키로 복호화] ✅                │
│     └─ 서버 공개키 추출                  │
│                                         │
│ ④ 클라이언트: 세션키 생성 및 전송         │
│    세션키 생성: &quot;secret123&quot;              │
│     ↓ [서버 공개키로 암호화]             │
│    전송: &quot;8j#kL@9x...&quot;                  │
│                                         │
│ ⑤ 서버: 세션키 복호화                     │
│    수신: &quot;8j#kL@9x...&quot;                  │
│     ↓ [서버 개인키로 복호화]             │
│    세션키: &quot;secret123&quot; ✅                │
│                                         │
│ ⑥ 암호화된 통신 시작                      │
│    [양쪽 모두 세션키 보유]                │
│     └─ 대칭키 암호화로 빠른 통신          │
└─────────────────────────────────────────┘</code></pre><hr>
<h3 id="❓-자주-묻는-질문">❓ 자주 묻는 질문</h3>
<p><strong>Q1. 왜 세션키를 사용하나요? 그냥 공개키/개인키로 통신하면 안 되나요?</strong></p>
<pre><code>A. 비대칭키는 연산이 느립니다!

비대칭키 암호화: 🐌🐌🐌 (느림)
대칭키 암호화:   ⚡⚡⚡ (빠름)

→ 세션키 교환만 비대칭키 (안전하게)
→ 실제 데이터는 대칭키 (빠르게)</code></pre><p><strong>Q2. 세션키를 매번 새로 만드나요?</strong></p>
<pre><code>A. 네, 세션마다 새로운 세션키를 생성합니다.

세션 1: session_key_abc123
세션 2: session_key_xyz789
세션 3: session_key_def456

→ 이전 세션의 키가 노출되어도 안전</code></pre><p><strong>Q3. CA 공개키는 어디서 오나요?</strong></p>
<pre><code>A. 브라우저와 OS에 사전 설치되어 있습니다.

Chrome, Firefox, Edge, Safari 등
└─→ 주요 CA들의 공개키 내장
    (Let&#39;s Encrypt, DigiCert, ...)</code></pre><hr>
<h2 id="🎯-실전-체크리스트">🎯 실전 체크리스트</h2>
<h3 id="웹사이트-운영자를-위한-https-적용-가이드">웹사이트 운영자를 위한 HTTPS 적용 가이드</h3>
<h4 id="✅-https-전환-체크리스트">✅ HTTPS 전환 체크리스트</h4>
<ul>
<li><input disabled="" type="checkbox"> SSL/TLS 인증서 발급<ul>
<li><input disabled="" type="checkbox"> 무료: Let&#39;s Encrypt</li>
<li><input disabled="" type="checkbox"> 유료: 상용 인증서</li>
</ul>
</li>
<li><input disabled="" type="checkbox"> 웹 서버에 인증서 설치</li>
<li><input disabled="" type="checkbox"> HTTP → HTTPS 리다이렉트 설정</li>
<li><input disabled="" type="checkbox"> Mixed Content 문제 해결<ul>
<li><input disabled="" type="checkbox"> 모든 리소스를 HTTPS로 로드</li>
</ul>
</li>
<li><input disabled="" type="checkbox"> HSTS 헤더 설정</li>
<li><input disabled="" type="checkbox"> 인증서 자동 갱신 설정</li>
<li><input disabled="" type="checkbox"> CDN HTTPS 적용</li>
<li><input disabled="" type="checkbox"> 서브도메인 인증서 확인</li>
</ul>
<h4 id="🔍-https-적용-확인-방법">🔍 HTTPS 적용 확인 방법</h4>
<pre><code class="language-bash"># 1. 브라우저 주소창 확인
🔒 자물쇠 아이콘 확인

# 2. 온라인 도구
https://www.ssllabs.com/ssltest/
→ A+ 등급 목표

# 3. 개발자 도구 콘솔
Mixed Content 경고 없는지 확인</code></pre>
<hr>
<h2 id="📚-추가-리소스">📚 추가 리소스</h2>
<h3 id="공식-문서">공식 문서</h3>
<ul>
<li><a href="https://developer.mozilla.org/ko/docs/Web/HTTP">MDN: HTTP</a></li>
<li><a href="https://developer.mozilla.org/ko/docs/Glossary/HTTPS">MDN: HTTPS</a></li>
<li><a href="https://letsencrypt.org/">Let&#39;s Encrypt 공식 사이트</a></li>
</ul>
<h3 id="도구">도구</h3>
<ul>
<li><a href="https://www.ssllabs.com/ssltest/">SSL Labs Test</a> - SSL/TLS 설정 테스트</li>
<li><a href="https://www.whynopadlock.com/">Why No Padlock?</a> - Mixed Content 찾기</li>
<li><a href="https://www.sslshopper.com/ssl-checker.html">SSL Checker</a> - 인증서 확인</li>
</ul>
<hr>
<h2 id="🎓-요약">🎓 요약</h2>
<h3 id="http-1">HTTP</h3>
<pre><code>✅ 빠른 속도
❌ 보안 취약
❌ SEO 불리
💰 무료</code></pre><h3 id="https-1">HTTPS</h3>
<pre><code>✅ 안전한 통신
✅ SEO 유리
✅ 사용자 신뢰
⚡ 성능 거의 동일 (현재)
💰 무료~유료 (Let&#39;s Encrypt 무료)</code></pre><h3 id="결론">결론</h3>
<p><strong>2025년 현재, HTTPS는 선택이 아닌 필수입니다!</strong></p>
<ul>
<li>🔐 사용자 데이터 보호</li>
<li>📈 검색 순위 향상</li>
<li>🏆 브랜드 신뢰도 증가</li>
<li>⚡ 성능 저하 미미</li>
<li>💰 무료 옵션 존재</li>
</ul>
<blockquote>
<p>💡 <strong>추천</strong>: 모든 웹사이트는 HTTPS를 적용하세요. Let&#39;s Encrypt를 사용하면 무료로 시작할 수 있습니다!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 16 업데이트]]></title>
            <link>https://velog.io/@lee_1124/Next.js-16-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8</link>
            <guid>https://velog.io/@lee_1124/Next.js-16-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8</guid>
            <pubDate>Wed, 03 Dec 2025 06:22:09 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>📅 릴리즈: 2025년 10월 21일<br>🔗 공식 문서: <a href="https://nextjs.org/blog/next-16">Next.js 16 Release</a></p>
</blockquote>
<h2 id="🎯-개요">🎯 개요</h2>
<p>Next.js 16은 성능, 캐싱 전략, 그리고 React 최신 기능과의 통합에 초점을 맞춘 메이저 업데이트입니다. 이번 릴리즈의 핵심은 <strong>명시적이고 예측 가능한 캐싱</strong>, <strong>빌드 성능 향상</strong>, 그리고 <strong>개발자 경험 개선</strong>입니다.</p>
<hr>
<h2 id="✨-주요-신기능">✨ 주요 신기능</h2>
<h3 id="1-cache-components---캐싱의-새로운-패러다임">1. Cache Components - 캐싱의 새로운 패러다임</h3>
<h4 id="개념">개념</h4>
<p>기존 App Router의 암묵적(implicit) 캐싱 방식에서 벗어나, <strong>명시적(explicit) 캐싱</strong>으로 전환되었습니다.</p>
<p>Cache Components는 <code>use cache</code> 디렉티브를 중심으로 하며, 페이지, 컴포넌트, 함수를 캐시 가능하게 만들 수 있습니다.</p>
<h4 id="주요-특징">주요 특징</h4>
<ul>
<li><strong>기본 동작 변경</strong>: 모든 동적 코드는 기본적으로 요청 시점에 실행되며, 캐싱은 완전히 opt-in 방식</li>
<li><strong>Partial Pre-Rendering (PPR) 완성</strong>: 정적 콘텐츠와 동적 콘텐츠를 하나의 페이지에서 조합 가능</li>
<li><strong>자동 캐시 키 생성</strong>: 컴파일러가 자동으로 캐시 키를 생성</li>
</ul>
<h4 id="설정-방법">설정 방법</h4>
<pre><code class="language-typescript">// next.config.ts
const nextConfig = {
  cacheComponents: true,
};
export default nextConfig;</code></pre>
<h4 id="사용-예시">사용 예시</h4>
<p><strong>1. 페이지 레벨 캐싱</strong></p>
<pre><code class="language-typescript">// app/blog/page.tsx
&quot;use cache&quot;;

export default async function BlogPage() {
  const posts = await fetchPosts();
  return (
    &lt;div&gt;
      {posts.map(post =&gt; (
        &lt;Article key={post.id} {...post} /&gt;
      ))}
    &lt;/div&gt;
  );
}</code></pre>
<p><strong>2. 컴포넌트 레벨 캐싱</strong></p>
<pre><code class="language-typescript">async function ProductList() {
  &quot;use cache&quot;;
  const products = await getProducts();
  return (
    &lt;&gt;
      {products.map(product =&gt; (
        &lt;p key={product.id}&gt;{product.name}&lt;/p&gt;
      ))}
    &lt;/&gt;
  );
}</code></pre>
<p><strong>3. 함수 레벨 캐싱</strong></p>
<pre><code class="language-typescript">async function getProducts() {
  &quot;use cache&quot;;
  cacheTag(&quot;products&quot;);
  const products = await db.query(&quot;SELECT * FROM products&quot;);
  return products;
}</code></pre>
<h4 id="관련-설정-제거">관련 설정 제거</h4>
<ul>
<li><code>experimental.ppr</code> 플래그 제거</li>
<li><code>experimental.dynamicIO</code> 플래그 제거</li>
<li><code>export const dynamic = &#39;force-dynamic&#39;</code> 불필요</li>
</ul>
<hr>
<h3 id="2-turbopack---안정화-및-기본-번들러-지정">2. Turbopack - 안정화 및 기본 번들러 지정</h3>
<h4 id="성능-개선">성능 개선</h4>
<p>프로덕션 빌드가 2-5배 빠르고, Fast Refresh가 최대 10배 향상되었습니다.</p>
<h4 id="주요-변경사항">주요 변경사항</h4>
<ul>
<li><strong>안정화</strong>: 베타에서 안정 버전으로 승격</li>
<li><strong>기본 번들러</strong>: 모든 새 Next.js 프로젝트의 기본 번들러</li>
<li><strong>광범위한 채택</strong>: Next.js 15.3+ 이상에서 개발 세션의 50%, 프로덕션 빌드의 20% 이상이 Turbopack 사용</li>
</ul>
<h4 id="webpack-사용-방법">Webpack 사용 방법</h4>
<p>여전히 webpack이 필요한 경우:</p>
<pre><code class="language-bash">next dev --webpack
next build --webpack</code></pre>
<h4 id="파일시스템-캐싱-베타">파일시스템 캐싱 (베타)</h4>
<p>대규모 프로젝트를 위한 추가 성능 향상 기능:</p>
<pre><code class="language-typescript">// next.config.ts
const nextConfig = {
  experimental: {
    turbopackFileSystemCacheForDev: true,
  },
};
export default nextConfig;</code></pre>
<p><strong>효과</strong>: 재시작 간 컴파일 아티팩트를 디스크에 저장하여 컴파일 시간 대폭 단축</p>
<hr>
<h3 id="3-개선된-캐싱-api">3. 개선된 캐싱 API</h3>
<h4 id="revalidatetag---업데이트됨"><code>revalidateTag()</code> - 업데이트됨</h4>
<p>이제 stale-while-revalidate (SWR) 동작을 위한 cacheLife 프로필이 두 번째 인자로 필수입니다.</p>
<pre><code class="language-typescript">import { revalidateTag } from &#39;next/cache&#39;;

// ✅ 권장: 대부분의 경우 &#39;max&#39; 사용
revalidateTag(&#39;blog-posts&#39;, &#39;max&#39;);

// 다른 내장 프로필 사용
revalidateTag(&#39;news-feed&#39;, &#39;hours&#39;);
revalidateTag(&#39;analytics&#39;, &#39;days&#39;);

// 커스텀 재검증 시간 사용
revalidateTag(&#39;products&#39;, { expire: 3600 });

// ⚠️ 더 이상 사용 불가
revalidateTag(&#39;blog-posts&#39;); // 에러!</code></pre>
<p><strong>사용 시나리오</strong>:</p>
<ul>
<li>최종 일관성을 허용할 수 있는 콘텐츠</li>
<li>사용자가 캐시된 데이터를 즉시 받고 백그라운드에서 재검증</li>
</ul>
<h4 id="updatetag---신규"><code>updateTag()</code> - 신규</h4>
<p>Server Action 전용 API로 read-your-writes 시맨틱을 제공합니다.</p>
<pre><code class="language-typescript">&#39;use server&#39;;
import { updateTag } from &#39;next/cache&#39;;

export async function updateUserProfile(userId: string, profile: Profile) {
  await db.users.update(userId, profile);

  // 캐시 만료 및 즉시 새로운 데이터 읽기
  updateTag(`user-${userId}`);
}</code></pre>
<p><strong>사용 시나리오</strong>:</p>
<ul>
<li>폼 제출</li>
<li>사용자 설정 변경</li>
<li>사용자가 즉시 변경사항을 봐야 하는 모든 경우</li>
</ul>
<h4 id="refresh---신규"><code>refresh()</code> - 신규</h4>
<p>캐시되지 않은 데이터만 새로고침하는 Server Action 전용 API입니다.</p>
<pre><code class="language-typescript">&#39;use server&#39;;
import { refresh } from &#39;next/cache&#39;;

export async function markNotificationAsRead(notificationId: string) {
  await db.notifications.markAsRead(notificationId);

  // 헤더의 알림 카운트만 새로고침 (캐시되지 않은 데이터)
  refresh();
}</code></pre>
<p><strong>사용 시나리오</strong>:</p>
<ul>
<li>알림 카운트</li>
<li>라이브 메트릭</li>
<li>상태 표시기</li>
<li>캐시되지 않은 동적 데이터</li>
</ul>
<hr>
<h3 id="4-proxyts-이전-middlewarets">4. proxy.ts (이전 middleware.ts)</h3>
<h4 id="변경-사항">변경 사항</h4>
<p>middleware.ts가 proxy.ts로 이름 변경되어 앱의 네트워크 경계를 명확히 함</p>
<p><strong>마이그레이션 방법</strong>:</p>
<pre><code class="language-bash"># 파일 이름 변경
mv middleware.ts proxy.ts</code></pre>
<pre><code class="language-typescript">// proxy.ts
import { NextRequest, NextResponse } from &#39;next/server&#39;;

export default function proxy(request: NextRequest) {
  return NextResponse.redirect(new URL(&#39;/home&#39;, request.url));
}</code></pre>
<h4 id="주요-변경점">주요 변경점</h4>
<table>
<thead>
<tr>
<th>변경 전</th>
<th>변경 후</th>
</tr>
</thead>
<tbody><tr>
<td><code>middleware.ts</code></td>
<td><code>proxy.ts</code></td>
</tr>
<tr>
<td><code>export function middleware()</code></td>
<td><code>export default function proxy()</code></td>
</tr>
<tr>
<td><code>skipMiddlewareUrlNormalize</code></td>
<td><code>skipProxyUrlNormalize</code></td>
</tr>
</tbody></table>
<p>⚠️ <strong>주의</strong>:</p>
<ul>
<li>proxy.ts는 Node.js 런타임에서만 실행</li>
<li>Edge 런타임이 필요한 경우 middleware.ts를 계속 사용 (deprecated)</li>
</ul>
<hr>
<h3 id="5-nextjs-devtools-mcp">5. Next.js DevTools MCP</h3>
<p>Model Context Protocol을 통한 AI 기반 디버깅 도구입니다.</p>
<h4 id="기능">기능</h4>
<p>AI 에이전트에 Next.js 지식, 통합 로그, 자동 에러 접근, 페이지 인식 기능을 제공합니다.</p>
<ul>
<li><strong>라우팅, 캐싱, 렌더링 동작 이해</strong></li>
<li><strong>브라우저와 서버 로그 통합</strong>: 컨텍스트 전환 없이 확인</li>
<li><strong>자동 에러 접근</strong>: 수동 복사 없이 상세한 스택 트레이스 제공</li>
<li><strong>페이지 인식</strong>: 활성 라우트에 대한 컨텍스트 이해</li>
</ul>
<h4 id="활용">활용</h4>
<p>AI 도구가 프로젝트 구조를 이해하여:</p>
<ul>
<li>이슈 진단</li>
<li>동작 설명</li>
<li>개발 워크플로우 내에서 직접 수정 제안</li>
</ul>
<hr>
<h3 id="6-react-192-통합">6. React 19.2 통합</h3>
<p>Next.js 16은 React 19.2의 최신 기능들을 포함한 React Canary 릴리스를 사용합니다.</p>
<h4 id="새로운-기능">새로운 기능</h4>
<p><strong>View Transitions</strong></p>
<pre><code class="language-typescript">// Transition 내부에서 애니메이션 적용
&lt;Transition&gt;
  &lt;ElementThatAnimates /&gt;
&lt;/Transition&gt;</code></pre>
<p><strong>useEffectEvent()</strong></p>
<pre><code class="language-typescript">// Effect에서 비반응형 로직을 재사용 가능한 함수로 추출
const onVisit = useEffectEvent(() =&gt; {
  logVisit(url);
});</code></pre>
<p><strong>Activity 컴포넌트</strong></p>
<pre><code class="language-typescript">// display: none으로 UI를 숨기면서 상태 유지 및 Effect 정리
&lt;Activity&gt;
  &lt;BackgroundTask /&gt;
&lt;/Activity&gt;</code></pre>
<hr>
<h3 id="7-향상된-라우팅-및-네비게이션">7. 향상된 라우팅 및 네비게이션</h3>
<h4 id="layout-deduplication">Layout Deduplication</h4>
<p>여러 URL이 공유 레이아웃을 가질 때, 레이아웃을 한 번만 다운로드합니다.</p>
<p><strong>예시</strong>: 50개의 제품 링크가 있는 페이지에서 공유 레이아웃을 50번이 아닌 1번만 다운로드</p>
<h4 id="incremental-prefetching">Incremental Prefetching</h4>
<p>Next.js는 캐시에 없는 부분만 프리페치하며, 전체 페이지를 프리페치하지 않음</p>
<p><strong>특징</strong>:</p>
<ul>
<li>링크가 뷰포트를 벗어나면 요청 취소</li>
<li>호버 시 또는 뷰포트에 재진입 시 우선순위 지정</li>
<li>데이터가 무효화되면 링크 재프리페치</li>
<li>Cache Components와 원활하게 작동</li>
</ul>
<p><strong>트레이드오프</strong>:</p>
<ul>
<li>✅ 개별 프리페치 요청이 더 많을 수 있음</li>
<li>✅ 하지만 총 전송 크기는 훨씬 작음</li>
</ul>
<hr>
<h3 id="8-간소화된-create-next-app">8. 간소화된 create-next-app</h3>
<p>설정 플로우 단순화, 업데이트된 프로젝트 구조, 개선된 기본값</p>
<h4 id="새-템플릿-기본값">새 템플릿 기본값</h4>
<ul>
<li>✅ App Router (기본)</li>
<li>✅ TypeScript 우선 설정</li>
<li>✅ Tailwind CSS</li>
<li>✅ ESLint</li>
</ul>
<pre><code class="language-bash">npx create-next-app@latest</code></pre>
<hr>
<h3 id="9-react-compiler-지원---안정화">9. React Compiler 지원 - 안정화</h3>
<p>React Compiler의 1.0 릴리스에 따라 Next.js 16에서 안정화</p>
<h4 id="기능-1">기능</h4>
<ul>
<li><strong>자동 메모이제이션</strong>: 컴포넌트를 자동으로 메모이제이션하여 불필요한 리렌더링 감소</li>
<li><strong>수동 코드 변경 불필요</strong>: <code>useMemo</code>, <code>useCallback</code> 등을 수동으로 추가할 필요 없음</li>
</ul>
<h4 id="설정">설정</h4>
<pre><code class="language-typescript">// next.config.ts
const nextConfig = {
  reactCompiler: true,
};
export default nextConfig;</code></pre>
<pre><code class="language-bash">npm install babel-plugin-react-compiler@latest</code></pre>
<p>⚠️ <strong>주의</strong>:</p>
<ul>
<li>기본적으로 비활성화 (빌드 성능 데이터 수집 중)</li>
<li>개발 및 빌드 시 컴파일 시간이 증가할 수 있음 (Babel 의존성)</li>
</ul>
<hr>
<h3 id="10-build-adapters-api-알파">10. Build Adapters API (알파)</h3>
<p>커스텀 빌드 통합을 위한 새로운 API입니다.</p>
<h4 id="용도">용도</h4>
<ul>
<li>배포 플랫폼이 빌드 프로세스에 hook</li>
<li>Next.js 설정 수정</li>
<li>빌드 출력 처리</li>
</ul>
<pre><code class="language-javascript">// next.config.js
const nextConfig = {
  experimental: {
    adapterPath: require.resolve(&#39;./my-adapter.js&#39;),
  },
};
module.exports = nextConfig;</code></pre>
<hr>
<h3 id="11-로깅-개선">11. 로깅 개선</h3>
<h4 id="개발-요청-로그">개발 요청 로그</h4>
<p>시간이 소요되는 위치를 명확히 표시:</p>
<ul>
<li><strong>Compile</strong>: 라우팅 및 컴파일</li>
<li><strong>Render</strong>: 코드 실행 및 React 렌더링</li>
</ul>
<h4 id="빌드-로그">빌드 로그</h4>
<p>각 빌드 단계와 소요 시간을 표시:</p>
<pre><code>▲ Next.js 16 (Turbopack)
✓ Compiled successfully in 615ms
✓ Finished TypeScript in 1114ms
✓ Collecting page data in 208ms
✓ Generating static pages in 239ms
✓ Finalizing page optimization in 5ms</code></pre><hr>
<h2 id="🔴-breaking-changes">🔴 Breaking Changes</h2>
<h3 id="1-버전-요구사항">1. 버전 요구사항</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>최소 버전</th>
</tr>
</thead>
<tbody><tr>
<td>Node.js</td>
<td>20.9.0+ (LTS)</td>
</tr>
<tr>
<td>TypeScript</td>
<td>5.1.0+</td>
</tr>
<tr>
<td>Chrome</td>
<td>111+</td>
</tr>
<tr>
<td>Edge</td>
<td>111+</td>
</tr>
<tr>
<td>Firefox</td>
<td>111+</td>
</tr>
<tr>
<td>Safari</td>
<td>16.4+</td>
</tr>
</tbody></table>
<h3 id="2-async-params-및-searchparams">2. Async Params 및 SearchParams</h3>
<p>가장 큰 변경사항 중 하나입니다.</p>
<h4 id="변경-전-nextjs-15">변경 전 (Next.js 15)</h4>
<pre><code class="language-typescript">export default function Page({ 
  params,
  searchParams 
}: { 
  params: { id: string };
  searchParams: { sort: string };
}) {
  const id = params.id;  // 직접 접근
  const sort = searchParams.sort;
  return &lt;div&gt;Product {id}&lt;/div&gt;;
}</code></pre>
<h4 id="변경-후-nextjs-16">변경 후 (Next.js 16)</h4>
<pre><code class="language-typescript">export default async function Page({ 
  params,
  searchParams 
}: { 
  params: Promise&lt;{ id: string }&gt;;
  searchParams: Promise&lt;{ sort: string }&gt;;
}) {
  const { id } = await params;  // await 필수
  const { sort } = await searchParams;
  return &lt;div&gt;Product {id}&lt;/div&gt;;
}</code></pre>
<h3 id="3-동적-함수들도-async">3. 동적 함수들도 Async</h3>
<pre><code class="language-typescript">// 변경 전
import { cookies } from &#39;next/headers&#39;;

export function getAuthToken() {
  const cookieStore = cookies();
  return cookieStore.get(&#39;token&#39;);
}

// 변경 후
import { cookies } from &#39;next/headers&#39;;

export async function getAuthToken() {
  const cookieStore = await cookies();
  return cookieStore.get(&#39;token&#39;);
}</code></pre>
<p><strong>적용 대상</strong>:</p>
<ul>
<li><code>cookies()</code></li>
<li><code>headers()</code></li>
<li><code>draftMode()</code></li>
</ul>
<h3 id="4-제거된-기능">4. 제거된 기능</h3>
<table>
<thead>
<tr>
<th>제거된 기능</th>
<th>대체 방안</th>
</tr>
</thead>
<tbody><tr>
<td>AMP 지원</td>
<td>제거됨 (Google도 더 이상 우선순위 두지 않음)</td>
</tr>
<tr>
<td><code>next lint</code> 명령어</td>
<td>Biome 또는 ESLint를 직접 사용</td>
</tr>
<tr>
<td><code>serverRuntimeConfig</code>, <code>publicRuntimeConfig</code></td>
<td>환경 변수 (.env 파일) 사용</td>
</tr>
<tr>
<td><code>experimental.turbopack</code> 위치</td>
<td>최상위 <code>turbopack</code>으로 이동</td>
</tr>
<tr>
<td><code>experimental.dynamicIO</code></td>
<td><code>cacheComponents</code>로 이름 변경</td>
</tr>
<tr>
<td><code>experimental.ppr</code></td>
<td>Cache Components로 진화</td>
</tr>
<tr>
<td><code>unstable_rootParams()</code></td>
<td>향후 마이너 릴리스에서 대체 API 제공 예정</td>
</tr>
</tbody></table>
<h3 id="5-동작-변경">5. 동작 변경</h3>
<h4 id="기본-번들러">기본 번들러</h4>
<ul>
<li><strong>변경</strong>: Turbopack이 모든 앱의 기본 번들러</li>
<li><strong>옵트아웃</strong>: <code>next build --webpack</code></li>
</ul>
<h4 id="이미지-관련">이미지 관련</h4>
<ul>
<li><strong><code>images.minimumCacheTTL</code></strong>: 60초 → 4시간 (14400초)</li>
<li><strong><code>images.imageSizes</code></strong>: 기본값에서 <code>16</code> 제거 (4.2%만 사용)</li>
<li><strong><code>images.qualities</code></strong>: <code>[1..100]</code> → <code>[75]</code></li>
<li><strong><code>images.maximumRedirects</code></strong>: 무제한 → 3회</li>
</ul>
<h4 id="nextimage-로컬-소스">next/image 로컬 소스</h4>
<p>쿼리 문자열이 있는 로컬 이미지 소스는 열거 공격 방지를 위해 images.localPatterns.search 설정이 필요</p>
<h4 id="parallel-routes">Parallel Routes</h4>
<p>모든 parallel route 슬롯에 명시적인 <code>default.js</code> 파일 필요</p>
<hr>
<h2 id="⚠️-deprecated-향후-제거-예정">⚠️ Deprecated (향후 제거 예정)</h2>
<table>
<thead>
<tr>
<th>기능</th>
<th>대체 방안</th>
</tr>
</thead>
<tbody><tr>
<td><code>middleware.ts</code> 파일명</td>
<td><code>proxy.ts</code>로 변경</td>
</tr>
<tr>
<td><code>next/legacy/image</code></td>
<td><code>next/image</code> 사용</td>
</tr>
<tr>
<td><code>images.domains</code></td>
<td><code>images.remotePatterns</code> 사용</td>
</tr>
<tr>
<td><code>revalidateTag()</code> 단일 인자</td>
<td><code>revalidateTag(tag, profile)</code> 또는 <code>updateTag(tag)</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="🚀-마이그레이션-가이드">🚀 마이그레이션 가이드</h2>
<h3 id="자동-마이그레이션">자동 마이그레이션</h3>
<p>Next.js는 자동 codemod를 제공합니다:</p>
<pre><code class="language-bash">npx @next/codemod@canary upgrade latest</code></pre>
<h3 id="수동-업그레이드">수동 업그레이드</h3>
<pre><code class="language-bash">npm install next@latest react@latest react-dom@latest</code></pre>
<h3 id="새-프로젝트-시작">새 프로젝트 시작</h3>
<pre><code class="language-bash">npx create-next-app@latest</code></pre>
<hr>
<h2 id="📊-성능-개선-요약">📊 성능 개선 요약</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>개선 정도</th>
</tr>
</thead>
<tbody><tr>
<td>프로덕션 빌드</td>
<td>2-5배 빠름</td>
</tr>
<tr>
<td>Fast Refresh</td>
<td>최대 10배 빠름</td>
</tr>
<tr>
<td>파일시스템 캐싱</td>
<td>대규모 앱에서 시작 시간 대폭 단축</td>
</tr>
<tr>
<td>Layout Deduplication</td>
<td>네트워크 전송 크기 대폭 감소</td>
</tr>
<tr>
<td>Incremental Prefetching</td>
<td>총 전송 크기 감소</td>
</tr>
</tbody></table>
<hr>
<h2 id="🎯-주요-사용-사례별-가이드">🎯 주요 사용 사례별 가이드</h2>
<h3 id="case-1-블로그-사이트">Case 1: 블로그 사이트</h3>
<pre><code class="language-typescript">// 전체 페이지 캐싱
&quot;use cache&quot;;

export default async function BlogPage() {
  const posts = await fetchPosts();
  return &lt;PostList posts={posts} /&gt;;
}</code></pre>
<h3 id="case-2-전자상거래">Case 2: 전자상거래</h3>
<pre><code class="language-typescript">// 제품 목록은 캐싱, 사용자 장바구니는 동적
export default async function ProductPage() {
  return (
    &lt;&gt;
      &lt;ProductList /&gt;  {/* use cache */}
      &lt;Suspense fallback={&lt;Loading /&gt;}&gt;
        &lt;UserCart /&gt;   {/* 동적 */}
      &lt;/Suspense&gt;
    &lt;/&gt;
  );
}</code></pre>
<h3 id="case-3-대시보드">Case 3: 대시보드</h3>
<pre><code class="language-typescript">// 실시간 데이터는 캐시하지 않음
export default async function Dashboard() {
  const realtimeData = await fetchRealtimeData();
  return &lt;MetricsDashboard data={realtimeData} /&gt;;
}</code></pre>
<hr>
<h2 id="🔗-유용한-링크">🔗 유용한 링크</h2>
<ul>
<li><a href="https://nextjs.org/blog/next-16">공식 릴리스 노트</a></li>
<li><a href="https://nextjs.org/docs/app/guides/upgrading/version-16">업그레이드 가이드</a></li>
<li><a href="https://nextjs.org/docs/app/getting-started/cache-components">Cache Components 문서</a></li>
<li><a href="https://nextjs.org/docs/architecture/turbopack">Turbopack 문서</a></li>
<li><a href="https://react.dev/blog/2025/10/01/react-19-2">React 19.2 발표</a></li>
</ul>
<hr>
<h2 id="💡-마이그레이션-체크리스트">💡 마이그레이션 체크리스트</h2>
<ul>
<li><input disabled="" type="checkbox"> Node.js 20.9+ 설치</li>
<li><input disabled="" type="checkbox"> TypeScript 5.1+ 업그레이드</li>
<li><input disabled="" type="checkbox"> <code>params</code> 및 <code>searchParams</code>를 async로 변경</li>
<li><input disabled="" type="checkbox"> <code>cookies()</code>, <code>headers()</code>, <code>draftMode()</code>에 await 추가</li>
<li><input disabled="" type="checkbox"> <code>middleware.ts</code> → <code>proxy.ts</code> 변경</li>
<li><input disabled="" type="checkbox"> <code>revalidateTag()</code> 호출에 두 번째 인자 추가</li>
<li><input disabled="" type="checkbox"> 제거된 기능 사용 여부 확인</li>
<li><input disabled="" type="checkbox"> <code>cacheComponents</code> 플래그 고려</li>
<li><input disabled="" type="checkbox"> 자동 codemod 실행</li>
<li><input disabled="" type="checkbox"> 테스트 및 배포</li>
</ul>
<hr>
<h2 id="🎉-결론">🎉 결론</h2>
<p>Next.js 16은 성능과 개발자 경험을 크게 개선한 릴리스입니다. 특히:</p>
<p>✅ <strong>명시적 캐싱</strong>으로 더 예측 가능한 동작<br>✅ <strong>Turbopack 안정화</strong>로 빠른 빌드<br>✅ <strong>개선된 API</strong>로 더 나은 제어<br>✅ <strong>React 19.2</strong>로 최신 기능 활용</p>
<p>기존 프로젝트는 breaking changes를 주의 깊게 검토하고, 새 프로젝트는 바로 Next.js 16으로 시작하는 것을 권장합니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 19.2 업데이트 알아보기]]></title>
            <link>https://velog.io/@lee_1124/React-19.2-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@lee_1124/React-19.2-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Fri, 10 Oct 2025 04:39:45 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>2025년 10월 1일</strong> - React 팀<br>React 19.2가 npm에서 사용 가능합니다!</p>
</blockquote>
<p><img src="https://img.shields.io/badge/React-19.2-61DAFB?style=flat-square&logo=react&logoColor=white" alt="React"></p>
<hr>
<h2 id="📋-목차">📋 목차</h2>
<ul>
<li><a href="#%EB%93%A4%EC%96%B4%EA%B0%80%EB%A9%B0">들어가며</a></li>
<li><a href="#%EC%83%88%EB%A1%9C%EC%9A%B4-react-%EA%B8%B0%EB%8A%A5">새로운 React 기능</a><ul>
<li><a href="#activity-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8">Activity 컴포넌트</a></li>
<li><a href="#useeffectevent-%ED%9B%85">useEffectEvent 훅</a></li>
<li><a href="#cachesignal-api">cacheSignal API</a></li>
<li><a href="#performance-tracks">Performance Tracks</a></li>
</ul>
</li>
<li><a href="#%EC%83%88%EB%A1%9C%EC%9A%B4-react-dom-%EA%B8%B0%EB%8A%A5">새로운 React DOM 기능</a><ul>
<li><a href="#partial-pre-rendering">Partial Pre-rendering</a></li>
</ul>
</li>
<li><a href="#%EC%A3%BC%EB%AA%A9%ED%95%A0-%EB%A7%8C%ED%95%9C-%EB%B3%80%EA%B2%BD%EC%82%AC%ED%95%AD">주목할 만한 변경사항</a></li>
<li><a href="#%EB%B3%80%EA%B2%BD-%EB%A1%9C%EA%B7%B8">변경 로그</a></li>
</ul>
<hr>
<h2 id="들어가며">들어가며</h2>
<p>이번 릴리스는 12월의 React 19와 6월의 React 19.1에 이어 <strong>지난 1년간 세 번째 릴리스</strong>입니다.
React 19.2의 새로운 기능과 주목할 만한 변경사항이 어떤 게 있는지 알아보겠습니다!</p>
<h3 id="🎯-주요-업데이트">🎯 주요 업데이트</h3>
<table>
<thead>
<tr>
<th>카테고리</th>
<th>기능</th>
</tr>
</thead>
<tbody><tr>
<td><strong>React Core</strong></td>
<td><code>&lt;Activity /&gt;</code>, <code>useEffectEvent</code>, <code>cacheSignal</code>, Performance Tracks</td>
</tr>
<tr>
<td><strong>React DOM</strong></td>
<td>Partial Pre-rendering</td>
</tr>
<tr>
<td><strong>개선사항</strong></td>
<td>SSR Suspense 일괄 처리, Node Web Streams 지원</td>
</tr>
<tr>
<td><strong>도구</strong></td>
<td>eslint-plugin-react-hooks v6</td>
</tr>
</tbody></table>
<hr>
<h2 id="새로운-react-기능">새로운 React 기능</h2>
<h3 id="🎭-activity-컴포넌트">🎭 Activity 컴포넌트</h3>
<p><code>&lt;Activity&gt;</code>를 사용하면 앱을 제어하고 우선순위를 지정할 수 있는 <strong>&quot;활동&quot;으로 분할</strong>할 수 있습니다.
그리고, 컴포넌트를 언마운트하지 않고 숨기면서 state는 유지하고 effects만 정리할 수 있어, 기존 조건부 렌더링보다 유연한 제어가 가능합니다.</p>
<h4 id="기본-사용법">기본 사용법</h4>
<pre><code class="language-jsx">// ❌ 이전 방식
{isVisible &amp;&amp; &lt;Page /&gt;}

// ✅ 새로운 방식
&lt;Activity mode={isVisible ? &#39;visible&#39; : &#39;hidden&#39;}&gt;
  &lt;Page /&gt;
&lt;/Activity&gt;</code></pre>
<h4 id="지원-모드">지원 모드</h4>
<table>
<thead>
<tr>
<th>모드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong><code>visible</code></strong></td>
<td>자식을 표시하고, effect를 마운트하며, 업데이트를 정상적으로 처리</td>
</tr>
<tr>
<td><strong><code>hidden</code></strong></td>
<td>자식을 숨기고, effect를 언마운트하며, 모든 업데이트를 연기</td>
</tr>
</tbody></table>
<h4 id="💡-주요-이점">💡 주요 이점</h4>
<ul>
<li>🚀 <strong>성능 최적화</strong>: 화면에 보이는 것의 성능에 영향 없이 숨겨진 부분을 미리 렌더링</li>
<li>🔄 <strong>빠른 네비게이션</strong>: 백그라운드에서 데이터, CSS, 이미지 로드</li>
<li>💾 <strong>상태 유지</strong>: 뒤로 탐색 시 입력 필드 같은 상태 보존</li>
</ul>
<hr>
<h3 id="🎪-useeffectevent-훅">🎪 useEffectEvent 훅</h3>
<p><code>useEffect</code>에서 이벤트 핸들러를 분리하여 <strong>불필요한 재실행을 방지</strong>합니다.</p>
<h4 id="문제-상황">문제 상황</h4>
<pre><code class="language-jsx">// ❌ theme이 변경되면 채팅방이 다시 연결됨
function ChatRoom({ roomId, theme }) {
  useEffect(() =&gt; {
    const connection = createConnection(serverUrl, roomId);
    connection.on(&#39;connected&#39;, () =&gt; {
      showNotification(&#39;Connected!&#39;, theme);
    });
    connection.connect();
    return () =&gt; connection.disconnect();
  }, [roomId, theme]); // theme 변경 시 재연결!
}</code></pre>
<h4 id="해결-방법">해결 방법</h4>
<pre><code class="language-jsx">// ✅ useEffectEvent로 이벤트 분리
function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() =&gt; {
    showNotification(&#39;Connected!&#39;, theme);
  });

  useEffect(() =&gt; {
    const connection = createConnection(serverUrl, roomId);
    connection.on(&#39;connected&#39;, () =&gt; {
      onConnected();
    });
    connection.connect();
    return () =&gt; connection.disconnect();
  }, [roomId]); // ✅ theme은 의존성 배열에서 제외
}</code></pre>
<h4 id="⚠️-사용-시-주의사항">⚠️ 사용 시 주의사항</h4>
<blockquote>
<p><strong>언제 사용해야 할까요?</strong></p>
<ul>
<li>✅ Effect에서 발생하는 개념적인 &quot;이벤트&quot;에 사용</li>
<li>❌ 단순히 lint 오류를 무시하기 위해 사용 금지</li>
<li>📌 <code>eslint-plugin-react-hooks@6.1.1</code> 이상 필요</li>
</ul>
</blockquote>
<hr>
<h3 id="🔄-cachesignal-api">🔄 cacheSignal API</h3>
<blockquote>
<p><strong>React Server Components 전용</strong></p>
</blockquote>
<p><code>cache()</code> 수명 주기를 추적하여 <strong>불필요한 작업을 정리</strong>할 수 있습니다.</p>
<pre><code class="language-jsx">import { cache, cacheSignal } from &#39;react&#39;;

const dedupedFetch = cache(fetch);

async function Component() {
  await dedupedFetch(url, { signal: cacheSignal() });
}</code></pre>
<h4 id="정리-시점">정리 시점</h4>
<ul>
<li>✅ React가 렌더링을 성공적으로 완료</li>
<li>🛑 렌더링이 중단됨</li>
<li>❌ 렌더링이 실패함</li>
</ul>
<hr>
<h3 id="📊-performance-tracks">📊 Performance Tracks</h3>
<p>Chrome DevTools에 <strong>새로운 성능 프로필 트랙</strong>이 추가되었습니다!</p>
<h4 id="1️⃣-scheduler-⚛-트랙">1️⃣ Scheduler ⚛ 트랙</h4>
<p>React가 다양한 우선순위로 작업을 처리하는 과정을 시각화합니다.</p>
<pre><code>🟦 Blocking   ← 사용자 상호작용
🟩 Transition ← startTransition 업데이트</code></pre><p><strong>확인 가능한 정보:</strong></p>
<ul>
<li>업데이트를 예약한 이벤트</li>
<li>렌더링 발생 시점</li>
<li>우선순위 대기 상황</li>
<li>Paint 대기 시간</li>
</ul>
<h4 id="2️⃣-components-⚛-트랙">2️⃣ Components ⚛ 트랙</h4>
<p>컴포넌트 렌더링과 effect 실행을 추적합니다.</p>
<pre><code>📌 Mount   ← 컴포넌트/effect 마운트
⏸️ Blocked ← React 외부 작업으로 양보</code></pre><p><strong>성능 문제 식별:</strong></p>
<ul>
<li>렌더링 시점</li>
<li>Effect 실행 시점</li>
<li>작업 완료 시간</li>
</ul>
<hr>
<h2 id="새로운-react-dom-기능">새로운 React DOM 기능</h2>
<h3 id="🎨-partial-pre-rendering">🎨 Partial Pre-rendering</h3>
<p>앱의 일부를 <strong>미리 렌더링하고 나중에 재개</strong>하는 강력한 기능입니다.</p>
<h4 id="작동-방식">작동 방식</h4>
<pre><code class="language-mermaid">graph LR
    A[Static Parts] --&gt;|CDN| B[Prelude Shell]
    B --&gt; C[Dynamic Content]
    C --&gt; D[Complete Page]</code></pre>
<h4 id="구현-예제">구현 예제</h4>
<h5 id="1단계-사전-렌더링">1단계: 사전 렌더링</h5>
<pre><code class="language-jsx">const { prelude, postponed } = await prerender(&lt;App /&gt;, {
  signal: controller.signal,
});

// postponed 상태 저장
await savePostponedState(postponed);

// prelude를 CDN으로 전송</code></pre>
<h5 id="2단계-ssr-재개">2단계: SSR 재개</h5>
<pre><code class="language-jsx">const postponed = await getPostponedState(request);
const resumeStream = await resume(&lt;App /&gt;, postponed);

// 스트림을 클라이언트로 전송</code></pre>
<h5 id="3단계-ssg-재개">3단계: SSG 재개</h5>
<pre><code class="language-jsx">const postponedState = await getPostponedState(request);
const { prelude } = await resumeAndPrerender(&lt;App /&gt;, postponedState);

// 완전한 HTML을 CDN으로 전송</code></pre>
<h4 id="📚-새로운-api">📚 새로운 API</h4>
<p><strong>react-dom/server</strong></p>
<ul>
<li><code>resume</code> - Web Streams용</li>
<li><code>resumeToPipeableStream</code> - Node Streams용</li>
</ul>
<p><strong>react-dom/static</strong></p>
<ul>
<li><code>resumeAndPrerender</code> - Web Streams용</li>
<li><code>resumeAndPrerenderToNodeStream</code> - Node Streams용</li>
</ul>
<hr>
<h2 id="주목할-만한-변경사항">주목할 만한 변경사항</h2>
<h3 id="🎯-ssr을-위한-suspense-경계-일괄-처리">🎯 SSR을 위한 Suspense 경계 일괄 처리</h3>
<p>클라이언트와 서버의 렌더링 동작을 일치시키는 중요한 개선사항입니다.</p>
<h4 id="before-vs-after">Before vs After</h4>
<p><strong>이전 (19.1)</strong></p>
<pre><code>┌─────────────┐
│ Loading... │  ← 즉시 대체
├─────────────┤
│ Content A  │  ← 즉시 표시
├─────────────┤
│ Loading... │
└─────────────┘</code></pre><p><strong>현재 (19.2)</strong></p>
<pre><code>┌─────────────┐
│ Loading... │
├─────────────┤  ← 짧은 시간 일괄 처리
│ Content A  │
├─────────────┤
│ Content B  │  ← 함께 표시
└─────────────┘</code></pre><h4 id="💡-장점">💡 장점</h4>
<ul>
<li>🎬 <strong>View Transitions 지원</strong>: 더 큰 단위로 애니메이션 실행</li>
<li>⚡ <strong>성능 최적화</strong>: 연쇄 애니메이션 방지</li>
<li>📊 <strong>Core Web Vitals</strong>: LCP 메트릭 자동 고려</li>
</ul>
<blockquote>
<p><strong>참고:</strong> React는 페이지 로드 시간이 2.5초에 가까워지면 자동으로 일괄 처리를 중단하여 메트릭에 영향을 주지 않습니다.</p>
</blockquote>
<hr>
<h3 id="🌊-ssr-node용-web-streams-지원">🌊 SSR: Node용 Web Streams 지원</h3>
<p>Node.js 환경에서도 <strong>Web Streams API</strong>를 사용할 수 있습니다!</p>
<h4 id="새로-추가된-api">새로 추가된 API</h4>
<pre><code class="language-jsx">// Web Streams 사용 가능
✅ renderToReadableStream
✅ prerender
✅ resume
✅ resumeAndPrerender</code></pre>
<h4 id="⚠️-중요한-권장사항">⚠️ 중요한 권장사항</h4>
<p><strong>Node.js에서는 Node Streams를 권장합니다!</strong></p>
<pre><code class="language-jsx">// 🚀 권장: Node Streams (더 빠름)
renderToPipeableStream
resumeToPipeableStream
prerenderToNodeStream
resumeAndPrerenderToNodeStream

// ⚡ Web Streams는 Node에서 느리고 압축 미지원</code></pre>
<hr>
<h3 id="🔧-eslint-plugin-react-hooks-v6">🔧 eslint-plugin-react-hooks v6</h3>
<p><strong>Flat Config</strong>가 기본값으로 설정되고, <strong>React Compiler</strong> 기반 규칙을 선택적으로 사용할 수 있습니다.</p>
<h4 id="레거시-설정-사용하기">레거시 설정 사용하기</h4>
<pre><code class="language-js">// package.json 또는 eslint.config.js
{
  // ❌ 이전
  extends: [&#39;plugin:react-hooks/recommended&#39;]

  // ✅ 레거시 유지
  extends: [&#39;plugin:react-hooks/recommended-legacy&#39;]
}</code></pre>
<hr>
<h3 id="🆔-useid-접두사-업데이트">🆔 useId 접두사 업데이트</h3>
<p>기본 접두사가 변경되었습니다!</p>
<table>
<thead>
<tr>
<th>버전</th>
<th>접두사</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>19.0.0</td>
<td><code>:r:</code></td>
<td>CSS 선택자 회피</td>
</tr>
<tr>
<td>19.1.0</td>
<td><code>«r»</code></td>
<td>특수문자 사용</td>
</tr>
<tr>
<td><strong>19.2.0</strong></td>
<td><strong><code>_r_</code></strong></td>
<td><strong>View Transitions &amp; XML 1.0 호환</strong></td>
</tr>
</tbody></table>
<hr>
<h2 id="변경-로그">변경 로그</h2>
<h3 id="✨-기타-주목할-만한-변경사항">✨ 기타 주목할 만한 변경사항</h3>
<ul>
<li><strong>react-dom</strong>: hoistable 스타일에 nonce 사용 허용 (<a href="https://github.com/facebook/react/pull/32461">#32461</a>)</li>
<li><strong>react-dom</strong>: React 소유 노드를 Container로 사용 시 경고 추가 (<a href="https://github.com/facebook/react/pull/32774">#32774</a>)</li>
</ul>
<h3 id="🐛-주목할-만한-버그-수정">🐛 주목할 만한 버그 수정</h3>
<table>
<thead>
<tr>
<th>분류</th>
<th>설명</th>
<th>PR</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Context</strong></td>
<td>문자열화 개선 &quot;SomeContext&quot;</td>
<td><a href="https://github.com/facebook/react/pull/33507">#33507</a></td>
</tr>
<tr>
<td><strong>useDeferredValue</strong></td>
<td>popstate 무한 루프 수정</td>
<td><a href="https://github.com/facebook/react/pull/32821">#32821</a></td>
</tr>
<tr>
<td><strong>useDeferredValue</strong></td>
<td>초기 값 버그 수정</td>
<td><a href="https://github.com/facebook/react/pull/34376">#34376</a></td>
</tr>
<tr>
<td><strong>Client Actions</strong></td>
<td>양식 제출 충돌 수정</td>
<td><a href="https://github.com/facebook/react/pull/33055">#33055</a></td>
</tr>
<tr>
<td><strong>Suspense</strong></td>
<td>Dehydrated 경계 처리 개선</td>
<td><a href="https://github.com/facebook/react/pull/32900">#32900</a></td>
</tr>
<tr>
<td><strong>Hot Reload</strong></td>
<td>스택 오버플로 방지</td>
<td><a href="https://github.com/facebook/react/pull/34145">#34145</a></td>
</tr>
<tr>
<td><strong>Component Stacks</strong></td>
<td>다양한 개선</td>
<td><a href="https://github.com/facebook/react/pull/33629">#33629</a> 외</td>
</tr>
<tr>
<td><strong>React.use</strong></td>
<td>lazy 컴포넌트 내 버그 수정</td>
<td><a href="https://github.com/facebook/react/pull/33941">#33941</a></td>
</tr>
<tr>
<td><strong>ARIA</strong></td>
<td>1.3 속성 경고 제거</td>
<td><a href="https://github.com/facebook/react/pull/34264">#34264</a></td>
</tr>
<tr>
<td><strong>Suspense</strong></td>
<td>중첩 Suspense 버그 수정</td>
<td><a href="https://github.com/facebook/react/pull/33467">#33467</a></td>
</tr>
<tr>
<td><strong>SSR</strong></td>
<td>렌더링 중단 후 멈춤 방지</td>
<td><a href="https://github.com/facebook/react/pull/34192">#34192</a></td>
</tr>
</tbody></table>
<hr>
<h2 id="📚-참고-자료">📚 참고 자료</h2>
<ul>
<li><a href="https://react.dev/blog">React 공식 블로그</a></li>
<li><a href="https://react.dev/reference/react/Activity">Activity 문서</a></li>
<li><a href="https://react.dev/reference/react/useEffectEvent">useEffectEvent 문서</a></li>
<li><a href="https://react.dev/learn/react-developer-tools">Performance Tracks 문서</a></li>
<li><a href="https://github.com/facebook/react/blob/main/CHANGELOG.md">전체 변경 로그</a></li>
</ul>
<hr>
<h2 id="🎉-마무리">🎉 마무리</h2>
<p>React 19.2는 성능 최적화, 개발자 경험 개선, 그리고 서버 사이드 렌더링의 강화를 가져왔습니다. 특히 <code>&lt;Activity&gt;</code> 컴포넌트와 <code>useEffectEvent</code> 훅은 실무에서 바로 활용할 수 있는 강력한 기능입니다.</p>
<p>지금 바로 업데이트하고 새로운 기능을 경험해보세요! 🚀</p>
<pre><code class="language-bash">npm install react@19.2 react-dom@19.2</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 학습코스 따라하기 (Chapter 14)]]></title>
            <link>https://velog.io/@lee_1124/Next.js-%ED%95%99%EC%8A%B5%EC%BD%94%EC%8A%A4-%EB%94%B0%EB%9D%BC%ED%95%98%EA%B8%B0-Chapter-14</link>
            <guid>https://velog.io/@lee_1124/Next.js-%ED%95%99%EC%8A%B5%EC%BD%94%EC%8A%A4-%EB%94%B0%EB%9D%BC%ED%95%98%EA%B8%B0-Chapter-14</guid>
            <pubDate>Fri, 22 Aug 2025 04:12:27 GMT</pubDate>
            <description><![CDATA[<h1 id="🌐-improving-accessibility">🌐 Improving Accessibility</h1>
<p>웹 접근성을 개선하여 모든 사용자가 쉽게 사용할 수 있는 애플리케이션을 만드는 방법을 알아보겠습니다.</p>
<hr>
<h2 id="📚-목차">📚 목차</h2>
<ol>
<li><a href="#1-%EC%9B%B9-%EC%A0%91%EA%B7%BC%EC%84%B1%EC%9D%B4%EB%9E%80">웹 접근성이란?</a></li>
<li><a href="#2-eslint-%EC%A0%91%EA%B7%BC%EC%84%B1-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-%EC%84%A4%EC%A0%95">ESLint 접근성 플러그인 설정</a></li>
<li><a href="#3-form-%EC%A0%91%EA%B7%BC%EC%84%B1-%ED%96%A5%EC%83%81">Form 접근성 향상</a></li>
<li><a href="#4-form-%EC%9C%A0%ED%9A%A8%EC%84%B1-%EA%B2%80%EC%82%AC">Form 유효성 검사</a></li>
<li><a href="#5-%EC%8B%A4%EC%8A%B5-aria-%EB%9D%BC%EB%B2%A8-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0">실습: Aria 라벨 추가하기</a></li>
</ol>
<hr>
<h2 id="1-웹-접근성이란">1. 웹 접근성이란?</h2>
<blockquote>
<p>💡 <strong>접근성(Accessibility)</strong>은 모든 사용자가 웹 애플리케이션을 쉽게 사용할 수 있도록 설계하는 것을 의미합니다.</p>
</blockquote>
<p>웹 접근성은 단순히 장애가 있는 사용자만을 위한 것이 아닙니다. 다음과 같은 다양한 요소들을 포함합니다:</p>
<ul>
<li>🎹 <strong>키보드 내비게이션</strong></li>
<li>🏷️ <strong>시맨틱(Semantic) HTML</strong>  </li>
<li>🖼️ <strong>이미지 대체 텍스트</strong></li>
<li>🎨 <strong>색상 대비</strong></li>
<li>🎬 <strong>비디오 자막</strong></li>
</ul>
<hr>
<h2 id="2-eslint-접근성-플러그인-설정">2. ESLint 접근성 플러그인 설정</h2>
<p>Next.js는 기본적으로 <code>eslint-plugin-jsx-a11y</code> 플러그인을 포함하여 접근성 문제를 조기에 발견할 수 있게 도와줍니다.</p>
<h3 id="21-설정-방법">2.1 설정 방법</h3>
<p><strong>1단계: package.json에 스크립트 추가</strong></p>
<pre><code class="language-json">{
  &quot;scripts&quot;: {
    &quot;build&quot;: &quot;next build&quot;,
    &quot;dev&quot;: &quot;next dev&quot;, 
    &quot;start&quot;: &quot;next start&quot;,
    &quot;seed&quot;: &quot;node -r dotenv/config ./scripts/seed.js&quot;,
    &quot;lint&quot;: &quot;next lint&quot;  // 👈 이 라인 추가
  }
}</code></pre>
<p><strong>2단계: ESLint 실행</strong></p>
<pre><code class="language-bash"># pnpm 사용시
pnpm lint

# npm 사용시 
npm run lint</code></pre>
<h3 id="22-설치-과정-스크린샷">2.2 설치 과정 스크린샷</h3>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/14764491-5bc3-49fe-8044-1032ef249869/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/b0d8eb3f-e281-489c-8455-778a08343fcf/image.png" alt=""></p>
<blockquote>
<p>⚠️ <strong>주의사항</strong>: ESLint v9에서 오류가 발생하면 v8.57.0으로 다운그레이드하세요.</p>
<pre><code class="language-bash">npm uninstall eslint
npm install -D eslint@8.57.0</code></pre>
</blockquote>
<h3 id="23-접근성-검사-테스트">2.3 접근성 검사 테스트</h3>
<p>ESLint 설치 후 접근성 오류를 테스트해보겠습니다. </p>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/1f05992e-00af-455a-9110-b34dd3c584ea/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/f58e293a-3f4d-4909-b317-7cf47eabaf75/image.png" alt=""></p>
<p>다음 코드에서 <code>alt</code> 속성을 제거하고 테스트해보세요:</p>
<pre><code class="language-tsx">// /app/ui/invoices/table.tsx
&lt;Image
  src={invoice.image_url}
  className=&quot;mr-2 rounded-full&quot;
  width={28}
  height={28}
  // alt={`${invoice.name}&#39;s profile picture`} 👈 이 라인을 주석처리
/&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/397381a4-f1e5-4b44-8ff0-780ea2fed317/image.png" alt=""></p>
<hr>
<h2 id="3-form-접근성-향상">3. Form 접근성 향상</h2>
<p>현재 프로젝트에서는 이미 Form 접근성을 위해 다음 세 가지 요소를 구현하고 있습니다:</p>
<h3 id="31-semantic-html-사용">3.1 Semantic HTML 사용</h3>
<pre><code class="language-tsx">// ✅ 좋은 예: 시맨틱 요소 사용
&lt;input type=&quot;email&quot; /&gt;
&lt;select&gt;
  &lt;option&gt;옵션1&lt;/option&gt;
&lt;/select&gt;

// ❌ 나쁜 예: 비시맨틱 요소 사용
&lt;div onClick={handleClick}&gt;클릭&lt;/div&gt;</code></pre>
<h3 id="32-레이블-지정">3.2 레이블 지정</h3>
<pre><code class="language-tsx">&lt;label htmlFor=&quot;email&quot; className=&quot;block text-sm font-medium&quot;&gt;
  이메일 주소
&lt;/label&gt;
&lt;input 
  id=&quot;email&quot;
  name=&quot;email&quot;
  type=&quot;email&quot;
  className=&quot;mt-1 block w-full rounded-md border-gray-300&quot;
/&gt;</code></pre>
<h3 id="33-포커스-아웃라인">3.3 포커스 아웃라인</h3>
<pre><code class="language-css">/* 포커스 시 아웃라인 표시 */
.form-input:focus {
  outline: 2px solid #3b82f6;
  outline-offset: 2px;
}</code></pre>
<blockquote>
<p>💡 <strong>Tip</strong>: Tab 키를 눌러 포커스 이동을 테스트해보세요!</p>
</blockquote>
<hr>
<h2 id="4-form-유효성-검사">4. Form 유효성 검사</h2>
<p>Form 검증은 사용자 경험과 데이터 무결성을 위해 필수적입니다.</p>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/bf3c887f-3828-4a6d-8f07-3cdc60322741/image.png" alt=""></p>
<h3 id="41-클라이언트-사이드-검증">4.1 클라이언트 사이드 검증</h3>
<p>가장 간단한 방법은 HTML5의 기본 검증을 사용하는 것입니다:</p>
<pre><code class="language-tsx">&lt;input
  id=&quot;amount&quot;
  name=&quot;amount&quot; 
  type=&quot;number&quot;
  step=&quot;0.01&quot;
  placeholder=&quot;Enter USD amount&quot;
  className=&quot;peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500&quot;
  required  // 👈 필수 속성 추가
/&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/55f907e3-33e1-4e89-91f7-5f3cc997486d/image.png" alt=""></p>
<h3 id="42-서버-사이드-검증">4.2 서버 사이드 검증</h3>
<p>서버 사이드 검증의 장점:</p>
<ul>
<li>✅ 데이터베이스 전송 전 형식 확인</li>
<li>✅ 악의적인 사용자의 클라이언트 검증 우회 방지  </li>
<li>✅ 데이터 유효성에 대한 단일 신뢰 소스</li>
</ul>
<h3 id="43-useactionstate-hook-사용">4.3 useActionState Hook 사용</h3>
<p>React의 <code>useActionState</code> Hook을 사용하여 폼 상태를 관리합니다:</p>
<pre><code class="language-tsx">import { useActionState } from &quot;react&quot;;

// useActionState 사용법 예시
async function increment(previousState, formData) {
  return previousState + 1;
}

function StatefulForm() {
  const [state, formAction] = useActionState(increment, 0);

  return (
    &lt;form&gt;
      {state}
      &lt;button formAction={formAction}&gt;Increment&lt;/button&gt;
    &lt;/form&gt;
  );
}</code></pre>
<h4 id="📋-useactionstate-매개변수">📋 useActionState 매개변수</h4>
<table>
<thead>
<tr>
<th>매개변수</th>
<th>타입</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>fn</code></td>
<td>Function</td>
<td>폼 제출 시 호출되는 함수</td>
</tr>
<tr>
<td><code>initialState</code></td>
<td>Any</td>
<td>초기 상태값</td>
</tr>
<tr>
<td><code>permalink</code></td>
<td>String (선택사항)</td>
<td>JavaScript 번들 로드 전 폼 제출 시 이동할 URL</td>
</tr>
</tbody></table>
<h4 id="📤-useactionstate-반환값">📤 useActionState 반환값</h4>
<table>
<thead>
<tr>
<th>반환값</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>state</code></td>
<td>현재 상태 (첫 렌더링 시 initialState와 동일)</td>
</tr>
<tr>
<td><code>formAction</code></td>
<td>Form의 action 속성에 전달할 액션</td>
</tr>
</tbody></table>
<h3 id="44-form-컴포넌트-구현">4.4 Form 컴포넌트 구현</h3>
<pre><code class="language-tsx">&quot;use client&quot;;

import { useActionState } from &quot;react&quot;;
import { createInvoice, State } from &quot;@/app/lib/actions&quot;;

export default function Form({ customers }: { customers: CustomerField[] }) {
  const initialState: State = { message: null, errors: {} };
  const [state, formAction] = useActionState(createInvoice, initialState);

  return (
    &lt;form action={formAction}&gt;
      {/* 폼 내용 */}
    &lt;/form&gt;
  );
}</code></pre>
<h3 id="45-zod를-사용한-스키마-검증">4.5 Zod를 사용한 스키마 검증</h3>
<pre><code class="language-typescript">// /app/lib/actions.ts
const FormSchema = z.object({
  id: z.string(),

  customerId: z.string({
    invalid_type_error: &quot;Please select a customer.&quot;,
  }),

  amount: z.coerce
    .number()
    .gt(0, { message: &quot;Please enter an amount greater than $0.&quot; }),

  status: z.enum([&quot;pending&quot;, &quot;paid&quot;], {
    invalid_type_error: &quot;Please select an invoice status.&quot;,
  }),

  date: z.string(),
});

export type State = {
  errors?: {
    customerId?: string[];
    amount?: string[];
    status?: string[];
  };
  message?: string | null;
};</code></pre>
<h3 id="46-서버-액션-구현">4.6 서버 액션 구현</h3>
<pre><code class="language-typescript">export async function createInvoice(prevState: State, formData: FormData) {
  // Zod를 사용한 폼 검증
  const validatedFields = CreateInvoice.safeParse({
    customerId: formData.get(&#39;customerId&#39;),
    amount: formData.get(&#39;amount&#39;),
    status: formData.get(&#39;status&#39;),
  });

  // 검증 실패 시 오류 반환
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
      message: &#39;Missing Fields. Failed to Create Invoice.&#39;,
    };
  }

  // 데이터 준비
  const { customerId, amount, status } = validatedFields.data;
  const amountInCents = amount * 100;
  const date = new Date().toISOString().split(&#39;T&#39;)[0];

  // 데이터베이스 삽입
  try {
    await sql`
      INSERT INTO invoices (customer_id, amount, status, date)
      VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
    `;
  } catch (error) {
    return {
      message: &#39;Database Error: Failed to Create Invoice.&#39;,
    };
  }

  revalidatePath(&#39;/dashboard/invoices&#39;);
  redirect(&#39;/dashboard/invoices&#39;);
}</code></pre>
<h3 id="47-오류-메시지-표시">4.7 오류 메시지 표시</h3>
<pre><code class="language-tsx">export default function Form({ customers }: { customers: CustomerField[] }) {
  const initialState: State = { message: null, errors: {} };
  const [state, formAction] = useActionState(createInvoice, initialState);

  return (
    &lt;form action={formAction}&gt;
      &lt;div className=&quot;rounded-md bg-gray-50 p-4 md:p-6&quot;&gt;
        &lt;div className=&quot;mb-4&quot;&gt;
          {/* 고객 선택 필드 */}
          &lt;select id=&quot;customer&quot; name=&quot;customerId&quot;&gt;
            {/* 옵션들 */}
          &lt;/select&gt;

          {/* 오류 메시지 표시 */}
          &lt;div id=&quot;customer-error&quot; aria-live=&quot;polite&quot; aria-atomic=&quot;true&quot;&gt;
            {state.errors?.customerId &amp;&amp;
              state.errors.customerId.map((error: string) =&gt; (
                &lt;p className=&quot;mt-2 text-sm text-red-500&quot; key={error}&gt;
                  {error}
                &lt;/p&gt;
              ))}
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  );
}</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/891b7884-619d-4f85-802c-9c92a2e76e0b/image.png" alt=""></p>
<hr>
<h2 id="5-실습-aria-라벨-추가하기">5. 실습: Aria 라벨 추가하기</h2>
<p>이번 실습에서는 <code>edit-form.tsx</code> 컴포넌트에 접근성을 향상시키는 요소들을 추가해보겠습니다.</p>
<h3 id="51-구현-목표">5.1 구현 목표</h3>
<ul>
<li>✅ <code>useActionState</code>를 추가하여 폼 상태 관리</li>
<li>✅ <code>updateInvoice</code> 액션에 Zod 검증 추가  </li>
<li>✅ 오류 메시지 표시 및 ARIA 라벨 추가</li>
</ul>
<h3 id="52-edit-form-컴포넌트">5.2 Edit Form 컴포넌트</h3>
<pre><code class="language-tsx">&quot;use client&quot;;

import { CustomerField, InvoiceForm } from &quot;@/app/lib/definitions&quot;;
import { Button } from &quot;@/app/ui/button&quot;;
import { State, updateInvoice } from &quot;@/app/lib/actions&quot;;
import { useFormState } from &quot;react-dom&quot;;

export default function EditInvoiceForm({
  invoice,
  customers,
}: {
  invoice: InvoiceForm;
  customers: CustomerField[];
}) {
  const initialState: State = { message: null, errors: {} };
  const updateInvoiceWithId = updateInvoice.bind(null, invoice.id);
  const [state, formAction] = useFormState(updateInvoiceWithId, initialState);

  return (
    &lt;form action={formAction}&gt;
      &lt;div className=&quot;rounded-md bg-gray-50 p-4 md:p-6&quot;&gt;
        {/* 숨겨진 ID 필드 */}
        &lt;input type=&quot;hidden&quot; name=&quot;id&quot; value={invoice.id} /&gt;

        {/* 고객 선택 */}
        &lt;div className=&quot;mb-4&quot;&gt;
          &lt;label htmlFor=&quot;customer&quot; className=&quot;mb-2 block text-sm font-medium&quot;&gt;
            Choose customer
          &lt;/label&gt;
          &lt;div className=&quot;relative&quot;&gt;
            &lt;select
              id=&quot;customer&quot;
              name=&quot;customerId&quot;
              className=&quot;peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500&quot;
              defaultValue={invoice.customer_id}
            &gt;
              &lt;option value=&quot;&quot; disabled&gt;
                Select a customer
              &lt;/option&gt;
              {customers.map((customer) =&gt; (
                &lt;option key={customer.id} value={customer.id}&gt;
                  {customer.name}
                &lt;/option&gt;
              ))}
            &lt;/select&gt;
          &lt;/div&gt;

          {/* 🎯 ARIA 라벨과 오류 메시지 추가 */}
          &lt;div id=&quot;customer-error&quot; aria-live=&quot;polite&quot; aria-atomic=&quot;true&quot;&gt;
            {state.errors?.customerId &amp;&amp;
              state.errors.customerId.map((error: string) =&gt; (
                &lt;p className=&quot;mt-2 text-sm text-red-500&quot; key={error}&gt;
                  {error}
                &lt;/p&gt;
              ))}
          &lt;/div&gt;
        &lt;/div&gt;

        {/* 금액 입력 */}
        &lt;div className=&quot;mb-4&quot;&gt;
          &lt;label htmlFor=&quot;amount&quot; className=&quot;mb-2 block text-sm font-medium&quot;&gt;
            Choose an amount
          &lt;/label&gt;
          &lt;div className=&quot;relative mt-2 rounded-md&quot;&gt;
            &lt;input
              id=&quot;amount&quot;
              name=&quot;amount&quot;
              type=&quot;number&quot;
              defaultValue={invoice.amount}
              placeholder=&quot;Enter USD amount&quot;
              className=&quot;peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500&quot;
            /&gt;
          &lt;/div&gt;

          {/* 금액 오류 메시지 */}
          &lt;div id=&quot;amount-error&quot; aria-live=&quot;polite&quot; aria-atomic=&quot;true&quot;&gt;
            {state.errors?.amount &amp;&amp;
              state.errors.amount.map((error: string) =&gt; (
                &lt;p className=&quot;mt-2 text-sm text-red-500&quot; key={error}&gt;
                  {error}
                &lt;/p&gt;
              ))}
          &lt;/div&gt;
        &lt;/div&gt;

        {/* 상태 선택 */}
        &lt;div&gt;
          &lt;label htmlFor=&quot;status&quot; className=&quot;mb-2 block text-sm font-medium&quot;&gt;
            Set the invoice status
          &lt;/label&gt;
          &lt;div className=&quot;rounded-md border border-gray-200 bg-white px-[14px] py-3&quot;&gt;
            &lt;div className=&quot;flex gap-4&quot;&gt;
              &lt;div className=&quot;flex items-center&quot;&gt;
                &lt;input
                  id=&quot;pending&quot;
                  name=&quot;status&quot;
                  type=&quot;radio&quot;
                  value=&quot;pending&quot;
                  defaultChecked={invoice.status === &quot;pending&quot;}
                /&gt;
                &lt;label htmlFor=&quot;pending&quot; className=&quot;ml-2 flex items-center gap-1.5 rounded-full bg-gray-100 px-3 py-1.5 text-xs font-medium text-gray-600&quot;&gt;
                  Pending
                &lt;/label&gt;
              &lt;/div&gt;
              &lt;div className=&quot;flex items-center&quot;&gt;
                &lt;input
                  id=&quot;paid&quot;
                  name=&quot;status&quot;
                  type=&quot;radio&quot;
                  value=&quot;paid&quot;
                  defaultChecked={invoice.status === &quot;paid&quot;}
                /&gt;
                &lt;label htmlFor=&quot;paid&quot; className=&quot;ml-2 flex items-center gap-1.5 rounded-full bg-green-500 px-3 py-1.5 text-xs font-medium text-white&quot;&gt;
                  Paid
                &lt;/label&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;

          {/* 상태 오류 메시지 */}
          &lt;div id=&quot;status-error&quot; aria-live=&quot;polite&quot; aria-atomic=&quot;true&quot;&gt;
            {state.errors?.status &amp;&amp;
              state.errors.status.map((error: string) =&gt; (
                &lt;p className=&quot;mt-2 text-sm text-red-500&quot; key={error}&gt;
                  {error}
                &lt;/p&gt;
              ))}
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div className=&quot;mt-6 flex justify-end gap-4&quot;&gt;
        &lt;Link
          href=&quot;/dashboard/invoices&quot;
          className=&quot;flex h-10 items-center rounded-lg bg-gray-100 px-4 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-200&quot;
        &gt;
          Cancel
        &lt;/Link&gt;
        &lt;Button type=&quot;submit&quot;&gt;Edit Invoice&lt;/Button&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  );
}</code></pre>
<h3 id="53-update-invoice-액션">5.3 Update Invoice 액션</h3>
<pre><code class="language-typescript">// /app/lib/actions.ts

export async function updateInvoice(
  id: string,
  prevState: State,
  formData: FormData
) {
  // Zod를 사용한 폼 검증
  const validatedFields = UpdateInvoice.safeParse({
    customerId: formData.get(&quot;customerId&quot;),
    amount: formData.get(&quot;amount&quot;),
    status: formData.get(&quot;status&quot;),
  });

  // 검증 실패 시 오류 반환
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
      message: &quot;Missing Fields. Failed to Update Invoice.&quot;,
    };
  }

  const { customerId, amount, status } = validatedFields.data;
  const amountInCents = amount * 100;

  // 데이터베이스 업데이트
  try {
    await sql`
      UPDATE invoices
      SET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status}
      WHERE id = ${id}
    `;
  } catch (error) {
    return {
      message: &quot;Database Error: Failed to Update Invoice.&quot;,
    };
  }

  // 캐시 무효화 및 리다이렉트
  revalidatePath(&quot;/dashboard/invoices&quot;);
  redirect(&quot;/dashboard/invoices&quot;);
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 학습코스 따라하기 (Chapter 13) ]]></title>
            <link>https://velog.io/@lee_1124/Next.js-%ED%95%99%EC%8A%B5%EC%BD%94%EC%8A%A4-%EB%94%B0%EB%9D%BC%ED%95%98%EA%B8%B0-Chapter-13</link>
            <guid>https://velog.io/@lee_1124/Next.js-%ED%95%99%EC%8A%B5%EC%BD%94%EC%8A%A4-%EB%94%B0%EB%9D%BC%ED%95%98%EA%B8%B0-Chapter-13</guid>
            <pubDate>Wed, 06 Aug 2025 04:02:39 GMT</pubDate>
            <description><![CDATA[<h1 id="handling-errors">Handling Errors</h1>
<p>이전 Chapter에서는 Server Action을 사용하여 데이터를 변경하는 방법에 대해 알아보았습니다. 이번에는 JavaScript의 try/catch 문과 Next.js API를 사용하여 오류를 효과적으로 처리하는 방법을 살펴보겠습니다.</p>
<h2 id="📋-이번-chapter에서-다룰-내용">📋 이번 Chapter에서 다룰 내용</h2>
<ol>
<li><p><strong><code>error.tsx</code> 파일을 활용한 오류 처리</strong><br>라우트 세그먼트에서 발생한 오류를 잡고, 사용자에게 대체 UI를 표시하는 방법</p>
</li>
<li><p><strong>404 오류 처리</strong><br><code>notFound</code> 함수와 <code>not-found</code> 파일을 사용하여 존재하지 않는 리소스에 대한 404 오류를 처리하는 방법</p>
</li>
<li><p><strong>Server Action 오류 처리</strong><br>서버 액션에 <code>try/catch</code> 문을 추가하여 오류를 안전하게 처리하는 방법</p>
</li>
</ol>
<hr>
<h2 id="1-adding-trycatch-to-server-actions">1. Adding <code>try/catch</code> to Server Actions</h2>
<p>JavaScript의 <code>try/catch</code> 문을 서버 액션에 추가하여 오류를 처리해 보겠습니다.</p>
<h3 id="applibactionsts-수정"><code>/app/lib/actions.ts</code> 수정</h3>
<pre><code class="language-typescript">// ...

// 송장 데이터 생성부분 수정
export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get(&quot;customerId&quot;),
    amount: formData.get(&quot;amount&quot;),
    status: formData.get(&quot;status&quot;),
  });

  const amountInCents = amount * 100;
  const date = new Date().toISOString().split(&quot;T&quot;)[0];

  try { // 수정
    await sql`
    INSERT INTO invoices (customer_id, amount, status, date)
    VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
  `;
  } catch (e) {
    return {
      message: &quot;DB Error : Faileed to Create Invoice&quot;,
    };
  }

  revalidatePath(&quot;/dashboard/invoices&quot;);
  redirect(&quot;/dashboard/invoices&quot;); // add code
}

// 송장 데이터 업데이트부분 수정
export async function updateInvoice(id: string, formData: FormData) {
  const { customerId, amount, status } = UpdateInvoice.parse({
    customerId: formData.get(&quot;customerId&quot;),
    amount: formData.get(&quot;amount&quot;),
    status: formData.get(&quot;status&quot;),
  });

  const amountInCents = amount * 100;

  try {
    await sql`
    UPDATE invoices
    SET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status}
    WHERE id = ${id}
  `;
  } catch (e) {
    return {
      message: &quot;DB Error : Faileed to Update Invoice&quot;,
    };
  }

  // 클라이언트 캐시 무효화 및 서버 요청 갱신
  revalidatePath(&quot;/dashboard/invoices&quot;);
  // 업데이트 후 인보이스 페이지로 리다이렉트
  redirect(&quot;/dashboard/invoices&quot;);
}

// 송장 데이터 삭제부분 수정
export async function deleteInvoice(id: string) {
  try {
    await sql`DELETE FROM invoices WHERE id = ${id}`;
    revalidatePath(&quot;/dashboard/invoices&quot;);
    return { message: &quot;Deleted Invoice.&quot; };
  } catch (error) {
    return { message: &quot;Database Error: Failed to Delete Invoice.&quot; };
  }
}</code></pre>
<h3 id="⚠️-중요한-주의사항">⚠️ 중요한 주의사항</h3>
<p>위 코드에서 <code>redirect</code>가 <code>try/catch</code> 구문 밖에서 호출되는 점에 주목해야 합니다.</p>
<p><strong>Next.js에서 <code>redirect</code> 함수는 내부적으로 오류를 던져서 redirect를 실행합니다.</strong> 따라서 <code>redirect</code>가 <code>try/catch</code> 블록 안에 있을 경우, redirect가 호출되면 오류가 발생하여 바로 <code>catch</code> 블록에서 잡히게 됩니다.</p>
<p>그래서 <code>redirect</code> 함수를 사용할 때는 <code>try</code> 영역 내 코드흐름이 성공한 다음에 도달할 수 있게끔 구현해야 합니다.</p>
<h3 id="오류-테스트해보기">오류 테스트해보기</h3>
<p>Server Action에서 오류가 발생했을 때 어떻게 되는지 확인해 보겠습니다. 예시로 <code>deleteInvoice</code> 액션에서 의도적인 오류를 발생시켜보겠습니다.</p>
<pre><code class="language-typescript">export async function deleteInvoice(id: string) {
  // 의도적 에러 발생
  throw new Error(&quot;Failed to Delete Invoice&quot;);

  // 도달불가
  try {
    await sql`DELETE FROM invoices WHERE id = ${id}`;
    revalidatePath(&quot;/dashboard/invoices&quot;);
    return { message: &quot;Deleted Invoice.&quot; };
  } catch (error) {
    return { message: &quot;Database Error: Failed to Delete Invoice.&quot; };
  }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/c93c328a-97b9-4cc8-9207-dcf63d00bbbe/image.png" alt=""></p>
<p>이러한 오류를 개발 중에 확인하는 것은 잠재적인 문제를 조기에 발견하는 데 도움이 됩니다. 하지만 애플리케이션이 갑작스럽게 실패하는 것을 방지하고, 사용자에게 오류를 표시하여 계속 실행되도록 하는 것이 더 <strong>중요</strong>합니다! 이때 Next.js의 <code>error.tsx</code> 파일이 유용하게 사용됩니다.</p>
<hr>
<h2 id="2-handling-all-errors-with-errortsx">2. Handling all errors with <code>error.tsx</code></h2>
<p><code>error.tsx</code> 파일은 특정 라우트 세그먼트에 대한 UI 경계를 정의하는 데 사용되는 Next.js의 약속된 파일입니다. 이 파일은 예상치 못한 오류를 잡아내고, 사용자에게 대체 UI를 표시할 수 있습니다.</p>
<h3 id="dashboardinvoiceserrortsx-파일-생성"><code>/dashboard/invoices/error.tsx</code> 파일 생성</h3>
<pre><code class="language-tsx">&quot;use client&quot;;

import { useEffect } from &quot;react&quot;;

export default function Error({
  error,
  reset,
}: {
  error: Error &amp; { digest?: string };
  reset: () =&gt; void;
}) {
  useEffect(() =&gt; {
    // 필요에 따라 오류를 오류 보고 서비스에 로깅
    console.error(error);
  }, [error]);

  return (
    &lt;main className=&quot;flex h-full flex-col items-center justify-center&quot;&gt;
      &lt;h2 className=&quot;text-center&quot;&gt;Something went wrong!&lt;/h2&gt;
      &lt;button
        className=&quot;mt-4 rounded-md bg-blue-500 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-400&quot;
        onClick={() =&gt; reset()}
      &gt;
        Try again
      &lt;/button&gt;
    &lt;/main&gt;
  );
}</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/1ea7dd38-22d2-4bf8-8e9e-aa9b32a15aec/image.png" alt=""></p>
<h3 id="🔑-errortsx-파일의-주요-특징">🔑 error.tsx 파일의 주요 특징</h3>
<ol>
<li><p><strong><code>&quot;use client&quot;</code></strong>: <code>error.tsx</code> 파일은 클라이언트 컴포넌트여야 합니다.</p>
</li>
<li><p><strong>필수 props</strong>: 해당 파일은 항상 두 개의 props를 받습니다.</p>
<ul>
<li><code>error</code>: JavaScript의 기본 Error 객체</li>
<li><code>reset</code>: 오류 경계를 리셋하는 함수로, 실행하면 해당 라우트 세그먼트를 다시 렌더링을 시도합니다.</li>
</ul>
</li>
</ol>
<p>이제 다시 송장 데이터의 삭제를 진행하려 하면 다음과 같은 UI가 표시됩니다.</p>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/e11eb2cc-82e1-45c5-b7fb-5e991abe7481/image.png" alt=""></p>
<hr>
<h2 id="3-handling-404-errors-with-the-notfound-function">3. Handling 404 errors with the <code>notFound</code> function</h2>
<p><code>error.tsx</code>가 모든 오류를 처리하는 데 유용하지만, <code>notFound</code> 함수는 존재하지 않는 리소스를 요청할 때 더 적절한 처리를 제공합니다.</p>
<h3 id="404-오류-상황-테스트">404 오류 상황 테스트</h3>
<p>다음 URL을 방문한다고 가정해보겠습니다 (해당 UUID는 DB에 없는 값입니다).</p>
<pre><code>http://localhost:3000/dashboard/invoices/2e94d1ed-d220-449f-9f11-f0bbceed9645/edit</code></pre><h3 id="데이터-존재-여부-확인">데이터 존재 여부 확인</h3>
<p>리소스가 존재하지 않는지 확인하기 위해 <code>fetchInvoiceById</code> 함수에서 console.log로 확인해보겠습니다.</p>
<pre><code class="language-typescript">export async function fetchInvoiceById(id: string) {
  noStore();

  try {
    const data = await sql&lt;InvoiceForm&gt;`
      SELECT
        invoices.id,
        invoices.customer_id,
        invoices.amount,
        invoices.status
      FROM invoices
      WHERE invoices.id = ${id};
    `;

    const invoice = data.rows.map((invoice) =&gt; ({
      ...invoice,
      amount: invoice.amount / 100,
    }));

    console.log(&quot;invoice is &quot;, invoice); // 출력 : invoices is []
    return invoice[0];
  } catch (error) {
    console.error(&quot;Database Error:&quot;, error);
  }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/0b2412cf-d842-4a9e-8cb7-c8be90a886a8/image.png" alt=""></p>
<h3 id="notfound-함수-적용">notFound() 함수 적용</h3>
<p>데이터베이스에 해당 송장데이터가 없다는 것을 확인했으니, <code>notFound()</code> 함수를 사용해 적절한 404 처리를 해보겠습니다.</p>
<h3 id="dashboardinvoicesideditpagetsx-수정"><code>/dashboard/invoices/[id]/edit/page.tsx</code> 수정</h3>
<pre><code class="language-tsx">import Form from &quot;@/app/ui/invoices/edit-form&quot;;
import Breadcrumbs from &quot;@/app/ui/invoices/breadcrumbs&quot;;
import { fetchCustomers, fetchInvoiceById } from &quot;@/app/lib/data&quot;;
import { notFound } from &quot;next/navigation&quot;;

export default async function Page({ params }: { params: { id: string } }) {
  const id = params.id;

  // Promise.all을 사용하여 인보이스와 고객 데이터를 동시에 가져옴
  const [invoice, customers] = await Promise.all([
    fetchInvoiceById(id), // 특정 인보이스 데이터를 가져옴
    fetchCustomers(), // 고객 데이터를 가져옴
  ]);

  if (!invoice) notFound();

  return (
    &lt;main&gt;
      &lt;Breadcrumbs
        breadcrumbs={[
          { label: &quot;Invoices&quot;, href: &quot;/dashboard/invoices&quot; },
          {
            label: &quot;Edit Invoice&quot;,
            href: `/dashboard/invoices/${id}/edit`,
            active: true,
          },
        ]}
      /&gt;
      &lt;Form invoice={invoice} customers={customers} /&gt;
    &lt;/main&gt;
  );
}</code></pre>
<h3 id="not-foundtsx-파일-생성">not-found.tsx 파일 생성</h3>
<p><code>notFound()</code> 함수가 호출되면 내부적으로 라우트 세그먼트 범위 내의 <code>not-found.tsx</code> 파일을 탐색하고, 해당 파일 내용을 렌더링합니다.</p>
<h3 id="appdashboardinvoicesideditnot-foundtsx-생성"><code>/app/dashboard/invoices/[id]/edit/not-found.tsx</code> 생성</h3>
<pre><code class="language-tsx">import Link from &quot;next/link&quot;;
import { FaceFrownIcon } from &quot;@heroicons/react/24/outline&quot;;

export default function NotFound() {
  return (
    &lt;main className=&quot;flex h-full flex-col items-center justify-center gap-2&quot;&gt;
      &lt;FaceFrownIcon className=&quot;w-10 text-gray-400&quot; /&gt;
      &lt;h2 className=&quot;text-xl font-semibold&quot;&gt;404 Not Found&lt;/h2&gt;
      &lt;p&gt;Could not find the requested invoice.&lt;/p&gt;
      &lt;Link
        href=&quot;/dashboard/invoices&quot;
        className=&quot;mt-4 rounded-md bg-blue-500 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-400&quot;
      &gt;
        Go Back
      &lt;/Link&gt;
    &lt;/main&gt;
  );
}</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/3a7de2e9-5253-43ff-9769-8be4aa9c25e1/image.png" alt=""></p>
<h3 id="🎯-중요한-우선순위">🎯 중요한 우선순위</h3>
<p><code>notFound()</code> 함수로 호출되어 표시되는 <code>not-found.tsx</code>는 <strong><code>error.tsx</code>보다 우선순위가 높습니다.</strong> </p>
<p>이는 특정 오류 상황을 더 구체적으로 처리할 때 <code>notFound()</code> 함수와 <code>not-found.tsx</code> 파일을 사용할 수 있다는 의미입니다.</p>
<hr>
<h2 id="4-참고-route-segment-라우트-세그먼트">4. 참고: Route Segment (라우트 세그먼트)</h2>
<p>위에서 몇 번 <strong>라우트 세그먼트</strong>라고 언급했는데, 이것이 정확히 무엇인지 알아보겠습니다.</p>
<h3 id="라우트-세그먼트란">라우트 세그먼트란?</h3>
<p>라우트 세그먼트(route segment)는 <strong>Next.js</strong>에서 URL 경로를 구성하는 각 <strong>경로(path)의 부분</strong>을 의미합니다.</p>
<h3 id="예시로-이해하기">예시로 이해하기</h3>
<p>다음과 같은 URL이 있다고 가정해보겠습니다.</p>
<pre><code class="language-bash">http://localhost:3000/dashboard/invoices/edit</code></pre>
<p>위 경로의 라우트 세그먼트는 다음과 같습니다:</p>
<ul>
<li><code>dashboard</code></li>
<li><code>invoices</code> </li>
<li><code>edit</code></li>
</ul>
<h3 id="폴더-구조와의-관계">폴더 구조와의 관계</h3>
<p><strong>Next.js</strong>에서 각각의 세그먼트는 <strong>폴더 구조</strong>와 1:1로 대응됩니다.</p>
<pre><code class="language-bash"># App Router 방식
/app/dashboard/invoices/edit/page.tsx

# Pages Router 방식  
/pages/dashboard/invoices/edit.tsx</code></pre>
<h3 id="ui-경계-설정">UI 경계 설정</h3>
<p>Next.js는 각 세그먼트별로 독립적인 <strong>UI 경계</strong>(error boundary)를 설정할 수 있습니다.</p>
<p>예를 들어, <code>invoices</code> 세그먼트에 오류가 발생했을 때, <code>error.tsx</code> 파일이 <code>/dashboard/invoices/</code> 폴더에 정의되어 있다면, 이 세그먼트 내의 오류를 처리할 수 있게 됩니다.</p>
<h3 id="정리">정리</h3>
<p><strong>라우트 세그먼트</strong>는 Next.js에서 특정 경로 또는 URL의 한 부분을 의미하며, 이를 통해 각 라우트 세그먼트마다 개별적으로 다음과 같은 기능들을 관리할 수 있습니다:</p>
<ul>
<li>🔥 오류 처리 (<code>error.tsx</code>, <code>not-found.tsx</code>)</li>
<li>📊 데이터 가져오기</li>
<li>🎨 레이아웃 설정</li>
<li>🛡️ 로딩 상태 관리</li>
</ul>
<hr>
<h2 id="🎯-마무리">🎯 마무리</h2>
<p>이번 Chapter에서는 Next.js에서 오류를 효과적으로 처리하는 세 가지 주요 방법을 살펴보았습니다:</p>
<ol>
<li><strong>Server Actions에서 try/catch 사용</strong>: 서버 단에서의 안전한 오류 처리</li>
<li><strong>error.tsx 파일 활용</strong>: 예상치 못한 오류에 대한 사용자 친화적 UI 제공  </li>
<li><strong>notFound() 함수와 not-found.tsx</strong>: 404 오류에 대한 구체적이고 명확한 처리</li>
</ol>
<p>이러한 오류 처리 메커니즘을 적절히 활용하면, 사용자에게 더 안정적이고 친화적인 웹 애플리케이션 경험을 제공할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 학습코스 따라하기 (Chapter 12) (2)]]></title>
            <link>https://velog.io/@lee_1124/Next.js-%ED%95%99%EC%8A%B5%EC%BD%94%EC%8A%A4-%EB%94%B0%EB%9D%BC%ED%95%98%EA%B8%B0-Chapter-12-2</link>
            <guid>https://velog.io/@lee_1124/Next.js-%ED%95%99%EC%8A%B5%EC%BD%94%EC%8A%A4-%EB%94%B0%EB%9D%BC%ED%95%98%EA%B8%B0-Chapter-12-2</guid>
            <pubDate>Tue, 29 Jul 2025 01:51:39 GMT</pubDate>
            <description><![CDATA[<h1 id="mutating-data2">Mutating Data(2)</h1>
<h2 id="🎯-개요">🎯 개요</h2>
<p>이번 포스트에서는 Next.js Server Actions를 활용하여 Invoice(송장) 데이터를 업데이트하고 삭제하는 방법을 다룹니다. 동적 라우팅, 폼 데이터 처리, 그리고 데이터베이스 CRUD 작업의 전체 과정을 단계별로 살펴보겠습니다.</p>
<hr>
<h2 id="📋-invoice-업데이트-프로세스">📋 Invoice 업데이트 프로세스</h2>
<p>Invoice를 업데이트하는 과정은 다음 5단계로 구성됩니다:</p>
<ol>
<li><strong>동적 경로 세그먼트 생성</strong> - Invoice ID를 사용한 동적 라우팅</li>
<li><strong>Invoice ID 읽기</strong> - URL 파라미터에서 ID 추출</li>
<li><strong>특정 Invoice 가져오기</strong> - 데이터베이스에서 해당 Invoice 조회</li>
<li><strong>양식에 데이터 미리 채우기</strong> - 기존 데이터로 폼 초기화</li>
<li><strong>데이터베이스 업데이트</strong> - 수정된 데이터 저장</li>
</ol>
<hr>
<h2 id="🛣️-1-동적-경로-세그먼트-생성">🛣️ 1. 동적 경로 세그먼트 생성</h2>
<h3 id="동적-라우팅의-개념">동적 라우팅의 개념</h3>
<p>Next.js에서는 폴더명을 대괄호로 감싸서 동적 경로를 생성할 수 있습니다. 이는 블로그 포스트 ID나 제품 페이지 같이 데이터에 기반한 경로가 필요할 때 유용합니다.</p>
<h3 id="폴더-구조-생성">폴더 구조 생성</h3>
<pre><code>/app/dashboard/invoices/[id]/edit/page.tsx</code></pre><h3 id="초기-페이지-설정">초기 페이지 설정</h3>
<pre><code class="language-tsx">// /app/dashboard/invoices/[id]/edit/page.tsx
export default function Page() {
  return &lt;&gt;Update Page&lt;/&gt;;
}</code></pre>
<h3 id="updateinvoice-버튼-연결">UpdateInvoice 버튼 연결</h3>
<pre><code class="language-tsx">// /app/ui/invoices/button.tsx
export function UpdateInvoice({ id }: { id: string }) {
  return (
    &lt;Link
      href={`/dashboard/invoices/${id}/edit`}
      className=&quot;rounded-md border p-2 hover:bg-gray-100&quot;
    &gt;
      &lt;PencilIcon className=&quot;w-5&quot; /&gt;
    &lt;/Link&gt;
  );
}</code></pre>
<p><strong>✨ 포인트:</strong> 템플릿 리터럴을 사용하여 동적으로 경로를 생성합니다.</p>
<hr>
<h2 id="🔍-2-invoice-id-읽기">🔍 2. Invoice ID 읽기</h2>
<h3 id="url-파라미터-추출">URL 파라미터 추출</h3>
<p>Next.js는 페이지 컴포넌트에 <code>params</code> 객체를 제공하여 동적 경로의 값을 읽을 수 있게 해줍니다.</p>
<pre><code class="language-tsx">// /app/dashboard/invoices/[id]/edit/page.tsx
import Form from &quot;@/app/ui/invoices/edit-form&quot;;
import Breadcrumbs from &quot;@/app/ui/invoices/breadcrumbs&quot;;
import { fetchCustomers } from &quot;@/app/lib/data&quot;;

export default async function Page({ params }: { params: { id: string } }) {
  const id = params.id;

  return (
    &lt;main&gt;
      &lt;Breadcrumbs
        breadcrumbs={[
          { label: &quot;Invoices&quot;, href: &quot;/dashboard/invoices&quot; },
          {
            label: &quot;Edit Invoice&quot;,
            href: `/dashboard/invoices/${id}/edit`,
            active: true,
          },
        ]}
      /&gt;
      &lt;Form invoice={invoice} customers={customers} /&gt;
    &lt;/main&gt;
  );
}</code></pre>
<hr>
<h2 id="📊-3-특정-invoice-가져오기--4-양식에-데이터-미리-채우기">📊 3. 특정 Invoice 가져오기 &amp; 4. 양식에 데이터 미리 채우기</h2>
<h3 id="병렬-데이터-페칭">병렬 데이터 페칭</h3>
<p><code>Promise.all</code>을 사용하여 Invoice 데이터와 고객 데이터를 동시에 가져와 성능을 최적화합니다.</p>
<pre><code class="language-tsx">// /app/dashboard/invoices/[id]/edit/page.tsx
import Form from &quot;@/app/ui/invoices/edit-form&quot;;
import Breadcrumbs from &quot;@/app/ui/invoices/breadcrumbs&quot;;
import { fetchCustomers, fetchInvoiceById } from &quot;@/app/lib/data&quot;;

export default async function Page({ params }: { params: { id: string } }) {
  const id = params.id;

  // 병렬로 데이터 가져오기
  const [invoice, customers] = await Promise.all([
    fetchInvoiceById(id),
    fetchCustomers(),
  ]);

  return (
    &lt;main&gt;
      &lt;Breadcrumbs
        breadcrumbs={[
          { label: &quot;Invoices&quot;, href: &quot;/dashboard/invoices&quot; },
          {
            label: &quot;Edit Invoice&quot;,
            href: `/dashboard/invoices/${id}/edit`,
            active: true,
          },
        ]}
      /&gt;
      &lt;Form invoice={invoice} customers={customers} /&gt;
    &lt;/main&gt;
  );
}</code></pre>
<h3 id="결과-화면">결과 화면</h3>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/bc3a3243-f397-41e9-a3d5-f5918d9f6fbd/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_1124/post/c21c9ff5-8839-49d0-9f4a-1d89344a964a/image.png" alt=""></p>
<hr>
<h2 id="💡-uuid-vs-자동-증가-키">💡 UUID vs 자동 증가 키</h2>
<p>이 프로젝트에서는 자동 증가 키 대신 <strong>UUID</strong>를 사용합니다.</p>
<h3 id="uuid의-장점">UUID의 장점</h3>
<ul>
<li>✅ ID 충돌 위험 제거</li>
<li>✅ 전역적으로 고유한 식별자</li>
<li>✅ 열거 공격(enumeration attack) 방지</li>
<li>✅ 대규모 데이터베이스에서 안전성 보장</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li>❌ URL이 길어짐</li>
<li>❌ 가독성 저하</li>
</ul>
<blockquote>
<p><strong>참고:</strong> 짧고 깔끔한 URL을 원한다면 자동 증가 키를 사용할 수도 있습니다.</p>
</blockquote>
<hr>
<h2 id="🔄-5-데이터베이스에서-invoice-데이터-업데이트">🔄 5. 데이터베이스에서 Invoice 데이터 업데이트</h2>
<h3 id="step-1-form-action-연결-시-주의사항">STEP 1: Form Action 연결 시 주의사항</h3>
<h4 id="❌-잘못된-방법">❌ 잘못된 방법</h4>
<pre><code class="language-tsx">// 이 방식은 동작하지 않습니다!
&lt;form action={updateInvoice(id)}&gt;
  {/* ... */}
&lt;/form&gt;</code></pre>
<p><strong>문제점:</strong></p>
<ul>
<li><code>updateInvoice(id)</code>가 <strong>즉시 실행</strong>됨</li>
<li><code>action</code> 속성은 URL 경로가 필요하지만 함수 실행 결과를 받게 됨</li>
<li>폼 제출 시점이 아닌 렌더링 시점에 함수가 호출됨</li>
</ul>
<h4 id="✅-올바른-방법---bind-사용">✅ 올바른 방법 - bind 사용</h4>
<pre><code class="language-tsx">// /app/ui/invoices/edit-form.tsx
import { updateInvoice } from &quot;@/app/lib/actions&quot;;

export default function EditInvoiceForm({
  invoice,
  customers,
}: {
  invoice: InvoiceForm;
  customers: CustomerField[];
}) {
  const updateInvoiceWithId = updateInvoice.bind(null, invoice.id);

  return (
    &lt;form action={updateInvoiceWithId}&gt;
      {/* 폼 내용 */}
    &lt;/form&gt;
  );
}</code></pre>
<p><strong>bind 메서드의 동작 방식:</strong></p>
<ul>
<li>함수의 인수(<code>invoice.id</code>)를 미리 설정한 새로운 함수 생성</li>
<li>폼 제출 시에만 실행되는 <strong>함수 레퍼런스</strong> 전달</li>
<li>즉시 실행이 아닌 <strong>지연 실행</strong> 구현</li>
</ul>
<h3 id="step-2-updateinvoice-서버-액션-구현">STEP 2: updateInvoice 서버 액션 구현</h3>
<pre><code class="language-typescript">// /app/lib/actions.ts
&quot;use server&quot;;

import { sql } from &quot;@vercel/postgres&quot;;
import { revalidatePath } from &quot;next/cache&quot;;
import { redirect } from &quot;next/navigation&quot;;
import { z } from &quot;zod&quot;;

const FormSchema = z.object({
  id: z.string(),
  customerId: z.string(),
  amount: z.coerce.number(), // &quot;100&quot; -&gt; 100 자동 변환
  status: z.enum([&quot;pending&quot;, &quot;paid&quot;]),
  date: z.string(),
});

const UpdateInvoice = FormSchema.omit({ id: true, date: true });

export async function updateInvoice(id: string, formData: FormData) {
  // 1. 폼 데이터 추출 및 검증
  const { customerId, amount, status } = UpdateInvoice.parse({
    customerId: formData.get(&quot;customerId&quot;),
    amount: formData.get(&quot;amount&quot;),
    status: formData.get(&quot;status&quot;),
  });

  // 2. 데이터 변환 (달러를 센트로)
  const amountInCents = amount * 100;

  // 3. 데이터베이스 업데이트
  await sql`
    UPDATE invoices
    SET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status}
    WHERE id = ${id}
  `;

  // 4. 캐시 무효화
  revalidatePath(&quot;/dashboard/invoices&quot;);

  // 5. 리다이렉트
  redirect(&quot;/dashboard/invoices&quot;);
}</code></pre>
<h3 id="주요-작업-단계">주요 작업 단계</h3>
<ol>
<li><strong>📝 데이터 추출</strong> - <code>formData.get()</code> 메서드 사용</li>
<li><strong>🔍 데이터 검증</strong> - Zod 스키마로 타입 검증</li>
<li><strong>💱 데이터 변환</strong> - 금액을 센트 단위로 변환</li>
<li><strong>💾 SQL 쿼리 실행</strong> - UPDATE 쿼리로 데이터 수정</li>
<li><strong>🔄 캐시 무효화</strong> - <code>revalidatePath</code>로 클라이언트 캐시 갱신</li>
<li><strong>🔀 리다이렉트</strong> - 업데이트 완료 후 목록 페이지로 이동</li>
</ol>
<h3 id="결과-화면-1">결과 화면</h3>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/377f3662-fd6e-4b5f-9b25-c78011bb95b8/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_1124/post/03eaa050-6f1d-462d-82bf-9441e5ae41eb/image.png" alt=""></p>
<hr>
<h2 id="🗑️-invoice-삭제-기능">🗑️ Invoice 삭제 기능</h2>
<h3 id="삭제-버튼-구현">삭제 버튼 구현</h3>
<pre><code class="language-tsx">// /app/ui/invoices/buttons.tsx
import { deleteInvoice } from &#39;@/app/lib/actions&#39;;

export function DeleteInvoice({ id }: { id: string }) {
  const deleteInvoiceWithId = deleteInvoice.bind(null, id);

  return (
    &lt;form action={deleteInvoiceWithId}&gt;
      &lt;button type=&quot;submit&quot; className=&quot;rounded-md border p-2 hover:bg-gray-100&quot;&gt;
        &lt;span className=&quot;sr-only&quot;&gt;Delete&lt;/span&gt;
        &lt;TrashIcon className=&quot;w-4&quot; /&gt;
      &lt;/button&gt;
    &lt;/form&gt;
  );
}</code></pre>
<h3 id="삭제-서버-액션">삭제 서버 액션</h3>
<pre><code class="language-typescript">// /app/lib/actions.ts
export async function deleteInvoice(id: string) {
  await sql`DELETE FROM invoices WHERE id = ${id}`;
  revalidatePath(&quot;/dashboard/invoices&quot;);
}</code></pre>
<h3 id="결과-화면-2">결과 화면</h3>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/49a735fb-f5c3-419b-8b2f-f562a80eb32e/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_1124/post/08cd7f6e-4967-4df7-b3ba-f1908d08a49d/image.png" alt=""></p>
<hr>
<h2 id="🎯-핵심-포인트-정리">🎯 핵심 포인트 정리</h2>
<h3 id="✨-주요-학습-내용">✨ 주요 학습 내용</h3>
<ol>
<li><strong>동적 라우팅</strong> - 대괄호를 사용한 동적 경로 생성</li>
<li><strong>bind 메서드</strong> - 서버 액션에 파라미터 안전하게 전달</li>
<li><strong>병렬 데이터 페칭</strong> - Promise.all로 성능 최적화</li>
<li><strong>데이터 검증</strong> - Zod를 활용한 타입 안전성 확보</li>
<li><strong>캐시 관리</strong> - revalidatePath로 데이터 동기화</li>
</ol>
<h3 id="🛡️-보안-고려사항">🛡️ 보안 고려사항</h3>
<ul>
<li>UUID 사용으로 열거 공격 방지</li>
<li>Zod 스키마로 입력 데이터 검증</li>
<li>SQL 인젝션 방지를 위한 템플릿 리터럴 사용</li>
</ul>
<h3 id="🚀-성능-최적화">🚀 성능 최적화</h3>
<ul>
<li>Promise.all을 통한 병렬 처리</li>
<li>revalidatePath를 통한 효율적인 캐시 관리</li>
<li>서버 액션을 통한 클라이언트-서버 간 최적화된 통신</li>
</ul>
<hr>
<p>이로써 Next.js Server Actions를 사용한 Invoice 데이터의 CRUD 작업 구현이 완료되었습니다. 다음 포스트에서는 에러 처리와 사용자 경험 개선에 대해 다루어보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 학습코스 따라하기 (Chapter 12) (1)]]></title>
            <link>https://velog.io/@lee_1124/Next.js-%ED%95%99%EC%8A%B5%EC%BD%94%EC%8A%A4-%EB%94%B0%EB%9D%BC%ED%95%98%EA%B8%B0-Chapter-12-1</link>
            <guid>https://velog.io/@lee_1124/Next.js-%ED%95%99%EC%8A%B5%EC%BD%94%EC%8A%A4-%EB%94%B0%EB%9D%BC%ED%95%98%EA%B8%B0-Chapter-12-1</guid>
            <pubDate>Tue, 29 Jul 2025 01:48:55 GMT</pubDate>
            <description><![CDATA[<h1 id="mutating-data1">Mutating Data(1)</h1>
<p>이전 장에서는 URL 검색 매개변수와 Next.js API를 사용하여 검색 및 페이지네이션 기능을 구현했었습니다. 이번에는 송장 페이지(Invoices page)에 송장 생성, 수정 및 삭제 기능을 추가하는 작업을 진행하려 합니다.</p>
<p>이번에 소개드릴 내용은 분량이 길기 때문에, 해당 챕터는 2개의 포스트로 분리하여 소개드리겠습니다!</p>
<h2 id="1-server-action이란">1. Server Action이란?</h2>
<p>React Server Actions는 비동기 코드를 서버에서 직접 실행할 수 있게 해주는 기능입니다. 이를 통해 데이터 변형을 위한 <strong>API 엔드포인트</strong>를 생성할 필요가 없어집니다.</p>
<blockquote>
<p><strong>API 엔드포인트</strong>: 애플리케이션 프로그래밍 인터페이스(API)에서 특정한 요청을 처리하는 URL</p>
</blockquote>
<p>대신 서버에서 실행되는 비동기 함수를 작성하고, 이를 클라이언트 또는 서버 컴포넌트에서 호출할 수 있습니다.</p>
<h3 id="보안-측면에서의-장점">보안 측면에서의 장점</h3>
<p>웹 애플리케이션은 다양한 위협에 취약할 수 있기 때문에 보안이 최우선입니다. Server Actions는 강력한 보안 솔루션을 제공하여 여러 유형의 공격으로부터 데이터를 보호하고, 권한 있는 접근만 허용합니다.</p>
<p>Server Actions가 사용하는 보안 기술:</p>
<ul>
<li>POST 요청</li>
<li>암호화된 클로저</li>
<li>엄격한 입력 검사</li>
<li>오류 메시지 해싱</li>
<li>호스트 제한</li>
</ul>
<p>이러한 기술들을 통해 앱의 안전성을 크게 향상시킬 수 있습니다.</p>
<h2 id="2-fetch와-server-action의-차이점">2. fetch와 Server Action의 차이점</h2>
<h3 id="데이터-처리-방식">데이터 처리 방식</h3>
<h4 id="fetch-함수-사용-방식"><code>fetch</code> 함수 사용 방식</h4>
<ul>
<li><strong>목적</strong>: 주로 데이터 가져오기(Read)</li>
<li><strong>특징</strong>: 단방향 데이터 처리</li>
<li><strong>실행 위치</strong>: 서버 컴포넌트에서 실행되지만, 데이터 변형은 클라이언트에서 별도 API 요청으로 처리</li>
</ul>
<pre><code class="language-tsx">export default async function Page() {
  const response = await fetch(&#39;https://api.example.com/data&#39;);
  const data = await response.json();

  return (
    &lt;div&gt;
      {/* 데이터를 렌더링 */}
    &lt;/div&gt;
  );
}</code></pre>
<h4 id="server-actions-방식">Server Actions 방식</h4>
<ul>
<li><strong>목적</strong>: 데이터 변형(Create, Update, Delete)</li>
<li><strong>특징</strong>: 양방향 데이터 처리 가능</li>
<li><strong>실행 위치</strong>: 서버에서 실행되며, 클라이언트나 서버 컴포넌트에서 호출</li>
</ul>
<pre><code class="language-tsx">// 서버 컴포넌트 내에서 서버 액션 정의
export default function Page() {
  // 액션 함수
  async function create(formData: FormData) {
    &#39;use server&#39;;

    // 데이터 변형 로직
  }

  // 폼을 사용하여 액션 호출
  return &lt;form action={create}&gt;...&lt;/form&gt;;
}</code></pre>
<h3 id="보안-측면-비교">보안 측면 비교</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>fetch 함수</th>
<th>Server Actions</th>
</tr>
</thead>
<tbody><tr>
<td>API 노출</td>
<td>별도 API 엔드포인트 필요 (외부 노출)</td>
<td>API 엔드포인트 외부 노출 없음</td>
</tr>
<tr>
<td>인증/권한</td>
<td>추가적인 인증 및 권한 부여 필요</td>
<td>서버에서 직접 처리로 더 안전</td>
</tr>
<tr>
<td>통신</td>
<td>추가적인 클라이언트-서버 통신</td>
<td>통신 최소화</td>
</tr>
</tbody></table>
<h3 id="요약">요약</h3>
<ol>
<li><strong>fetch 함수</strong>: 주로 데이터 읽기 용도, 별도 API 필요, 추가 보안 작업 필요</li>
<li><strong>Server Actions</strong>: 데이터 읽기/변형 모두 처리, 강화된 보안, 효율적인 통신</li>
</ol>
<p>Server Actions는 보안과 효율성 측면에서 더 나은 솔루션을 제공하며, 데이터 변형 작업을 더 간편하게 수행할 수 있게 해줍니다.</p>
<h2 id="3-server-action과-form-활용">3. Server Action과 Form 활용</h2>
<p>React에서 <code>&lt;form&gt;</code> 요소의 <code>action</code> 속성을 사용하면 Server Actions를 호출할 수 있습니다. action은 캡처된 데이터를 포함하는 native FormData 객체를 자동으로 받습니다.</p>
<pre><code class="language-tsx">// Server Component
export default function Page() {
  // Action
  async function create(formData: FormData) {
    &#39;use server&#39;;

    // 데이터 변형 로직...
  }

  // action 속성을 사용하여 action 호출
  return &lt;form action={create}&gt;...&lt;/form&gt;;
}</code></pre>
<h3 id="점진적-향상progressive-enhancement">점진적 향상(Progressive Enhancement)</h3>
<p>서버 컴포넌트 내에서 Server Action을 호출하는 가장 큰 장점 중 하나는 <strong>점진적 향상</strong>입니다.</p>
<blockquote>
<p><strong>점진적 향상이란?</strong>
웹 애플리케이션의 기능이 점진적으로 개선될 수 있음을 의미합니다. 클라이언트의 환경이나 기능에 상관없이 기본적인 동작이 보장되고, 추가적인 기능이 가능할 때 이를 활용할 수 있다는 뜻입니다.</p>
</blockquote>
<p>점진적 향상의 원칙:</p>
<ol>
<li><strong>기본 기능 보장</strong>: 최소한의 기능을 모든 사용자가 이용할 수 있도록 보장</li>
<li><strong>추가 기능 제공</strong>: 사용자의 브라우저나 네트워크 상태가 더 좋은 경우, 추가 기능으로 사용자 경험 개선</li>
</ol>
<h4 id="server-action과-점진적-향상-예시">Server Action과 점진적 향상 예시</h4>
<pre><code class="language-tsx">// 서버 컴포넌트 내에서 Server Action 정의
export default function Page() {
  // 서버 액션 함수
  async function create(formData: FormData) {
    &#39;use server&#39;;

    // 서버에서 데이터 처리 로직
    // 예: 데이터베이스에 새 항목 추가
  }

  // form을 사용하여 서버 액션 호출
  return (
    &lt;form action={create} method=&quot;post&quot;&gt;
      &lt;input type=&quot;text&quot; name=&quot;name&quot; required /&gt;
      &lt;button type=&quot;submit&quot;&gt;Submit&lt;/button&gt;
    &lt;/form&gt;
  );
}</code></pre>
<ol>
<li><p><strong>기본 기능 보장</strong>: JavaScript가 비활성화되어 있어도 Form이 작동합니다. 브라우저는 Form 데이터를 서버로 POST 요청을 통해 전송하고, 서버에서 데이터를 처리합니다.</p>
</li>
<li><p><strong>추가 기능 제공</strong>: JavaScript가 활성화된 환경에서는 Form 제출 후 페이지를 다시 로드하지 않고도 결과를 표시할 수 있습니다.</p>
</li>
</ol>
<h3 id="nextjs-캐싱과의-연동">Next.js 캐싱과의 연동</h3>
<p>Server Actions는 Next.js의 캐싱과도 긴밀하게 연관되어 있습니다. Server Action을 통해 Form이 제출되면 데이터를 변형하는 것뿐만 아니라 <code>revalidatePath</code> 및 <code>revalidateTag</code> 같은 API를 사용하여 관련 캐시를 재검증할 수도 있습니다.</p>
<h2 id="4-invoice-생성-구현하기">4. Invoice 생성 구현하기</h2>
<h3 id="4-1-invoice-생성-단계">4-1. Invoice 생성 단계</h3>
<p>Invoice를 생성하는 전체 과정은 다음과 같습니다:</p>
<ol>
<li>사용자의 입력을 수집하기 위한 양식 생성</li>
<li>서버 작업을 생성하고 양식에서 호출</li>
<li>서버 작업 내부에서 <code>formData</code> 객체로부터 데이터 추출</li>
<li>데이터베이스에 삽입할 데이터를 검증하고 준비</li>
<li>데이터를 삽입하고 오류 처리</li>
<li>캐시를 다시 검증하고 사용자를 송장 페이지로 리디렉션</li>
</ol>
<h3 id="4-2-새로운-라우트와-폼-생성">4-2. 새로운 라우트와 폼 생성</h3>
<p>먼저 <code>/invoices/create</code>에 해당하는 route를 생성하고, 이 페이지에서 invoice를 생성하기 위한 폼을 렌더링합니다.</p>
<p><strong><code>/app/dashboard/invoices/create/page.tsx</code></strong></p>
<pre><code class="language-tsx">import Form from &quot;@/app/ui/invoices/create-form&quot;;
import Breadcrumbs from &quot;@/app/ui/invoices/breadcrumbs&quot;;
import { fetchCustomers } from &quot;@/app/lib/data&quot;;

export default async function Page() {
  const customers = await fetchCustomers();

  return (
    &lt;main&gt;
      &lt;Breadcrumbs
        breadcrumbs={[
          { label: &quot;Invoices&quot;, href: &quot;/dashboard/invoices&quot; },
          {
            label: &quot;Create Invoice&quot;,
            href: &quot;/dashboard/invoices/create&quot;,
            active: true,
          },
        ]}
      /&gt;
      &lt;Form customers={customers} /&gt;
    &lt;/main&gt;
  );
}</code></pre>
<p>주요 코드 내용:</p>
<ul>
<li><code>fetchCustomers()</code>를 통해 고객 데이터를 가져와 <code>&lt;Form&gt;</code> 컴포넌트에 전달</li>
<li><code>&lt;Form&gt;</code> 컴포넌트는 고객 선택 드롭다운, 금액 입력 필드, 상태 선택 라디오 버튼, 제출 버튼으로 구성</li>
</ul>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/2af8b1f7-ad1f-47f2-b2b5-37673d2fb60f/image.png" alt=""></p>
<h3 id="4-3-서버-액션-생성">4-3. 서버 액션 생성</h3>
<p>폼이 제출될 때 호출될 서버 액션을 생성합니다. <code>lib/actions.ts</code> 파일을 만들고 <code>createInvoice</code> 함수를 정의합니다.</p>
<p><strong><code>/app/lib/actions.ts</code></strong></p>
<pre><code class="language-ts">&quot;use server&quot;;

export async function createInvoice(formData: FormData) {}</code></pre>
<p>이후 <code>&lt;Form&gt;</code> 컴포넌트에서 <code>createInvoice</code> 함수를 import하고 <code>&lt;form&gt;</code> 요소에 <code>action</code> 속성을 추가합니다.</p>
<pre><code class="language-tsx">&quot;use client&quot;;

import { CustomerField } from &quot;@/app/lib/definitions&quot;;
import Link from &quot;next/link&quot;;
import {
  CheckIcon,
  ClockIcon,
  CurrencyDollarIcon,
  UserCircleIcon,
} from &quot;@heroicons/react/24/outline&quot;;
import { Button } from &quot;../button&quot;;
import { createInvoice } from &quot;@/app/lib/actions&quot;;

export default function Form({ customers }: { customers: CustomerField[] }) {
  return (
    &lt;form action={createInvoice}&gt; {/* 서버 액션 연결 */}
     {/* ... */}
    &lt;/form&gt;
  );
}</code></pre>
<blockquote>
<p><strong>참고사항</strong></p>
<ul>
<li>HTML에서는 <code>action</code> 속성에 URL을 전달하여 폼 데이터를 제출할 대상(API 엔드포인트)을 지정합니다.</li>
<li>React에서는 <code>action</code> 속성이 특별한 prop으로 간주되어 액션 함수를 직접 호출할 수 있습니다.</li>
<li>백그라운드에서 서버 액션은 POST API 엔드포인트를 생성하므로, 수동으로 API 엔드포인트를 만들 필요가 없습니다.</li>
</ul>
</blockquote>
<h3 id="4-4-form-데이터에서-데이터-추출">4-4. Form 데이터에서 데이터 추출</h3>
<p><code>actions.ts</code> 파일에서 <code>formData</code>에서 값을 추출합니다. <code>.get(name)</code> 메서드를 사용합니다.</p>
<p><strong><code>/app/lib/actions.ts</code></strong></p>
<pre><code class="language-ts">&quot;use server&quot;;

export async function createInvoice(formData: FormData) {
  const rawFormData = {
    customerId: formData.get(&quot;customerId&quot;),
    amount: formData.get(&quot;amount&quot;),
    status: formData.get(&quot;status&quot;),
  };
  // 출력 테스트
  console.log(&quot;rawFormData is &quot;, rawFormData);
}</code></pre>
<p><strong>[이미지 삽입 위치: create invoice step 3 form 입력.png - 폼 입력 예시]</strong>
<strong>[이미지 삽입 위치: create invoice step 3 form 입력결과.png - 콘솔 출력 결과]</strong></p>
<h3 id="4-5-데이터-검증-및-준비">4-5. 데이터 검증 및 준비</h3>
<p>폼 데이터를 데이터베이스로 보내기 전에 사용자 입력값이 올바른 형식과 타입인지 확인해야 합니다.</p>
<p>현재 Form에서 받는 데이터: <code>customerId</code>, <code>amount</code>, <code>status</code></p>
<p><strong>Invoice Table이 기대하는 데이터 형식:</strong></p>
<pre><code class="language-ts">export type Invoice = {
  id: string;
  customer_id: string;
  amount: number;
  date: string;
  status: &quot;pending&quot; | &quot;paid&quot;; // string union type
};</code></pre>
<h4 id="타입-검증-및-강제-변환">타입 검증 및 강제 변환</h4>
<p><code>type=&quot;number&quot;</code>인 입력 요소도 실제로는 문자열을 반환합니다:</p>
<pre><code class="language-ts">console.log(typeof rawFormData.amount); // output: string</code></pre>
<p>TypeScript 우선 검증 라이브러리인 <strong>Zod</strong>를 사용하여 validation을 진행합니다:</p>
<pre><code class="language-ts">&quot;use server&quot;;

import { z } from &quot;zod&quot;;

// 전체 폼 데이터를 검증하는 스키마
const FormSchema = z.object({
  id: z.string(),
  customerId: z.string(),
  amount: z.coerce.number(), // 문자열을 숫자로 자동 변환
  status: z.enum([&quot;pending&quot;, &quot;paid&quot;]), // 열거형
  date: z.string(),
});

// id와 date 필드를 제거한 새 스키마
const CreateInvoice = FormSchema.omit({ id: true, date: true });

export async function createInvoice(formData: FormData) {
  // 스키마로 데이터 검증 및 변환
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get(&quot;customerId&quot;),
    amount: formData.get(&quot;amount&quot;),
    status: formData.get(&quot;status&quot;),
  });

  console.log(typeof amount); // output: number
}</code></pre>
<h4 id="센트-단위로-값-저장">센트 단위로 값 저장</h4>
<p>JavaScript의 부동 소수점 오류를 제거하고 더 높은 정확성을 보장하기 위해 데이터베이스에 금액을 센트 단위로 저장합니다:</p>
<pre><code class="language-ts">const amountInCents = amount * 100;</code></pre>
<h4 id="새-날짜-생성">새 날짜 생성</h4>
<p>Invoice 생성날짜를 &quot;YYYY-MM-DD&quot; 형식으로 생성합니다:</p>
<pre><code class="language-ts">const date = new Date().toISOString().split(&quot;T&quot;)[0];
console.log(&quot;date is &quot;, date); // output: 2024-08-23 (현재날짜기준)</code></pre>
<p>날짜 변환 과정:</p>
<ol>
<li><code>new Date()</code>: 현재 날짜와 시간을 나타내는 Date 객체 생성</li>
<li><code>.toISOString()</code>: ISO8601 형식의 문자열로 변환 (&quot;YYYY-MM-DDTHH:MM:SS.SSSZ&quot;)</li>
<li><code>.split(&quot;T&quot;)[0]</code>: &quot;T&quot; 문자 기준으로 분리하여 날짜 부분만 추출</li>
</ol>
<pre><code class="language-ts">console.log(&quot;date is &quot;, new Date()); // 2024-08-23T05:31:18.628Z
console.log(&quot;date is &quot;, new Date().toISOString()); // 2024-08-23T05:31:18.630Z
console.log(&quot;date is &quot;, new Date().toISOString().split(&quot;T&quot;)); // [ &#39;2024-08-23&#39;, &#39;05:31:18.630Z&#39; ]
console.log(&quot;date is &quot;, date); // 2024-08-23</code></pre>
<h3 id="4-6-데이터베이스에-데이터-삽입">4-6. 데이터베이스에 데이터 삽입</h3>
<p>준비된 값으로 데이터베이스에 새로운 Invoice 데이터를 삽입하는 SQL 쿼리를 생성합니다:</p>
<pre><code class="language-ts">&quot;use server&quot;;

import { sql } from &quot;@vercel/postgres&quot;;
import { z } from &quot;zod&quot;;

const FormSchema = z.object({
  id: z.string(),
  customerId: z.string(),
  amount: z.coerce.number(),
  status: z.enum([&quot;pending&quot;, &quot;paid&quot;]),
  date: z.string(),
});

const CreateInvoice = FormSchema.omit({ id: true, date: true });

export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get(&quot;customerId&quot;),
    amount: formData.get(&quot;amount&quot;),
    status: formData.get(&quot;status&quot;),
  });

  const amountInCents = amount * 100;
  const date = new Date().toISOString().split(&quot;T&quot;)[0];

  // 데이터베이스에 새 Invoice 삽입 (오류처리 미구현)
  await sql`
    INSERT INTO invoices (customer_id, amount, status, date)
    VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
  `;
}</code></pre>
<h3 id="4-7-캐시-재검증-및-리디렉션">4-7. 캐시 재검증 및 리디렉션</h3>
<h4 id="캐시-재검증">캐시 재검증</h4>
<p>Next.js는 클라이언트 사이드에서 사용자의 브라우저에 route segment를 일정 시간 동안 저장하는 <strong>라우터 캐시</strong>를 가지고 있습니다.</p>
<blockquote>
<p><strong>라우터 캐시</strong>
사용자가 웹 페이지를 이동할 때, 이전에 방문한 페이지나 데이터의 정보를 브라우저에 저장하는 기능입니다. 이를 통해 웹 애플리케이션이 더 빠르게 반응하고 부드럽게 작동할 수 있습니다.</p>
</blockquote>
<blockquote>
<p><strong>사전 로딩(Prefetching)</strong>
사용자가 현재 보고 있는 페이지와 다른 페이지를 미리 로드해 놓는 기술입니다. 사용자가 특정 페이지로 이동하기 전에 그 페이지의 리소스를 미리 받아오는 것입니다.</p>
</blockquote>
<p>라우터 캐시와 사전 로딩의 장점:</p>
<ul>
<li>사용자가 route 간에 빠르게 이동 가능</li>
<li>서버에 대한 요청 수 감소</li>
<li>페이지 전환의 매끄러운 진행</li>
</ul>
<p>Invoice 경로에서 표시되는 데이터를 업데이트했으므로, 캐시를 지우고 서버에 새로운 요청을 트리거해야 합니다. Next.js의 <code>revalidatePath</code> 함수를 사용합니다:</p>
<pre><code class="language-ts">import { revalidatePath } from &quot;next/cache&quot;;

// ... 기존 코드 ...

await sql`
  INSERT INTO invoices (customer_id, amount, status, date)
  VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
`;

revalidatePath(&quot;/dashboard/invoices&quot;); // 캐시 재검증</code></pre>
<h4 id="사용자-리디렉션">사용자 리디렉션</h4>
<p>데이터베이스 업데이트 후 사용자를 <code>/dashboard/invoices</code> 페이지로 리디렉션합니다. Next.js의 <code>redirect</code> 함수를 사용합니다:</p>
<p><strong>최종 완성된 <code>createInvoice</code> 함수:</strong></p>
<pre><code class="language-ts">&quot;use server&quot;;

import { sql } from &quot;@vercel/postgres&quot;;
import { revalidatePath } from &quot;next/cache&quot;;
import { redirect } from &quot;next/navigation&quot;;
import { z } from &quot;zod&quot;;

const FormSchema = z.object({
  id: z.string(),
  customerId: z.string(),
  amount: z.coerce.number(),
  status: z.enum([&quot;pending&quot;, &quot;paid&quot;]),
  date: z.string(),
});

const CreateInvoice = FormSchema.omit({ id: true, date: true });

export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get(&quot;customerId&quot;),
    amount: formData.get(&quot;amount&quot;),
    status: formData.get(&quot;status&quot;),
  });

  const amountInCents = amount * 100;
  const date = new Date().toISOString().split(&quot;T&quot;)[0];

  await sql`
    INSERT INTO invoices (customer_id, amount, status, date)
    VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
  `;

  revalidatePath(&quot;/dashboard/invoices&quot;); // 캐시 재검증
  redirect(&quot;/dashboard/invoices&quot;); // 사용자 리디렉션
}</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/c87f592a-b788-470c-979e-4eaf24a11b99/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_1124/post/2f42039f-6714-4186-86ea-c4faab7cef44/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_1124/post/b76d6862-8bbc-480d-9027-01c802cac641/image.png" alt=""></p>
<p>이제 데이터베이스에 새로운 Invoice가 추가된 후, 사용자가 자동으로 <code>/dashboard/invoices</code> 경로로 리디렉션되어 업데이트된 목록을 확인할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 학습코스 따라하기 (Chapter 11)]]></title>
            <link>https://velog.io/@lee_1124/Next.js-%ED%95%99%EC%8A%B5%EC%BD%94%EC%8A%A4-%EB%94%B0%EB%9D%BC%ED%95%98%EA%B8%B0-Chapter-11</link>
            <guid>https://velog.io/@lee_1124/Next.js-%ED%95%99%EC%8A%B5%EC%BD%94%EC%8A%A4-%EB%94%B0%EB%9D%BC%ED%95%98%EA%B8%B0-Chapter-11</guid>
            <pubDate>Mon, 28 Jul 2025 04:27:52 GMT</pubDate>
            <description><![CDATA[<h1 id="nextjs에서-url-검색-매개변수를-활용한-검색-및-페이징-구현">Next.js에서 URL 검색 매개변수를 활용한 검색 및 페이징 구현</h1>
<blockquote>
<p>이전 Chapter에서는 Partial Prerendering을 통해 Next.js에서 경로의 정적 부분을 미리 렌더링하고, 동적 부분은 사용자가 요청할 때까지 지연시키는 방법에 대해 알아보았습니다.</p>
</blockquote>
<p>이번 Chapter에서는 <strong>URL 검색 매개변수</strong>를 사용하여 검색 및 페이징을 구현하는 방법에 대해 알아보겠습니다.</p>
<h2 id="📋-목차">📋 목차</h2>
<ol>
<li><a href="#1-%EC%8B%9C%EC%9E%91-%EC%BD%94%EB%93%9C-%EC%84%A4%EC%A0%95">시작 코드 설정</a></li>
<li><a href="#2-%EA%B2%80%EC%83%89-%EA%B8%B0%EB%8A%A5-%EC%B6%94%EA%B0%80">검색 기능 추가</a></li>
<li><a href="#3-%EB%94%94%EB%B0%94%EC%9A%B4%EC%8B%B1%EC%9C%BC%EB%A1%9C-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94">디바운싱으로 성능 최적화</a></li>
<li><a href="#4-%ED%8E%98%EC%9D%B4%EC%A7%95-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84">페이징 기능 구현</a></li>
</ol>
<hr>
<h2 id="1-시작-코드-설정">1. 시작 코드 설정</h2>
<p>본격적으로 시작하기 앞서, <code>/app/dashboard/invoices/page.tsx</code> 경로에 파일을 추가하고 아래 코드를 붙여넣어주세요.</p>
<h3 id="주요-컴포넌트-소개">주요 컴포넌트 소개</h3>
<ul>
<li><strong><code>&lt;Search/&gt;</code></strong>: 사용자가 특정 송장을 검색할 수 있도록 하는 컴포넌트</li>
<li><strong><code>&lt;Pagination/&gt;</code></strong>: 사용자가 송장의 페이지 간을 탐색할 수 있게 하는 컴포넌트</li>
<li><strong><code>&lt;Table/&gt;</code></strong>: 송장을 표시하는 컴포넌트</li>
</ul>
<pre><code class="language-tsx">import Pagination from &quot;@/app/ui/invoices/pagination&quot;;
import Search from &quot;@/app/ui/search&quot;;
import Table from &quot;@/app/ui/invoices/table&quot;;
import { CreateInvoice } from &quot;@/app/ui/invoices/buttons&quot;;
import { InvoicesTableSkeleton } from &quot;@/app/ui/skeletons&quot;;
import { Suspense } from &quot;react&quot;;
import { lusitana } from &quot;../ui/font&quot;;

export default async function Page() {
  return (
    &lt;div className=&quot;w-full&quot;&gt;
      &lt;div className=&quot;flex w-full items-center justify-between&quot;&gt;
        &lt;h1 className={`${lusitana.className} text-2xl`}&gt;Invoices&lt;/h1&gt;
      &lt;/div&gt;
      &lt;div className=&quot;mt-4 flex items-center justify-between gap-2 md:mt-8&quot;&gt;
        &lt;Search placeholder=&quot;Search invoices...&quot; /&gt;
        &lt;CreateInvoice /&gt;
      &lt;/div&gt;
      {/*  &lt;Suspense key={query + currentPage} fallback={&lt;InvoicesTableSkeleton /&gt;}&gt;
        &lt;Table query={query} currentPage={currentPage} /&gt;
      &lt;/Suspense&gt; */}
      &lt;div className=&quot;mt-5 flex w-full justify-center&quot;&gt;
        {/* &lt;Pagination totalPages={totalPages} /&gt; */}
      &lt;/div&gt;
    &lt;/div&gt;
  );
}</code></pre>
<h3 id="검색-기능의-작동-원리">검색 기능의 작동 원리</h3>
<p>검색 기능은 클라이언트와 서버 두 곳에 걸쳐 작동합니다.</p>
<ol>
<li><strong>클라이언트</strong>: 사용자가 검색하면 URL 매개변수가 업데이트</li>
<li><strong>서버</strong>: 업데이트된 매개변수를 통해 데이터를 불러옴</li>
<li><strong>렌더링</strong>: 테이블 컴포넌트가 새로운 데이터를 기반으로 서버에서 다시 렌더링</li>
</ol>
<h3 id="개발-단계-개요">개발 단계 개요</h3>
<ol>
<li>사용자의 입력을 감지</li>
<li>검색 매개변수로 URL을 업데이트</li>
<li>입력 필드와 URL을 동기화</li>
<li>검색 쿼리에 따라 테이블을 업데이트</li>
</ol>
<h3 id="💡-왜-url-검색-매개변수를-사용할까">💡 왜 URL 검색 매개변수를 사용할까?</h3>
<p>클라이언트 측 상태로 검색 상태를 관리하는 것에 익숙하다면, 이 패턴이 새로울 수 있습니다. URL 매개변수를 통해 검색 상태를 관리하는 방법은 다음과 같은 이점이 있습니다:</p>
<h4 id="1-북마크-가능-및-공유-가능한-url">1. 북마크 가능 및 공유 가능한 URL</h4>
<p>검색 매개변수가 URL에 포함되어 있기 때문에 사용자는 검색 쿼리와 필터를 포함한 현재 애플리케이션 상태를 북마크하거나 공유할 수 있습니다.</p>
<h4 id="2-서버-사이드-렌더링-및-초기-로드">2. 서버 사이드 렌더링 및 초기 로드</h4>
<p>URL 매개변수는 서버에서 초기 상태를 렌더링하는 데 직접 사용될 수 있어 SSR(서버 사이드 렌더링)을 처리하기가 더 쉬워집니다.</p>
<h4 id="3-분석-및-추적">3. 분석 및 추적</h4>
<p>검색 쿼리와 필터가 URL에 직접 포함되어 있기 때문에, 추가 클라이언트 측 로직 구현 없이 사용자 행동을 추적하기가 더 쉽습니다.</p>
<hr>
<h2 id="2-검색-기능-추가">2. 검색 기능 추가</h2>
<p>검색 기능을 구현하기 위해 사용할 Next.js 클라이언트 <strong>hook</strong>들을 알아보겠습니다.</p>
<h3 id="주요-hook-소개">주요 Hook 소개</h3>
<ul>
<li><strong><code>useSearchParams</code></strong>: 현재 URL의 매개변수에 접근<ul>
<li>예: URL <code>/dashboard/invoices?page=1&amp;query=pending</code>의 검색 매개변수는 <code>{page: &#39;1&#39;, query: &#39;pending&#39;}</code></li>
</ul>
</li>
<li><strong><code>usePathname</code></strong>: 현재 URL의 경로명을 읽기<ul>
<li>예: 경로 <code>/dashboard/invoices</code>의 경우 <code>&#39;/dashboard/invoices&#39;</code>를 반환</li>
</ul>
</li>
<li><strong><code>useRouter</code></strong>: 클라이언트 컴포넌트 내에서 프로그래밍 방식으로 경로 간 이동 가능</li>
</ul>
<h3 id="2-1-사용자-입력-감지">2-1. 사용자 입력 감지</h3>
<p>먼저 <code>&lt;Search&gt;</code> 컴포넌트의 내용을 확인해보겠습니다.</p>
<blockquote>
<p><strong>참고</strong>: <code>&quot;use client&quot;</code> 지시어는 클라이언트 컴포넌트(RSC)에서 사용되며, 이벤트 리스너 및 hook을 사용하기 위해 필요합니다.</p>
</blockquote>
<pre><code class="language-tsx">&quot;use client&quot;;

import { MagnifyingGlassIcon } from &quot;@heroicons/react/24/outline&quot;;

export default function Search({ placeholder }: { placeholder: string }) {

  function handleSearch(term: string) {
    console.log(term);
  }

  return (
    &lt;div className=&quot;relative flex flex-1 flex-shrink-0&quot;&gt;
      &lt;label htmlFor=&quot;search&quot; className=&quot;sr-only&quot;&gt;
        Search
      &lt;/label&gt;
      &lt;input
        className=&quot;peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500&quot;
        placeholder={placeholder}
        onChange={(e) =&gt; {
          handleSearch(e.target.value);
        }}
      /&gt;
      &lt;MagnifyingGlassIcon className=&quot;absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900&quot; /&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>위 코드에서는 <code>&lt;input&gt;</code> 태그에서 사용자 입력이 발생할 때마다 <code>handleSearch()</code> 메소드를 호출합니다.</p>
<h3 id="2-2-검색-매개변수로-url-업데이트">2-2. 검색 매개변수로 URL 업데이트</h3>
<p>이제 <code>useSearchParams</code> hook을 import하여 URL을 업데이트해보겠습니다.</p>
<pre><code class="language-tsx">&quot;use client&quot;;

import { MagnifyingGlassIcon } from &quot;@heroicons/react/24/outline&quot;;
import { useSearchParams, usePathname, useRouter } from &quot;next/navigation&quot;;

export default function Search({ placeholder }: { placeholder: string }) {
  const searchParams = useSearchParams();
  const pathname = usePathname();
  const { replace } = useRouter();

  function handleSearch(term: string) {
    const params = new URLSearchParams(searchParams);

    if (term) {
      params.set(&quot;query&quot;, term);
    } else {
      params.delete(&quot;query&quot;);
    }

    replace(`${pathname}?${params.toString()}`);
  }

  return (
    // ... 이전과 동일한 JSX
  );
}</code></pre>
<ul>
<li><p>매개변수가 없을 때
<img src="https://velog.velcdn.com/images/lee_1124/post/73e4cce8-2029-470b-928e-31e4d82916b3/image.png" alt=""></p>
</li>
<li><p>매개변수가 있을 때 (URLSearchParams의 size가 늘어남)
<img src="https://velog.velcdn.com/images/lee_1124/post/9ef2e4ad-0978-45fe-b0b6-c97e9279ba1c/image.png" alt=""></p>
</li>
</ul>
<h4 id="주요-개념-설명">주요 개념 설명</h4>
<ul>
<li><strong><code>URLSearchParams</code></strong>: URL 쿼리 매개변수를 조작하기 위한 유틸리티 메서드를 제공하는 Web API</li>
<li><strong><code>pathname</code></strong>: 현재 URL 경로명 (예: <code>&quot;/dashboard/invoices&quot;</code>)</li>
<li><strong><code>replace()</code></strong>: 사용자의 검색 데이터를 포함하여 URL을 업데이트<ul>
<li>예: 사용자가 &quot;Lee&quot;를 검색하면 <code>/dashboard/invoices?query=lee</code>가 됨</li>
</ul>
</li>
</ul>
<h4 id="🚀-클라이언트-측-탐색의-장점">🚀 클라이언트 측 탐색의 장점</h4>
<p>Next.js의 클라이언트 측 탐색 기능으로 페이지를 새로고침하지 않고도 URL을 업데이트할 수 있습니다. 이는 SPA(Single Page Application) 방식으로 동작하며, 더 빠르고 부드러운 사용자 경험을 제공합니다.</p>
<h3 id="2-3-url과-입력-상태-동기화">2-3. URL과 입력 상태 동기화</h3>
<p>입력 필드가 URL과 동기화되고 공유할 때 입력 필드가 자동으로 채워지도록 하기 위해 <code>searchParams</code>에서 값을 읽어와 <code>defaultValue</code>로 전달합니다.</p>
<pre><code class="language-tsx">&lt;input
  className=&quot;peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500&quot;
  placeholder={placeholder}
  onChange={(e) =&gt; {
    handleSearch(e.target.value);
  }}
  defaultValue={searchParams.get(&quot;query&quot;)?.toString()}
/&gt;</code></pre>
<h4 id="💡-defaultvalue-vs-value">💡 defaultValue vs. value</h4>
<ul>
<li><strong><code>defaultValue</code></strong>: 입력값을 state로 관리하지 않는 경우, 네이티브 입력이 자체적으로 값을 관리</li>
<li><strong><code>value</code></strong>: 입력값을 state로 관리하는 경우, React가 input 태그의 입력 상태를 관리 (제어 컴포넌트)</li>
</ul>
<h3 id="2-4-테이블-업데이트">2-4. 테이블 업데이트</h3>
<p>마지막으로 검색 쿼리를 반영하여 테이블 컴포넌트를 업데이트해야 합니다.</p>
<p><strong>기본 페이지 컴포넌트</strong>는 <code>searchParams</code>라는 <code>props</code>를 받아올 수 있습니다. Next.js는 URL의 <code>search params</code>를 <code>props</code>로 자동으로 전달해주며, 이를 전달받는 컴포넌트는 디렉터리 경로별 기본 페이지 컴포넌트입니다.</p>
<pre><code class="language-tsx">import Pagination from &quot;@/app/ui/invoices/pagination&quot;;
import Search from &quot;@/app/ui/search&quot;;
import Table from &quot;@/app/ui/invoices/table&quot;;
import { CreateInvoice } from &quot;@/app/ui/invoices/buttons&quot;;
import { InvoicesTableSkeleton } from &quot;@/app/ui/skeletons&quot;;
import { Suspense } from &quot;react&quot;;
import { lusitana } from &quot;../../ui/font&quot;;

export default async function Page({
  searchParams,
}: {
  searchParams?: {
    query?: string;
    page?: string;
  };
}) {
  const query = searchParams?.query || &quot;&quot;;
  const currentPage = Number(searchParams?.page) || 1;

  return (
    &lt;div className=&quot;w-full&quot;&gt;
      &lt;div className=&quot;flex w-full items-center justify-between&quot;&gt;
        &lt;h1 className={`${lusitana.className} text-2xl`}&gt;Invoices&lt;/h1&gt;
      &lt;/div&gt;
      &lt;div className=&quot;mt-4 flex items-center justify-between gap-2 md:mt-8&quot;&gt;
        &lt;Search placeholder=&quot;Search invoices...&quot; /&gt;
        &lt;CreateInvoice /&gt;
      &lt;/div&gt;
      &lt;Suspense key={query + currentPage} fallback={&lt;InvoicesTableSkeleton /&gt;}&gt;
        &lt;Table query={query} currentPage={currentPage} /&gt;
      &lt;/Suspense&gt;
      &lt;div className=&quot;mt-5 flex w-full justify-center&quot;&gt;
        {/* &lt;Pagination totalPages={totalPages} /&gt; */}
      &lt;/div&gt;
    &lt;/div&gt;
  );
}</code></pre>
<h4 id="table-컴포넌트에서-데이터-처리">Table 컴포넌트에서 데이터 처리</h4>
<p><code>&lt;Table&gt;</code> 컴포넌트에서는 <code>query</code>와 <code>currentPage</code>의 <code>props</code>를 받아 <code>fetchFilteredInvoices()</code> 함수를 호출하여 해당 쿼리에 맞는 invoices 데이터를 반환받습니다.</p>
<pre><code class="language-tsx">// /app/ui/invoices/table.tsx
export default async function InvoicesTable({
  query,
  currentPage,
}: {
  query: string;
  currentPage: number;
}) {
  const invoices = await fetchFilteredInvoices(query, currentPage);
  // ...
}</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/eaddf309-34d5-4017-879a-8bfe6e4e90c2/image.png" alt=""></p>
<h4 id="📌-usesearchparams-hook-vs-searchparams-props-사용-시기">📌 useSearchParams hook vs searchParams props 사용 시기</h4>
<p>검색 파라미터를 추출하는 방법으로 2가지 방법을 사용했습니다. 어떤 방법을 사용할지는 클라이언트/서버 중 어디에서 작업을 수행하는지에 따라 다릅니다.</p>
<ul>
<li><strong>클라이언트 컴포넌트</strong>: <code>useSearchParams</code> hook 사용</li>
<li><strong>서버 컴포넌트</strong>: <code>searchParams</code> props 사용</li>
</ul>
<hr>
<h2 id="3-디바운싱으로-성능-최적화">3. 디바운싱으로 성능 최적화</h2>
<p>현재 코드에서는 사용자가 값을 입력할 때마다 <code>handleSearch()</code> 메소드가 실행되어 URL이 업데이트됩니다. 이는 매 입력마다 데이터베이스 쿼리를 날리게 되어 성능상 문제가 될 수 있습니다.</p>
<h3 id="디바운싱이란">디바운싱이란?</h3>
<p><strong>디바운싱(Debouncing)</strong>은 함수가 실행되는 빈도를 제한하는 프로그래밍 방법입니다. 사용자가 입력을 멈췄을 때만 데이터베이스에 쿼리를 날리는 것이 목표입니다.</p>
<blockquote>
<p><strong>디바운싱 미사용: 입력할 때마다 입력값 감지하여 검색 API 호출</strong>
<img src="https://velog.velcdn.com/images/lee_1124/post/32641886-6d51-4dcb-9bd9-35727706887f/image.png" alt=""></p>
</blockquote>
<blockquote>
<p><strong>디바운싱 사용: 입력 멈출 때 입력값 완전감지하여 검색 API 호출</strong>
<img src="https://velog.velcdn.com/images/lee_1124/post/e10608ba-6c4e-4381-a6e3-d581e7493ab8/image.png" alt=""></p>
</blockquote>
<h4 id="🔄-디바운싱-vs-쓰로틀링">🔄 디바운싱 vs 쓰로틀링</h4>
<ul>
<li><strong>쓰로틀링</strong>: 마지막 함수가 호출되고 일정시간이 지나기 전에는 다시 호출되지 않도록 하는 방법</li>
<li><strong>디바운싱</strong>: 연이어 호출되는 함수 중, 마지막이나 처음 함수만 호출되도록 하는 방법</li>
</ul>
<h3 id="디바운싱-구현">디바운싱 구현</h3>
<p><code>use-debounce</code> 라이브러리를 사용하여 디바운싱을 구현해보겠습니다.</p>
<pre><code class="language-bash">pnpm i use-debounce</code></pre>
<pre><code class="language-tsx">&quot;use client&quot;;

import { useDebouncedCallback } from &quot;use-debounce&quot;;
import { MagnifyingGlassIcon } from &quot;@heroicons/react/24/outline&quot;;
import { useSearchParams, usePathname, useRouter } from &quot;next/navigation&quot;;

export default function Search({ placeholder }: { placeholder: string }) {
  const searchParams = useSearchParams();
  const pathname = usePathname();
  const { replace } = useRouter();

  const handleSearch = useDebouncedCallback((term: string) =&gt; {
    const params = new URLSearchParams(searchParams);

    if (term) {
      params.set(&quot;query&quot;, term);
    } else {
      params.delete(&quot;query&quot;);
    }

    replace(`${pathname}?${params.toString()}`);
  }, 300);

  return (
    &lt;div className=&quot;relative flex flex-1 flex-shrink-0&quot;&gt;
      &lt;label htmlFor=&quot;search&quot; className=&quot;sr-only&quot;&gt;
        Search
      &lt;/label&gt;
      &lt;input
        className=&quot;peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500&quot;
        placeholder={placeholder}
        onChange={(e) =&gt; {
          handleSearch(e.target.value);
        }}
        defaultValue={searchParams.get(&quot;query&quot;)?.toString()}
      /&gt;
      &lt;MagnifyingGlassIcon className=&quot;absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900&quot; /&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>이제 사용자가 입력을 멈춘 후 300ms 동안 재입력이 발생하지 않아야 콜백함수가 실행됩니다.</p>
<hr>
<h2 id="4-페이징-기능-구현">4. 페이징 기능 구현</h2>
<h3 id="4-1-페이징-구현-전-알아둘-점">4-1. 페이징 구현 전 알아둘 점</h3>
<p>검색 기능을 도입한 후, 테이블에는 한 번에 6개의 송장만 표시됩니다. 이는 <code>fetchFilteredInvoices()</code> 함수가 페이지당 최대 6개의 송장을 반환하기 때문입니다.</p>
<p>페이징 기능을 추가하면 사용자가 모든 송장to 보기 위해 다른 페이지로 이동할 수 있게 됩니다.</p>
<h4 id="🔒-보안-고려사항">🔒 보안 고려사항</h4>
<p><code>&lt;Pagination/&gt;</code> 컴포넌트는 클라이언트 컴포넌트입니다. 클라이언트에서 데이터를 가져오게 되면 데이터베이스 비밀이 노출될 수 있으므로, 서버에서 데이터를 가져와 컴포넌트에 <code>props</code>로 전달하는 것이 좋습니다.</p>
<h3 id="4-2-페이징-코드-구현">4-2. 페이징 코드 구현</h3>
<p>먼저 페이지 컴포넌트를 수정하여 전체 페이지 수를 계산하고 Pagination 컴포넌트에 전달합니다.</p>
<pre><code class="language-tsx">// /app/dashboard/invoices/page.tsx
import Pagination from &quot;@/app/ui/invoices/pagination&quot;;
import Search from &quot;@/app/ui/search&quot;;
import Table from &quot;@/app/ui/invoices/table&quot;;
import { CreateInvoice } from &quot;@/app/ui/invoices/buttons&quot;;
import { InvoicesTableSkeleton } from &quot;@/app/ui/skeletons&quot;;
import { Suspense } from &quot;react&quot;;
import { lusitana } from &quot;../../ui/font&quot;;
import { fetchInvoicesPages } from &quot;@/app/lib/data&quot;;

export default async function Page({
  searchParams,
}: {
  searchParams?: {
    query?: string;
    page?: string;
  };
}) {
  const query = searchParams?.query || &quot;&quot;;
  const currentPage = Number(searchParams?.page) || 1;
  const totalPages = await fetchInvoicesPages(query);

  return (
    &lt;div className=&quot;w-full&quot;&gt;
      &lt;div className=&quot;flex w-full items-center justify-between&quot;&gt;
        &lt;h1 className={`${lusitana.className} text-2xl`}&gt;Invoices&lt;/h1&gt;
      &lt;/div&gt;
      &lt;div className=&quot;mt-4 flex items-center justify-between gap-2 md:mt-8&quot;&gt;
        &lt;Search placeholder=&quot;Search invoices...&quot; /&gt;
        &lt;CreateInvoice /&gt;
      &lt;/div&gt;
      &lt;Suspense key={query + currentPage} fallback={&lt;InvoicesTableSkeleton /&gt;}&gt;
        &lt;Table query={query} currentPage={currentPage} /&gt;
      &lt;/Suspense&gt;
      &lt;div className=&quot;mt-5 flex w-full justify-center&quot;&gt;
        &lt;Pagination totalPages={totalPages} /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}</code></pre>
<h3 id="pagination-컴포넌트-구현">Pagination 컴포넌트 구현</h3>
<pre><code class="language-tsx">// /app/ui/invoices/pagination.tsx
&#39;use client&#39;;

import { ArrowLeftIcon, ArrowRightIcon } from &#39;@heroicons/react/24/outline&#39;;
import clsx from &#39;clsx&#39;;
import Link from &#39;next/link&#39;;
import { generatePagination } from &#39;@/app/lib/utils&#39;;
import { usePathname, useSearchParams } from &#39;next/navigation&#39;;

export default function Pagination({ totalPages }: { totalPages: number }) {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const currentPage = Number(searchParams.get(&#39;page&#39;)) || 1;

  const createPageURL = (pageNumber: number | string) =&gt; {
    const params = new URLSearchParams(searchParams);
    params.set(&#39;page&#39;, pageNumber.toString());
    return `${pathname}?${params.toString()}`;
  };

  // ... 나머지 컴포넌트 로직
}</code></pre>
<h3 id="검색-시-페이지-리셋">검색 시 페이지 리셋</h3>
<p>사용자가 새 검색 쿼리를 입력할 때 페이지 번호를 1로 리셋해야 합니다.</p>
<pre><code class="language-tsx">// /app/ui/search.tsx
&#39;use client&#39;;

import { MagnifyingGlassIcon } from &#39;@heroicons/react/24/outline&#39;;
import { usePathname, useRouter, useSearchParams } from &#39;next/navigation&#39;;
import { useDebouncedCallback } from &#39;use-debounce&#39;;

export default function Search({ placeholder }: { placeholder: string }) {
  const searchParams = useSearchParams();
  const { replace } = useRouter();
  const pathname = usePathname();

  const handleSearch = useDebouncedCallback((term) =&gt; {
    const params = new URLSearchParams(searchParams);
    params.set(&#39;page&#39;, &#39;1&#39;); // 검색 시 페이지를 1로 리셋

    if (term) {
      params.set(&#39;query&#39;, term);
    } else {
      params.delete(&#39;query&#39;);
    }

    replace(`${pathname}?${params.toString()}`);
  }, 300);

  // ... 나머지 컴포넌트
}</code></pre>
<hr>
<h2 id="🎯-마무리">🎯 마무리</h2>
<p>이번 챕터에서는 Next.js에서 URL 검색 매개변수를 활용하여 다음 기능들을 구현했습니다:</p>
<ul>
<li>✅ <strong>검색 기능</strong>: URL 매개변수를 통한 실시간 검색</li>
<li>✅ <strong>디바운싱</strong>: 성능 최적화를 위한 검색 지연</li>
<li>✅ <strong>페이징</strong>: 대량 데이터의 효율적인 표시</li>
<li>✅ <strong>URL 동기화</strong>: 북마크 가능하고 공유 가능한 상태 관리</li>
</ul>
<p>이러한 패턴들은 사용자 경험을 향상시키고, SEO에 유리하며, 서버 사이드 렌더링을 효과적으로 활용할 수 있게 해줍니다.</p>
<h3 id="📚-참고자료">📚 참고자료</h3>
<ul>
<li><a href="https://nextjs.org/docs/app/api-reference/functions/use-search-params">Next.js 공식 문서 - useSearchParams</a></li>
<li><a href="https://github.com/xnimorz/use-debounce">use-debounce 라이브러리</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams">URLSearchParams - MDN</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 학습코스 따라하기 (Chapter 10)]]></title>
            <link>https://velog.io/@lee_1124/10.-Next.js14-%ED%95%99%EC%8A%B5%EC%BD%94%EC%8A%A4-%EB%94%B0%EB%9D%BC%ED%95%98%EA%B8%B0-Chapter-10</link>
            <guid>https://velog.io/@lee_1124/10.-Next.js14-%ED%95%99%EC%8A%B5%EC%BD%94%EC%8A%A4-%EB%94%B0%EB%9D%BC%ED%95%98%EA%B8%B0-Chapter-10</guid>
            <pubDate>Mon, 28 Jul 2025 02:46:41 GMT</pubDate>
            <description><![CDATA[<h1 id="nextjs-pprpartial-prerendering-가이드">Next.js PPR(Partial Prerendering) 가이드</h1>
<p>지금까지 빌드 및 revalidation 시간에 데이터 fetch 및 렌더링을 진행하는 <strong>정적 렌더링</strong>과 사용자 요청 등이 발생할 때 렌더링을 진행하는 <strong>동적 렌더링</strong>에 대해 알아보았습니다.</p>
<p>이번 포스팅에서는 <strong>PPR(Partial Prerendering)</strong>을 사용하여 정적 렌더링, 동적 렌더링, 스트리밍을 동일한 경로로 결합하는 방법을 알아보겠습니다.</p>
<h2 id="1-static-vs-dynamic-routes">1. Static vs. Dynamic Routes</h2>
<p>오늘날 대부분의 웹 애플리케이션에서는 전체 애플리케이션 또는 특정 경로(route)에 한해 정적 렌더링과 동적 렌더링 중 하나를 선택합니다.</p>
<ul>
<li>Next.js에서는 특정 경로(route)에서 동적 함수를 호출하면 (<code>noStore()</code>, <code>cookies()</code> 등) 해당 경로 전체가 동적으로 변화합니다.</li>
</ul>
<p>하지만 보통 대부분의 경로는 완전히 정적이거나 동적이지 않습니다.</p>
<p>예를 들어, 전자상거래 사이트를 생각해보겠습니다. 제품 정보 페이지의 대부분을 정적으로 렌더링하고 싶을 수 있지만, 사용자의 장바구니와 추천 제품 등은 동적으로 가져와야 합니다.</p>
<h3 id="대시보드-페이지-예시">대시보드 페이지 예시</h3>
<p>지금까지 만들어 본 대시보드 페이지를 생각해보면:</p>
<ul>
<li>페이지 사이드에 위치한 <code>&lt;SideNav/&gt;</code>는 <code>fetch</code>해오는 데이터에 의존하지 않기 때문에 정적으로 개발할 수 있습니다.</li>
<li>그러나 실질적으로 <code>fetch</code>해오는 데이터를 기반으로 화면을 구성하는 <code>&lt;Page/&gt;</code> 내의 컴포넌트들은 동적으로 개발해야 합니다.</li>
</ul>
<h4 id="sidenav-컴포넌트-정적"><code>&lt;SideNav/&gt;</code> 컴포넌트 (정적)</h4>
<pre><code class="language-tsx">import Link from &#39;next/link&#39;;
import NavLinks from &#39;@/app/ui/dashboard/nav-links&#39;;
import AcmeLogo from &#39;@/app/ui/acme-logo&#39;;
import { PowerIcon } from &#39;@heroicons/react/24/outline&#39;;

export default function SideNav() {
  return (
    &lt;div className=&quot;flex h-full flex-col px-3 py-4 md:px-2&quot;&gt;
      &lt;Link
        className=&quot;mb-2 flex h-20 items-end justify-start rounded-md bg-blue-600 p-4 md:h-40&quot;
        href=&quot;/&quot;
      &gt;
        &lt;div className=&quot;w-32 text-white md:w-40&quot;&gt;
          &lt;AcmeLogo /&gt;
        &lt;/div&gt;
      &lt;/Link&gt;
      &lt;div className=&quot;flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2&quot;&gt;
        &lt;NavLinks /&gt;
        &lt;div className=&quot;hidden h-auto w-full grow rounded-md bg-gray-50 md:block&quot;&gt;&lt;/div&gt;
        &lt;form&gt;
          &lt;button className=&quot;flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3&quot;&gt;
            &lt;PowerIcon className=&quot;w-6&quot; /&gt;
            &lt;div className=&quot;hidden md:block&quot;&gt;Sign Out&lt;/div&gt;
          &lt;/button&gt;
        &lt;/form&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}</code></pre>
<h4 id="cardstsx-컴포넌트-동적"><code>cards.tsx</code> 컴포넌트 (동적)</h4>
<pre><code class="language-tsx">import {
  BanknotesIcon,
  ClockIcon,
  UserGroupIcon,
  InboxIcon,
} from &quot;@heroicons/react/24/outline&quot;;
import { lusitana } from &quot;../font&quot;;
import { fetchCardData } from &quot;@/app/lib/data&quot;;

const iconMap = {
  collected: BanknotesIcon,
  customers: UserGroupIcon,
  pending: ClockIcon,
  invoices: InboxIcon,
};

export default async function CardWrapper() {
  // 동적 데이터 fetch
  const {
    numberOfCustomers,
    numberOfInvoices,
    totalPaidInvoices,
    totalPendingInvoices,
  } = await fetchCardData();

  return (
    &lt;&gt;
      &lt;Card title=&quot;Collected&quot; value={totalPaidInvoices} type=&quot;collected&quot; /&gt;
      &lt;Card title=&quot;Pending&quot; value={totalPendingInvoices} type=&quot;pending&quot; /&gt;
      &lt;Card title=&quot;Total Invoices&quot; value={numberOfInvoices} type=&quot;invoices&quot; /&gt;
      &lt;Card
        title=&quot;Total Customers&quot;
        value={numberOfCustomers}
        type=&quot;customers&quot;
      /&gt;
    &lt;/&gt;
  );
}

export function Card({
  title,
  value,
  type,
}: {
  title: string;
  value: number | string;
  type: &quot;invoices&quot; | &quot;customers&quot; | &quot;pending&quot; | &quot;collected&quot;;
}) {
  const Icon = iconMap[type];

  return (
    &lt;div className=&quot;rounded-xl bg-gray-50 p-2 shadow-sm&quot;&gt;
      &lt;div className=&quot;flex p-4&quot;&gt;
        {Icon ? &lt;Icon className=&quot;h-5 w-5 text-gray-700&quot; /&gt; : null}
        &lt;h3 className=&quot;ml-2 text-sm font-medium&quot;&gt;{title}&lt;/h3&gt;
      &lt;/div&gt;
      &lt;p
        className={`${lusitana.className}
          truncate rounded-xl bg-white px-4 py-8 text-center text-2xl`}
      &gt;
        {value}
      &lt;/p&gt;
    &lt;/div&gt;
  );
}</code></pre>
<h2 id="2-what-is-partial-prerendering">2. What is Partial Prerendering?</h2>
<p>공식 문서에서는 동일한 경로에서 정적 렌더링과 동적 렌더링의 장점을 결합할 수 있는 새로운 렌더링 모델로써 Partial Prerendering을 소개하고 있습니다.</p>
<p>이 모델은 빠른 초기 로드를 보장하면서도 개인화되고 자주 업데이트되는 콘텐츠를 제공하여 성능과 사용자 만족도를 모두 향상시킬 수 있습니다.</p>
<h3 id="ppr의-주요-효과">PPR의 주요 효과</h3>
<p>사용자가 경로를 방문할 때 다음과 같은 효과를 제공합니다:</p>
<ul>
<li><strong>빠른 초기 로드</strong>: 사전 렌더링된 정적 셸을 제공함으로써 사용자는 빠른 초기 로드 시간을 경험할 수 있습니다.</li>
<li><strong>향상된 사용자 경험</strong>: 동적 콘텐츠가 비동기적으로 병렬 로드되기 때문에 사용자는 대기 시간 없이 정적 부분과 상호작용할 수 있으며, 동적 데이터는 준비되자마자 표시됩니다.</li>
<li><strong>SEO 최적화</strong>: 페이지의 주요 부분이 사전 렌더링되므로 검색 엔진이 정적 콘텐츠를 효과적으로 인덱싱(indexing)할 수 있어 SEO가 향상됩니다.</li>
</ul>
<h3 id="인덱싱이란">인덱싱이란?</h3>
<p>인덱싱이란 데이터베이스 또는 검색 엔진에서 데이터 검색 속도를 높이기 위해 사용하는 기술을 의미합니다. 데이터베이스나 검색 엔진에서 인덱스는 특정 키를 기준으로 데이터의 위치를 저장한 구조체로, 대량의 데이터를 효율적으로 조회할 수 있도록 돕습니다.</p>
<h2 id="3-how-does-partial-prerendering-work">3. How does Partial Prerendering work?</h2>
<p>Partial Prerendering(부분 사전 렌더링)은 React의 <strong>Suspense</strong>를 활용하여 애플리케이션 일부를 렌더링하는 시점을 데이터 로딩과 같은 조건이 충족될 때까지 지연시키는 방식입니다.</p>
<h3 id="동작-원리">동작 원리</h3>
<ol>
<li><strong>Suspense의 fallback</strong>은 초기 HTML 파일에 정적 콘텐츠와 함께 삽입됩니다.</li>
<li><strong>빌드 시간에</strong> (또는 재검증 중에) 정적 콘텐츠는 Prerendering되고 CDN이나 Edge Network에 캐시됩니다.</li>
<li><strong>동적 콘텐츠의 렌더링</strong>은 사용자가 경로를 요청할 때까지 지연됩니다.</li>
</ol>
<h3 id="중요한-개념-suspense는-경계선-역할">중요한 개념: Suspense는 경계선 역할</h3>
<p>Suspense로 컴포넌트를 감싸는 것은 <strong>컴포넌트 자체를 동적으로 만드는 것이 아닙니다.</strong> 오히려 Suspense는 정적과 동적 코드 사이의 경계로 사용됩니다.</p>
<h4 id="정적과-동적-코드의-차이점">정적과 동적 코드의 차이점</h4>
<ul>
<li><strong>정적 코드</strong>: 렌더링할 때 변하지 않는 부분 (컴포넌트의 구조나 UI 레이아웃)</li>
<li><strong>동적 코드</strong>: 런타임(run-time) 중에 데이터에 따라 변하는 부분 (데이터 fetch)</li>
</ul>
<h4 id="suspense의-역할">Suspense의 역할</h4>
<ul>
<li>Suspense는 React에서 동적으로 데이터를 불러오거나 다른 비동기 작업을 수행할 때 사용됩니다.</li>
<li>Suspense는 이러한 작업이 완료될 때까지 대기하고, 그 사이에 사용자에게 로딩 상태를 보여주는 역할을 합니다. (이를 fallback이라고 합니다)</li>
<li>따라서 Suspense는 애플리케이션에서 정적인 부분과 동적인 부분 사이를 나누는 역할을 하며, 정적인 부분은 바로 렌더링되고, 동적인 부분은 필요할 때까지 지연됩니다.</li>
</ul>
<h2 id="4-partial-prerenderingppr-구현">4. Partial Prerendering(PPR) 구현</h2>
<p>대시보드 경로에 부분적 프리렌더링(PPR)을 구현하는 방법을 살펴보겠습니다.</p>
<h3 id="step-1-ppr-활성화">Step 1: PPR 활성화</h3>
<p>먼저 <code>next.config.js</code>에 다음 코드를 추가하여 PPR을 활성화합니다.</p>
<pre><code class="language-ts">/** @type {import(&#39;next&#39;).NextConfig} */

const nextConfig = {
  experimental: {
    ppr: &quot;incremental&quot;,
  },
};

module.exports = nextConfig;</code></pre>
<blockquote>
<p><code>ppr: &quot;incremental&quot;</code> 옵션을 사용하면 특정 경로에 대해 PPR을 선택적으로 적용할 수 있습니다.</p>
</blockquote>
<h3 id="step-2-레이아웃에서-ppr-설정">Step 2: 레이아웃에서 PPR 설정</h3>
<p>대시보드 레이아웃에서 <code>experimental_ppr</code> segment 구성 옵션을 추가합니다.</p>
<pre><code class="language-tsx">import SideNav from &quot;@/app/ui/dashboard/sidenav&quot;;

export const experimental_ppr = true; // PPR 활성화

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    &lt;div className=&quot;flex h-screen flex-col md:flex-row md:overflow-hidden&quot;&gt;
      &lt;div className=&quot;w-full flex-none md:w-64&quot;&gt;
        &lt;SideNav /&gt;
      &lt;/div&gt;
      &lt;div className=&quot;flex-grow p-6 md:overflow-y-auto md:p-12&quot;&gt;{children}&lt;/div&gt;
    &lt;/div&gt;
  );
}</code></pre>
<h3 id="설정-완료">설정 완료</h3>
<ul>
<li>개발 중에는 애플리케이션에서 큰 차이를 느끼지 못할 수 있지만, <strong>배포 환경(production)에서 성능 향상을 눈에 띄게 느낄 것입니다.</strong></li>
<li>이렇게 하면 <strong>Next.js는 경로의 정적 부분을 미리 렌더링하고, 동적 부분은 사용자가 요청할 때까지 지연시킵니다.</strong></li>
</ul>
<h2 id="ppr의-큰-장점">PPR의 큰 장점</h2>
<p>Partial Prerendering의 큰 장점 중 하나는 <strong>코드를 변경하지 않고도 사용할 수 있다</strong>는 것입니다.</p>
<p>특정 경로의 동적 부분을 Suspense로 감싸주기만 하면, Next.js가 어떤 부분이 정적이고 어떤 부분이 동적인지를 확실히 알 수 있도록 할 수 있습니다.</p>
<h2 id="마무리">마무리</h2>
<p>PPR(Partial Prerendering)은 Next.js에서 제공하는 혁신적인 렌더링 모델로, 정적 렌더링과 동적 렌더링의 장점을 모두 활용할 수 있게 해줍니다. </p>
<p>간단한 설정만으로 성능과 사용자 경험을 크게 향상시킬 수 있으니, 여러분의 Next.js 프로젝트에도 한번 적용해보시기 바랍니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 짧은 간단 지식 정리 - 자바스크립트의 배열]]></title>
            <link>https://velog.io/@lee_1124/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A7%A7%EC%9D%80-%EA%B0%84%EB%8B%A8-%EC%A7%80%EC%8B%9D-%EC%A0%95%EB%A6%AC-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%9D%98-%EB%B0%B0%EC%97%B4</link>
            <guid>https://velog.io/@lee_1124/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A7%A7%EC%9D%80-%EA%B0%84%EB%8B%A8-%EC%A7%80%EC%8B%9D-%EC%A0%95%EB%A6%AC-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%9D%98-%EB%B0%B0%EC%97%B4</guid>
            <pubDate>Wed, 22 Jan 2025 11:13:17 GMT</pubDate>
            <description><![CDATA[<h3 id="1-배열">1. 배열</h3>
<ul>
<li>자료구조로서의 배열은 데이터를 일정한 순서와 구조를 갖춘 집합으로 저장하는 방법을 의미한다.<ul>
<li>배열은 프로그래밍에서 기본적인 자료구조 중 하나로, 메모리 내에서 연속적인 공간에 요소를 저장한다.</li>
<li>배열의 핵심적인 특성과 개념은 다음과 같다.</li>
</ul>
</li>
</ul>
<h4 id="1-연속-메모리-배치">(1) 연속 메모리 배치</h4>
<blockquote>
<p><strong>정적 배열</strong></p>
</blockquote>
<ul>
<li>대부분의 배열은 메모리에서 연속적으로 저장된다.</li>
<li>이는 배열의 인덱스에 직접 접근할 수 있게 해 주며, 상수 시간 O(1) 복잡도로 요소에 접근할 수 있게 한다.</li>
</ul>
<blockquote>
<p><strong>동적 배열</strong></p>
</blockquote>
<ul>
<li>동적 배열은 필요에 따라 크기를 조절할 수 있으며, 초기에는 연속 메모리 공간을 사용하다가 크기가 커지면 재배치할 수 있다.</li>
<li>이 과정은 추가적인 연산이 필요하지만, 배열의 기본 속성은 유지된다.</li>
</ul>
<p>&amp;nbsp</p>
<h4 id="2-인덱스-기반-접근">(2) 인덱스 기반 접근</h4>
<ul>
<li>배열의 요소는 인덱스를 사용하여 접근한다.<ul>
<li>인덱스는 일반적으로 0부터 시작하여 배열의 각 요소에 위치를 제공한다.</li>
<li>예를 들어, 배열의 첫 번째 요소는 인덱스 0, 두 번째 요소는 인덱스 1이다.</li>
</ul>
</li>
</ul>
<p>&amp;nbsp</p>
<h4 id="3-고정-크기와-가변-크기">(3) 고정 크기와 가변 크기</h4>
<blockquote>
<p><strong>고정 크기 배열</strong>:</p>
</blockquote>
<ul>
<li>배열의 크기가 고정되어 있으며, 배열이 생성될 때 크기가 결정된다.</li>
<li>배열의 크기를 변경하려면 새로운 배열을 생성하고 기존의 요소를 복사해야 한다.</li>
</ul>
<blockquote>
<p><strong>가변 크기 배열</strong></p>
</blockquote>
<ul>
<li>배열의 크기를 동적으로 조절할 수 있으며, 필요에 따라 배열의 크기를 증가시키거나 줄일 수 있다. </li>
<li>많은 고급 프로그래밍 언어에서 가변 크기 배열(예: <code>ArrayList</code> in Java, <code>vector</code> in C++)을 제공한다.</li>
</ul>
<p>&amp;nbsp</p>
<h4 id="4-동일한-타입의-요소">(4) 동일한 타입의 요소</h4>
<blockquote>
<p><strong>정적 타입 언어</strong></p>
</blockquote>
<ul>
<li>배열의 요소는 동일한 데이터 타입이어야 한다.</li>
<li>예를 들어, C나 Java에서는 배열의 모든 요소가 같은 타입을 가져야 한다.</li>
</ul>
<blockquote>
<p><strong>동적 타입 언어</strong></p>
</blockquote>
<ul>
<li>배열의 요소는 다양한 타입일 수 있다.</li>
<li>예를 들어, JavaScript에서는 숫자, 문자열, 객체 등 다양한 타입의 요소를 포함할 수 있다.</li>
</ul>
<p>&amp;nbsp</p>
<h4 id="5-자료구조로서의-배열의-주요-속성">(5) 자료구조로서의 배열의 주요 속성</h4>
<blockquote>
<p><strong>인덱스 접근</strong></p>
</blockquote>
<ul>
<li>배열의 요소는 인덱스를 통해 빠르게 접근할 수 있다.</li>
</ul>
<blockquote>
<p><strong>순서</strong></p>
</blockquote>
<ul>
<li>배열은 요소의 순서를 유지하며, 각 요소의 위치가 인덱스로 표현된다.</li>
</ul>
<blockquote>
<p><strong>고정 및 가변 크기</strong></p>
</blockquote>
<ul>
<li>배열의 크기가 고정된 경우와 가변된 경우가 있으며, 데이터의 추가 및 삭제에 따라 배열의 크기를 조정할 수 있는 방식이 다르다.</li>
</ul>
<p>&amp;nbsp </p>
<h4 id="6-응용-및-활용">(6) 응용 및 활용</h4>
<blockquote>
<p><strong>정렬 및 검색</strong></p>
</blockquote>
<ul>
<li>배열은 데이터를 정렬하고 검색하는 알고리즘에서 기본적으로 사용된다.</li>
<li>예를 들어, quick sort(퀵 정렬)나 merge sort(병합 정렬) 알고리즘은 배열을 정렬하는 데 사용됩니다.</li>
</ul>
<blockquote>
<p><strong>스택과 큐</strong></p>
</blockquote>
<ul>
<li>배열을 사용하여 stack(스택)과 queue(큐)와 같은 다른 자료구조를 구현할 수 있다.</li>
</ul>
<blockquote>
<p><strong>다차원 배열</strong></p>
</blockquote>
<ul>
<li>2차원 배열 또는 3차원 배열과 같은 다차원 배열을 사용하여 매트릭스와 같은 복잡한 데이터를 표현할 수 있다.</li>
</ul>
<p>&amp;nbsp</p>
<h3 id="1-밀집-배열">1. 밀집 배열</h3>
<pre><code class="language-js">const denseArray = [1, 2, 3, 4, 5];</code></pre>
<ul>
<li><strong>밀집 배열(dense array)</strong> 은 배열의 모든 인덱스가 사용되며, 인덱스가 연속적이고 배열의 크기와 같은 방식으로 데이터를 저장하는 배열이다.<ul>
<li>즉, 배열의 모든 요소가 초기화되어 있고, 인덱스가 차례로 순서대로 존재한다는 특징이 있다.</li>
</ul>
</li>
</ul>
<ul>
<li>위 예시 코드의 배열은 인덱스 <code>0</code>부터 <code>4</code>까지 모두 값이 할당되어 있으며, 배열의 길이와 같은 방식으로 연속된 인덱스를 가지고 있다.<ul>
<li>이 배열은 모든 요소가 정의되어 있으므로 <strong>밀집 배열</strong>이라 부른다.</li>
</ul>
</li>
</ul>
<pre><code class="language-js">// 밀집 배열에 서로 다른 데이터 타입의 요소가 포함된 경우
const mixedArray = [1, &quot;hello&quot;, { key: &quot;value&quot; }, [1, 2, 3], function() { return &quot;function&quot;; }];</code></pre>
<ul>
<li>여기서 헷갈리지 말아야 할 것이 있는데, 바로 <strong>밀집 배열</strong>이라고, 모든 배열 요소들이 꼭 동일한 타입을 가져야 하는 것은 아니라는 것이다.<ul>
<li>밀집 배열(dense array)에서는 요소의 타입이 동일할 필요는 없다. 배열의 모든 인덱스가 정의되어 있고 연속적으로 값을 가지는 것은 밀집 배열의 특징이지만, 배열의 요소 타입에 대한 제약은 없다.<ul>
<li><strong>동일한 타입의 요소</strong>: 배열의 모든 요소가 동일한 데이터 타입을 가지는 경우가 많지만, JavaScript와 같은 동적 타입 언어에서는 배열의 요소가 다양한 데이터 타입을 가질 수 있다.</li>
<li><strong>다양한 타입의 요소</strong>: JavaScript 배열은 다양한 데이터 타입을 혼합하여 저장할 수 있다. 예를 들어, 숫자, 문자열, 객체, 함수 등 다양한 타입의 요소를 같은 배열에 넣을 수 있다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<ul>
<li>위 예제 코드의 배열 <code>mixedArray</code>는 숫자, 문자열, 객체, 배열, 함수 등 다양한 타입의 요소를 포함하고 있다.<ul>
<li>모든 요소가 정의되어 있으며, 배열의 인덱스가 연속적이기 때문에 밀집 배열로 간주된다.</li>
</ul>
</li>
</ul>
<p>&amp;nbsp</p>
<h3 id="3-희소-배열">3. 희소 배열</h3>
<pre><code class="language-js">const sparseArray = []; 
sparseArray[2] = &#39;a&#39;; 
sparseArray[5] = &#39;b&#39;;</code></pre>
<ul>
<li>하지만 자바스크립트의 배열은, 배열의 요소를 넣기 위한 각각의 메모리 공간이 동일한 크기를 갖지 않아도 되며, 또한 연속적으로 이어져 있지 않을 수도 있다.</li>
</ul>
<ul>
<li>희소 배열(sparse array)은 배열의 인덱스 중 일부만이 정의되어 있고, 나머지 인덱스는 정의되어 있지 않거나 비어 있는 배열을 의미한다.<ul>
<li>즉, 배열의 대부분의 인덱스가 값을 가지지 않으며, 배열의 크기보다 적은 수의 요소만 저장될 수 있다는 뜻이다.</li>
</ul>
</li>
</ul>
<ul>
<li>위 예시코드의 배열에서는 인덱스 <code>2</code>와 <code>5</code>에만 값이 있으며, 나머지 인덱스는 정의되지 않았다.<ul>
<li>배열의 길이는 <code>6</code>이지만, 실제로는 <code>2</code>와 <code>5</code>에만 값이 있다.</li>
</ul>
</li>
</ul>
<p>&amp;nbsp</p>
<h3 id="4-밀집-배열과-희소-배열의-차이">4. 밀집 배열과 희소 배열의 차이</h3>
<blockquote>
<p><strong>※ 메모리 사용</strong></p>
</blockquote>
<ul>
<li><strong>밀집 배열</strong>은 모든 인덱스가 채워져 있기 때문에 메모리를 연속적으로 사용한다.</li>
<li><strong>희소 배열</strong>은 메모리를 비효율적으로 사용할 수 있으며, 배열의 많은 부분이 비어있거나 정의되지 않는다. <ul>
<li>희소 배열에서는 데이터가 저장된 인덱스만 메모리에 할당된다.</li>
</ul>
</li>
</ul>
<blockquote>
<p><strong>※ 성능</strong></p>
</blockquote>
<ul>
<li><strong>밀집 배열</strong>은 인덱스가 연속적이므로 배열의 요소에 접근하는 속도가 빠르다.</li>
<li><strong>희소 배열</strong>은 비어있는 인덱스가 많기 때문에 특정 인덱스에 접근하는 데 시간이 더 걸릴 수 있다.</li>
</ul>
<blockquote>
<p><strong>※ 장점과 단점</strong></p>
</blockquote>
<ul>
<li><strong>밀집 배열</strong>은 배열의 모든 인덱스에 값이 있어 접근이 간단하지만, 불필요하게 큰 배열을 만들 수 있다.</li>
<li><strong>희소 배열</strong>은 메모리를 절약할 수 있지만, 배열의 요소에 접근할 때 추가적인 계산이 필요할 수 있다.</li>
</ul>
<p>&amp;nbsp</p>
<h3 id="5-자바스크립트에서-희소-배열의-동작">5. 자바스크립트에서 희소 배열의 동작</h3>
<ul>
<li>JavaScript에서, 희소 배열은 <strong>배열의 특정 인덱스만 정의된 경우</strong> 발생한다.<ul>
<li>배열의 크기(<code>length</code> 속성)는 정의된 최대 인덱스보다 클 수 있으며, 정의되지 않은 인덱스는 <code>undefined</code>로 간주된다.</li>
<li>희소 배열에서 정의된 인덱스만 실제로 값이 저장된다.</li>
</ul>
</li>
</ul>
<pre><code class="language-js">const sparseArray = [];
sparseArray[100] = &#39;a&#39;;

console.log(sparseArray.length); // 101
console.log(sparseArray[0]); // undefined
console.log(sparseArray[100]); // &#39;a&#39;</code></pre>
<ul>
<li>여기서 <code>sparseArray</code>의 길이는 <code>101</code>로 설정되어 있지만, 실제로는 인덱스 <code>100</code>에만 값이 있으며 나머지는 정의되지 않은 상태이다.</li>
</ul>
<p>&amp;nbsp</p>
<h3 id="6-요약">6. 요약</h3>
<ul>
<li><strong>밀집 배열</strong>: 모든 인덱스가 사용되고, 연속적인 값을 가지는 배열.</li>
<li><strong>희소 배열</strong>: 일부 인덱스만 사용되며, 많은 인덱스가 정의되지 않은 배열.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 짧은 간단 지식 정리 - 코드 스플리팅]]></title>
            <link>https://velog.io/@lee_1124/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A7%A7%EC%9D%80-%EA%B0%84%EB%8B%A8-%EC%A7%80%EC%8B%9D-%EC%A0%95%EB%A6%AC-%EC%BD%94%EB%93%9C-%EC%8A%A4%ED%94%8C%EB%A6%AC%ED%8C%85</link>
            <guid>https://velog.io/@lee_1124/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A7%A7%EC%9D%80-%EA%B0%84%EB%8B%A8-%EC%A7%80%EC%8B%9D-%EC%A0%95%EB%A6%AC-%EC%BD%94%EB%93%9C-%EC%8A%A4%ED%94%8C%EB%A6%AC%ED%8C%85</guid>
            <pubDate>Thu, 02 Jan 2025 08:25:00 GMT</pubDate>
            <description><![CDATA[<h3 id="1--코드-스플리팅">1.  코드 스플리팅</h3>
<ul>
<li>코드 스플리팅(Code Splitting)은 웹 애플리케이션에서 코드를 작은 청크(chunk)로 분리하여 애플리케이션의 초기 로딩 속도를 개선하는 기술이다. </li>
<li>이 기술은 번들 파일을 여러 개로 나누고, <strong>사용자가 필요로 하는 부분만 로드함으로써 네트워크 비용과 사용자 대기 시간을 줄이는 데 효과적</strong>이다.</li>
</ul>
<hr>
<h3 id="2-코드-스플리팅의-필요성">2. 코드 스플리팅의 필요성</h3>
<ul>
<li>SPA(Single Page Application)와 같은 현대 웹 애플리케이션은 모든 코드를 하나의 번들로 묶으면 초기 로딩 시간이 길어지게 되는 문제가 발생할 수 있다.<ul>
<li>아래는 모든 코드를 하나의 번들로 묶었을 때 발생할 수 있는 문제와 해결방안이다.</li>
</ul>
</li>
</ul>
<ul>
<li><strong>문제점</strong><ul>
<li>모든 코드가 한 번에 로드되면 대규모 애플리케이션의 경우 초기 로딩이 느려짐</li>
<li>사용자가 필요하지 않은 기능이나 페이지의 코드도 미리 로드됨</li>
</ul>
</li>
</ul>
<ul>
<li><strong>해결책</strong><ul>
<li>코드 스플리팅을 통해 필요한 시점에 필요한 코드만 로드하여 <strong>최소한의 리소스</strong>로 동작할 수 있도록 한다.</li>
</ul>
</li>
</ul>
<hr>
<h3 id="3-코드-스플리팅의-장점">3. 코드 스플리팅의 장점</h3>
<ul>
<li><strong>초기 로딩 속도 개선</strong><ul>
<li>앱에서 반드시 필요한 코드만 먼저 로드하고, 나머지는 나중에 로드</li>
</ul>
</li>
</ul>
<ul>
<li><strong>사용자 경험 향상</strong><ul>
<li>사용자가 더 빠르게 페이지와 상호작용할 수 있도록 지원</li>
</ul>
</li>
</ul>
<ul>
<li><strong>효율적인 네트워크 사용</strong><ul>
<li>페이지별로 코드를 분리해 불필요한 리소스를 로드하지 않음</li>
</ul>
</li>
</ul>
<ul>
<li><strong>확장성</strong><ul>
<li>기능별 모듈화로 코드 관리 및 유지보수가 쉬워짐</li>
</ul>
</li>
</ul>
<hr>
<h3 id="4-코드-스플리팅-구현방법">4. 코드 스플리팅 구현방법</h3>
<h4 id="4-1-webpack-기반-코드-스플리팅">4-1. Webpack 기반 코드 스플리팅</h4>
<ol>
<li><p>Dynamic Import를 사용한 코드 스플리팅</p>
<ul>
<li>Webpack의 <code>import()</code> 구문을 활용해 동적으로 모듈을 로드한다.<pre><code class="language-js">function loadComponent() {
import(&#39;./MyComponent&#39;).then((module) =&gt; {
const MyComponent = module.default;
// 모듈 사용
});
}</code></pre>
</li>
</ul>
</li>
<li><p>Webpack 설정으로 자동 코드 스플리팅</p>
<ul>
<li>Webpack 설정에서 <code>splitChunks</code> 옵션을 사용하면 청크 파일을 자동으로 생성할 수 있다.<pre><code class="language-js">module.exports = {
optimization: {
splitChunks: {
chunks: &#39;all&#39;, // 모든 청크에 대해 스플리팅 수행
},
},
};</code></pre>
</li>
</ul>
</li>
</ol>
<h4 id="4-2-react에서의-코드-스플리팅">4-2. React에서의 코드 스플리팅</h4>
<ol>
<li>React.lazy와 Suspense 사용<ul>
<li>React는 <code>React.lazy</code>와 <code>Suspense</code>를 통해 컴포넌트를 동적 로드하고, 로드 중 상태를 표시할 수 있다.<pre><code class="language-jsx">import React, { Suspense } from &#39;react&#39;;
</code></pre>
</li>
</ul>
</li>
</ol>
<p>// React.lazy : 동적으로 컴포넌트 로드
const MyComponent = React.lazy(() =&gt; import(&#39;./MyComponent&#39;));</p>
<p>function App() {
  return (
    {/* Suspense : 로딩 중일 때 fallback 속성으로 전달한 element를 표시한다. */}
    &lt;Suspense fallback={<div>Loading...</div>}&gt;
      <MyComponent />
    </Suspense>
  );
}</p>
<pre><code>
2. React Router와의 코드 스플리팅
    - React Router와 결합하여 페이지 단위로 코드를 스플리팅할 수 있다.
```jsx
import React, { Suspense } from &#39;react&#39;;
import { BrowserRouter as Router, Route, Switch } from &#39;react-router-dom&#39;;

const Home = React.lazy(() =&gt; import(&#39;./Home&#39;));
const About = React.lazy(() =&gt; import(&#39;./About&#39;));

function App() {
  return (
    &lt;Router&gt;
      &lt;Suspense fallback={&lt;div&gt;Loading...&lt;/div&gt;}&gt;
        &lt;Switch&gt;
          &lt;Route path=&quot;/&quot; exact component={Home} /&gt;
          &lt;Route path=&quot;/about&quot; component={About} /&gt;
        &lt;/Switch&gt;
      &lt;/Suspense&gt;
    &lt;/Router&gt;
  );
}</code></pre><h4 id="4-3-nextjs에서의-코드-스플리팅">4-3. Next.js에서의 코드 스플리팅</h4>
<ol>
<li><code>dynamic</code> 함수<ul>
<li>Next.js는 기본적으로 코드 스플리팅을 지원하며, <code>dynamic</code> 함수를 통해 추가 제어가 가능하다.</li>
<li>Next.js는 서버 사이드 렌더링(SSR)과 클라이언트 사이드 렌더링(CSR) 모두에서 코드 스플리팅을 지원한다.</li>
</ul>
</li>
</ol>
<pre><code class="language-jsx">import dynamic from &#39;next/dynamic&#39;;

// 동적 로드 설정
const DynamicComponent = dynamic(() =&gt; import(&#39;./MyComponent&#39;), {
  loading: () =&gt; &lt;p&gt;Loading...&lt;/p&gt;, // 로딩 중 표시할 컴포넌트
});

function App() {
  return &lt;DynamicComponent /&gt;;
}</code></pre>
<p> 실제 필자가 작성해본 <code>dynamic</code> 함수를 사용한 스플리팅은 아래와 같다.</p>
<pre><code class="language-tsx">&quot;use client&quot;;

import dynamic from &quot;next/dynamic&quot;;
import styles from &quot;./DashboardClient.module.scss&quot;;
import { User } from &quot;next-auth&quot;;
import Loading from &quot;@/components/common/Loading/Loading&quot;;

interface IProps {
  user: User;
}
const MonthlyEmissionsChart = dynamic(() =&gt; import(&quot;./MonthlyEmissionsChart&quot;), {
  ssr: false,
  loading: () =&gt; (
    &lt;&gt;
      &lt;Loading text=&quot;탄소배출량 현황 정보를 불러오고 있습니다.&quot; open={true} /&gt;
      &lt;div className={styles.w100_box}&gt;
        &lt;div className={styles.w100}&gt;
          &lt;div className={styles.tit}&gt;
            &lt;p&gt;
              탄소배출량 현황 &lt;span&gt;&amp;#40;단위 : CO2eq&amp;#41;&lt;/span&gt;
            &lt;/p&gt;
          &lt;/div&gt;
          &lt;div className={styles.chart_box}&gt;
            차트 데이터를 불러오고 있습니다...
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/&gt;
  ),
});
const ScopeEmissionsChart = dynamic(() =&gt; import(&quot;./ScopeEmissionsChart&quot;), {
  ssr: false,
  loading: () =&gt; (
    &lt;&gt;
      &lt;Loading
        text=&quot;Scope별 탄소배출량 현황 정보를 불러오고 있습니다.&quot;
        open={true}
      /&gt;
      &lt;div className={styles.w30}&gt;
        &lt;div className={styles.tit}&gt;
          &lt;p&gt;
            Scope별 탄소배출량 현황 &lt;span&gt;&amp;#40;단위 : CO2eq&amp;#41;&lt;/span&gt;
          &lt;/p&gt;
        &lt;/div&gt;
        &lt;div className={styles.chart_box}&gt;
          차트 데이터를 불러오고 있습니다...
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/&gt;
  ),
});
const ProcessEmissionsChart = dynamic(() =&gt; import(&quot;./ProcessEmissionsChart&quot;), {
  ssr: false,
  loading: () =&gt; (
    &lt;&gt;
      &lt;Loading
        text=&quot;공정별 탄소배출량 현황 정보를 불러오고 있습니다.&quot;
        open={true}
      /&gt;
      &lt;div className={styles.w30}&gt;
        &lt;div className={styles.tit}&gt;
          &lt;p&gt;
            공정별 탄소배출량 현황 &lt;span&gt;&amp;#40;단위 : CO2eq&amp;#41;&lt;/span&gt;
          &lt;/p&gt;
        &lt;/div&gt;
        &lt;div className={styles.chart_box}&gt;
          차트 데이터를 불러오고 있습니다...
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/&gt;
  ),
});
const SourceEmissionsChart = dynamic(() =&gt; import(&quot;./SourceEmissionsChart&quot;), {
  ssr: false,
  loading: () =&gt; (
    &lt;&gt;
      &lt;Loading
        text=&quot;배출원별 탄소배출량 현황 정보를 불러오고 있습니다.&quot;
        open={true}
      /&gt;
      &lt;div className={styles.w30}&gt;
        &lt;div className={styles.tit}&gt;
          &lt;p&gt;
            배출원별 탄소배출량 현황 &lt;span&gt;&amp;#40;단위 : CO2eq&amp;#41;&lt;/span&gt;
          &lt;/p&gt;
        &lt;/div&gt;
        &lt;div className={styles.chart_box}&gt;
          차트 데이터를 불러오고 있습니다...
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/&gt;
  ),
});
export default function DashboardClient({ user }: IProps) {
  return (
    &lt;div className={styles.wrap}&gt;
      {/* 탄소배출량 현황 */}
      &lt;MonthlyEmissionsChart user={user} /&gt;

      &lt;div className={styles.w100_box}&gt;
        {/* Scope별 탄소배출량 현황 */}
        &lt;ScopeEmissionsChart user={user} /&gt;

        {/* 공정별 탄소배출량 현황 */}
        &lt;ProcessEmissionsChart user={user} /&gt;

        {/* 배출원별 탄소배출량 현황 */}
        &lt;SourceEmissionsChart user={user} /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}</code></pre>
<hr>
<h3 id="5-코드-스플리팅을-적용-가능한-사례">5. 코드 스플리팅을 적용 가능한 사례</h3>
<ol>
<li><strong>대규모 애플리케이션</strong>: 여러 페이지나 기능별로 분리된 번들을 로드.</li>
<li><strong>SPA 환경</strong>: 라우트 단위로 스플리팅하여 초기 로드 시간 최소화.</li>
<li><strong>모바일 앱</strong>: 느린 네트워크 환경에서 효율적 로딩을 위한 최적화.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 짧은 간단 지식 정리 - 실행 컨텍스트]]></title>
            <link>https://velog.io/@lee_1124/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A7%A7%EC%9D%80-%EA%B0%84%EB%8B%A8-%EC%A7%80%EC%8B%9D-%EC%A0%95%EB%A6%AC-%EC%8B%A4%ED%96%89-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@lee_1124/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A7%A7%EC%9D%80-%EA%B0%84%EB%8B%A8-%EC%A7%80%EC%8B%9D-%EC%A0%95%EB%A6%AC-%EC%8B%A4%ED%96%89-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Mon, 30 Dec 2024 00:43:19 GMT</pubDate>
            <description><![CDATA[<h2 id="1-실행-컨텍스트란-🎯">1. 실행 컨텍스트란? 🎯</h2>
<p>JavaScript에서 <strong>코드가 실행되는 환경</strong>을 정의하는 핵심 개념입니다.</p>
<h3 id="🔍-실행-컨텍스트의-정의">🔍 실행 컨텍스트의 정의</h3>
<p><strong>실행 컨텍스트(Execution Context)</strong>는 자바스크립트가 코드를 실행할 때 필요한 모든 정보를 담고 있는 박스라고 생각하면 됩니다.</p>
<ul>
<li>동일한 환경에 있는 <strong>코드들을 실행할 때 필요한 환경 정보</strong>들을 모아 구성된 객체</li>
<li>코드가 실행될 때 필요한 <strong>변수, 객체, 함수</strong> 등을 정의</li>
<li><strong>콜 스택</strong>에 쌓아올려지며, 가장 위에 있는 컨텍스트의 코드가 실행됨</li>
</ul>
<h3 id="💡-왜-중요한가">💡 왜 중요한가?</h3>
<p>실행 컨텍스트를 이해하면 다음을 알 수 있습니다:</p>
<ul>
<li><strong>스코프(Scope)</strong>: 변수의 유효 범위</li>
<li><strong>호이스팅(Hoisting)</strong>: 변수와 함수 선언의 끌어올림</li>
<li><strong>클로저(Closure)</strong>: 함수와 렉시컬 환경의 조합</li>
<li><strong>this 바인딩</strong>: this 키워드의 동작 원리</li>
</ul>
<blockquote>
<p>💡 <strong>자바스크립트의 동적 언어 특성</strong><br>실행 컨텍스트는 자바스크립트의 동적 언어로서의 성격을 가장 잘 보여주는 개념입니다!</p>
</blockquote>
<hr>
<h2 id="2-실행-컨텍스트의-생성과-실행-🔄">2. 실행 컨텍스트의 생성과 실행 🔄</h2>
<h3 id="📋-실행-컨텍스트의-단계">📋 실행 컨텍스트의 단계</h3>
<pre><code>1️⃣ 생성 단계 (Creation Phase)
   - 변수 환경 생성 (호이스팅 발생)
   - 렉시컬 환경 생성
   - this 바인딩 결정

2️⃣ 실행 단계 (Execution Phase)
   - 코드 실행
   - 변수 할당
   - 함수 호출</code></pre><h3 id="🎯-생성-단계-상세">🎯 생성 단계 상세</h3>
<h4 id="1️⃣-변수-환경-생성">1️⃣ 변수 환경 생성</h4>
<pre><code class="language-javascript">// 코드 작성
console.log(name); // undefined (호이스팅!)
var name = &quot;Alice&quot;;
console.log(name); // &quot;Alice&quot;</code></pre>
<p><strong>생성 단계에서 일어나는 일:</strong></p>
<ul>
<li><code>var</code> 변수는 <code>undefined</code>로 초기화</li>
<li>함수 선언문은 전체가 메모리에 저장</li>
</ul>
<h4 id="2️⃣-렉시컬-환경-생성">2️⃣ 렉시컬 환경 생성</h4>
<pre><code class="language-javascript">function outer() {
    let x = 10; // 렉시컬 환경에 저장
    function inner() {
        console.log(x); // 외부 환경 참조로 접근
    }
    inner();
}
outer(); // 10</code></pre>
<p><strong>렉시컬 환경의 구성:</strong></p>
<ul>
<li><strong>환경 레코드</strong>: 변수와 함수 저장</li>
<li><strong>외부 환경 참조</strong>: 상위 스코프 연결</li>
</ul>
<h4 id="3️⃣-this-바인딩">3️⃣ this 바인딩</h4>
<pre><code class="language-javascript">const obj = {
    name: &quot;JavaScript&quot;,
    greet() {
        console.log(this.name);
    }
};
obj.greet(); // &quot;JavaScript&quot;</code></pre>
<hr>
<h2 id="3-실행-컨텍스트의-종류-📦">3. 실행 컨텍스트의 종류 📦</h2>
<h3 id="🌐-전역-실행-컨텍스트">🌐 전역 실행 컨텍스트</h3>
<p>프로그램이 시작될 때 생성되는 기본 컨텍스트입니다.</p>
<pre><code class="language-javascript">var globalVar = &quot;전역 변수&quot;;

function globalFunction() {
    console.log(&quot;전역 함수! 🌍&quot;);
}

// 전역 컨텍스트에서 실행
globalFunction(); // &quot;전역 함수! 🌍&quot;</code></pre>
<p><strong>특징:</strong></p>
<ul>
<li>프로그램 실행 시 <strong>가장 먼저 생성</strong></li>
<li><strong>단 하나만 존재</strong></li>
<li>브라우저에서는 <code>window</code>, Node.js에서는 <code>global</code> 객체와 연결</li>
<li>프로그램 종료까지 유지</li>
</ul>
<pre><code class="language-javascript">// 브라우저 환경
var a = 5;
console.log(window.a); // 5
window.globalFunction(); // &quot;전역 함수! 🌍&quot;</code></pre>
<h3 id="🔧-함수-실행-컨텍스트">🔧 함수 실행 컨텍스트</h3>
<p>함수가 <strong>호출될 때마다</strong> 생성됩니다.</p>
<pre><code class="language-javascript">function first() {
    console.log(&quot;First! 1️⃣&quot;);
    second();
}

function second() {
    console.log(&quot;Second! 2️⃣&quot;);
    third();
}

function third() {
    console.log(&quot;Third! 3️⃣&quot;);
}

first();
/* 출력:
First! 1️⃣
Second! 2️⃣
Third! 3️⃣
*/</code></pre>
<p><strong>특징:</strong></p>
<ul>
<li>함수 <strong>호출 시마다</strong> 생성</li>
<li>함수마다 <strong>고유한 컨텍스트</strong> 보유</li>
<li>실행 종료 후 콜 스택에서 제거</li>
</ul>
<hr>
<h2 id="4-실행-컨텍스트의-구성-요소-🧩">4. 실행 컨텍스트의 구성 요소 🧩</h2>
<h3 id="📊-구성-요소-개요">📊 구성 요소 개요</h3>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/3c7bbda9-ad78-43e6-bb1b-f3a3a8c674e8/image.png" alt=""></p>
<pre><code>실행 컨텍스트
├── Lexical Environment (렉시컬 환경)
│   ├── Environment Record (환경 레코드)
│   └── Outer Environment Reference (외부 환경 참조)
│
├── Variable Environment (변수 환경)
│   └── Environment Record (환경 레코드)
│
└── This Binding (this 바인딩)</code></pre><h3 id="1️⃣-lexical-environment-렉시컬-환경">1️⃣ Lexical Environment (렉시컬 환경)</h3>
<p><strong>정의:</strong> 코드의 스코프와 관련된 정보를 관리하는 구조</p>
<pre><code class="language-javascript">function outer() {
    let x = 10; // outer의 환경 레코드

    function inner() {
        let y = 20; // inner의 환경 레코드
        console.log(x + y); // 외부 환경 참조로 x 접근
    }

    inner(); // 30
}
outer();</code></pre>
<p><strong>구성 요소:</strong></p>
<h4 id="📝-환경-레코드-environment-record">📝 환경 레코드 (Environment Record)</h4>
<p>현재 스코프의 변수와 함수를 저장합니다.</p>
<pre><code class="language-javascript">function example() {
    let a = 1;
    const b = 2;
    function helper() {}

    // 환경 레코드: { a: 1, b: 2, helper: function }
}</code></pre>
<h4 id="🔗-외부-환경-참조-outer-environment-reference">🔗 외부 환경 참조 (Outer Environment Reference)</h4>
<p>상위 스코프를 참조하여 <strong>스코프 체인</strong>을 만듭니다.</p>
<pre><code class="language-javascript">let global = &quot;전역&quot;;

function level1() {
    let x = 10;

    function level2() {
        let y = 20;

        function level3() {
            let z = 30;
            // 스코프 체인: level3 → level2 → level1 → global
            console.log(global + x + y + z);
        }
        level3();
    }
    level2();
}
level1(); // &quot;전역102030&quot;</code></pre>
<h3 id="2️⃣-variable-environment-변수-환경">2️⃣ Variable Environment (변수 환경)</h3>
<p><strong>정의:</strong> <code>var</code>로 선언된 변수와 함수 선언을 관리</p>
<pre><code class="language-javascript">function test() {
    console.log(x); // undefined (호이스팅)
    var x = 10;
    console.log(x); // 10
}
test();</code></pre>
<p><strong>변수 환경 vs 렉시컬 환경:</strong></p>
<table>
<thead>
<tr>
<th>특징</th>
<th>Variable Environment</th>
<th>Lexical Environment</th>
</tr>
</thead>
<tbody><tr>
<td><strong>관리 대상</strong></td>
<td><code>var</code>, 함수 선언</td>
<td><code>let</code>, <code>const</code>, 함수</td>
</tr>
<tr>
<td><strong>외부 참조</strong></td>
<td>없음</td>
<td>있음 (스코프 체인)</td>
</tr>
<tr>
<td><strong>값 추적</strong></td>
<td>초기값만 기억</td>
<td>실행 중 값 변경 반영</td>
</tr>
</tbody></table>
<pre><code class="language-javascript">function outer() {
    var x = 10; // 변수 환경에 저장

    function inner() {
        console.log(x); // 렉시컬 환경의 외부 참조로 접근
    }

    x = 20; // 값 변경
    inner(); // 20 (렉시컬 환경은 변경된 값 참조)
}
outer();</code></pre>
<h3 id="3️⃣-this-binding">3️⃣ This Binding</h3>
<p><strong>정의:</strong> 현재 실행 컨텍스트의 <code>this</code> 값 결정</p>
<pre><code class="language-javascript">// 1. 전역 컨텍스트
console.log(this); // window (브라우저)

// 2. 함수 호출
function showThis() {
    console.log(this);
}
showThis(); // window (또는 undefined in strict mode)

// 3. 메서드 호출
const obj = {
    name: &quot;객체&quot;,
    method() {
        console.log(this.name);
    }
};
obj.method(); // &quot;객체&quot;

// 4. 생성자 함수
function Person(name) {
    this.name = name;
}
const person = new Person(&quot;Alice&quot;);
console.log(person.name); // &quot;Alice&quot;

// 5. 화살표 함수
const obj2 = {
    name: &quot;화살표&quot;,
    method: () =&gt; {
        console.log(this.name);
    }
};
obj2.method(); // undefined (상위 스코프의 this)</code></pre>
<hr>
<h2 id="5-콜-스택과-실행-컨텍스트-📚">5. 콜 스택과 실행 컨텍스트 📚</h2>
<h3 id="🔄-콜-스택의-동작">🔄 콜 스택의 동작</h3>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/506c120c-6cd0-434c-8a41-7551a222a3e3/image.png" alt=""></p>
<pre><code class="language-javascript">var x = &#39;xxx&#39;;

function foo() {
    var y = &#39;yyy&#39;;

    function bar() {
        var z = &#39;zzz&#39;;
        console.log(x + y + z);
    }

    bar();
}

foo(); // &quot;xxxyyyzzz&quot;</code></pre>
<p><strong>실행 순서:</strong></p>
<pre><code>1️⃣ 전역 실행 컨텍스트 생성
   └── 콜 스택: [전역]

2️⃣ foo() 호출
   └── 콜 스택: [전역, foo]

3️⃣ bar() 호출
   └── 콜 스택: [전역, foo, bar]

4️⃣ bar() 종료
   └── 콜 스택: [전역, foo]

5️⃣ foo() 종료
   └── 콜 스택: [전역]</code></pre><h3 id="💡-상세-예시">💡 상세 예시</h3>
<pre><code class="language-javascript">function first() {
    console.log(&quot;1️⃣ First function start&quot;);
    second();
    console.log(&quot;1️⃣ First function end&quot;);
}

function second() {
    console.log(&quot;  2️⃣ Second function start&quot;);
    third();
    console.log(&quot;  2️⃣ Second function end&quot;);
}

function third() {
    console.log(&quot;    3️⃣ Third function&quot;);
}

console.log(&quot;🌐 Global start&quot;);
first();
console.log(&quot;🌐 Global end&quot;);

/* 출력:
🌐 Global start
1️⃣ First function start
  2️⃣ Second function start
    3️⃣ Third function
  2️⃣ Second function end
1️⃣ First function end
🌐 Global end
*/</code></pre>
<hr>
<h2 id="6-스코프-체인-🔗">6. 스코프 체인 🔗</h2>
<h3 id="🎯-스코프-체인이란">🎯 스코프 체인이란?</h3>
<p>변수를 찾을 때 현재 스코프에서 시작하여 상위 스코프로 이어지는 연결 고리입니다.</p>
<pre><code class="language-javascript">let level1 = &quot;L1&quot;;

function outer() {
    let level2 = &quot;L2&quot;;

    function middle() {
        let level3 = &quot;L3&quot;;

        function inner() {
            let level4 = &quot;L4&quot;;

            // 스코프 체인을 통한 변수 검색
            console.log(level4); // 현재 스코프
            console.log(level3); // 상위 스코프 (middle)
            console.log(level2); // 상위 스코프 (outer)
            console.log(level1); // 전역 스코프
        }

        inner();
    }

    middle();
}

outer();
/* 출력:
L4
L3
L2
L1
*/</code></pre>
<h3 id="📊-스코프-체인-시각화">📊 스코프 체인 시각화</h3>
<pre><code class="language-javascript">let x = 10;

function foo() {
    let y = 20;

    function bar() {
        let z = 30;
        console.log(x + y + z); // 10 + 20 + 30
    }

    bar();
}

foo(); // 60</code></pre>
<p><strong>스코프 체인 구조:</strong></p>
<pre><code>bar의 렉시컬 환경
├── 환경 레코드: { z: 30 }
└── 외부 환경 참조 → foo의 렉시컬 환경
                      ├── 환경 레코드: { y: 20 }
                      └── 외부 환경 참조 → 전역 환경
                                           └── 환경 레코드: { x: 10 }</code></pre><h3 id="⚠️-스코프-체인의-한계">⚠️ 스코프 체인의 한계</h3>
<pre><code class="language-javascript">function outer() {
    let x = 10;
}

function other() {
    console.log(x); // ReferenceError: x is not defined
}

outer();
other();</code></pre>
<p><strong>스코프 체인은 선언된 위치(렉시컬)를 기준으로 형성됩니다!</strong></p>
<hr>
<h2 id="7-클로저와-실행-컨텍스트-🔐">7. 클로저와 실행 컨텍스트 🔐</h2>
<h3 id="🎯-클로저란">🎯 클로저란?</h3>
<p><strong>클로저(Closure)</strong>: 반환된 내부 함수와 내부 함수가 선언되었던 렉시컬 환경의 조합</p>
<pre><code class="language-javascript">function makeCounter() {
    let count = 0; // 외부 함수의 변수

    return function() {
        count++; // 클로저: 외부 함수 종료 후에도 접근 가능
        return count;
    };
}

const counter1 = makeCounter();
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter1()); // 3

const counter2 = makeCounter();
console.log(counter2()); // 1 (독립적인 클로저)</code></pre>
<h3 id="💡-클로저의-동작-원리">💡 클로저의 동작 원리</h3>
<pre><code class="language-javascript">function outer() {
    let privateVar = &quot;비밀 변수 🔒&quot;;

    return {
        getPrivate() {
            return privateVar;
        },
        setPrivate(value) {
            privateVar = value;
        }
    };
}

const instance1 = outer();
console.log(instance1.getPrivate()); // &quot;비밀 변수 🔒&quot;

instance1.setPrivate(&quot;변경된 값 ✨&quot;);
console.log(instance1.getPrivate()); // &quot;변경된 값 ✨&quot;

const instance2 = outer();
console.log(instance2.getPrivate()); // &quot;비밀 변수 🔒&quot; (독립적)</code></pre>
<h3 id="🌟-실용적인-클로저-예시">🌟 실용적인 클로저 예시</h3>
<pre><code class="language-javascript">// 1. 데이터 은닉
function createBankAccount(initialBalance) {
    let balance = initialBalance;

    return {
        deposit(amount) {
            balance += amount;
            return `입금 완료! 잔액: ${balance}원 💰`;
        },
        withdraw(amount) {
            if (balance &gt;= amount) {
                balance -= amount;
                return `출금 완료! 잔액: ${balance}원 💸`;
            }
            return &quot;잔액 부족! ⚠️&quot;;
        },
        getBalance() {
            return `현재 잔액: ${balance}원 💵`;
        }
    };
}

const myAccount = createBankAccount(10000);
console.log(myAccount.deposit(5000));   // &quot;입금 완료! 잔액: 15000원 💰&quot;
console.log(myAccount.withdraw(3000));  // &quot;출금 완료! 잔액: 12000원 💸&quot;
console.log(myAccount.getBalance());    // &quot;현재 잔액: 12000원 💵&quot;
// console.log(balance); // ReferenceError (외부에서 접근 불가)</code></pre>
<pre><code class="language-javascript">// 2. 함수 팩토리
function createMultiplier(multiplier) {
    return function(num) {
        return num * multiplier;
    };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);
const quadruple = createMultiplier(4);

console.log(double(5));     // 10
console.log(triple(5));     // 15
console.log(quadruple(5));  // 20</code></pre>
<hr>
<h2 id="8-this-바인딩-상세-가이드-🎭">8. this 바인딩 상세 가이드 🎭</h2>
<h3 id="📌-바인딩이란">📌 바인딩이란?</h3>
<p><strong>바인딩</strong>: 식별자와 값을 연결하는 과정</p>
<h3 id="🎯-this-바인딩-규칙">🎯 this 바인딩 규칙</h3>
<h4 id="1️⃣-전역-컨텍스트">1️⃣ 전역 컨텍스트</h4>
<pre><code class="language-javascript">console.log(this); // window (브라우저) / global (Node.js)

function showGlobalThis() {
    console.log(this);
}
showGlobalThis(); // window (non-strict) / undefined (strict)</code></pre>
<h4 id="2️⃣-암시적-바인딩-메서드-호출">2️⃣ 암시적 바인딩 (메서드 호출)</h4>
<pre><code class="language-javascript">const person = {
    name: &quot;Alice&quot;,
    greet() {
        console.log(`안녕하세요, ${this.name}입니다! 👋`);
    }
};

person.greet(); // &quot;안녕하세요, Alice입니다! 👋&quot;

// ⚠️ 주의: 메서드를 변수에 할당하면?
const greetFunc = person.greet;
greetFunc(); // &quot;안녕하세요, undefined입니다! 👋&quot; (this가 전역 객체)</code></pre>
<h4 id="3️⃣-명시적-바인딩-call-apply-bind">3️⃣ 명시적 바인딩 (call, apply, bind)</h4>
<pre><code class="language-javascript">const person1 = { name: &quot;Alice&quot; };
const person2 = { name: &quot;Bob&quot; };

function introduce(greeting, punctuation) {
    console.log(`${greeting}, 저는 ${this.name}입니다${punctuation}`);
}

// call: 인자를 개별적으로 전달
introduce.call(person1, &quot;안녕하세요&quot;, &quot;!&quot;);
// &quot;안녕하세요, 저는 Alice입니다!&quot;

// apply: 인자를 배열로 전달
introduce.apply(person2, [&quot;Hello&quot;, &quot;.&quot;]);
// &quot;Hello, 저는 Bob입니다.&quot;

// bind: 새로운 함수 생성
const aliceIntroduce = introduce.bind(person1);
aliceIntroduce(&quot;Hi&quot;, &quot;~&quot;);
// &quot;Hi, 저는 Alice입니다~&quot;</code></pre>
<h4 id="4️⃣-new-바인딩-생성자-함수">4️⃣ new 바인딩 (생성자 함수)</h4>
<pre><code class="language-javascript">function User(name, age) {
    this.name = name;
    this.age = age;
    this.greet = function() {
        console.log(`${this.name} (${this.age}세) 👤`);
    };
}

const user1 = new User(&quot;Alice&quot;, 25);
const user2 = new User(&quot;Bob&quot;, 30);

user1.greet(); // &quot;Alice (25세) 👤&quot;
user2.greet(); // &quot;Bob (30세) 👤&quot;</code></pre>
<h4 id="5️⃣-화살표-함수">5️⃣ 화살표 함수</h4>
<pre><code class="language-javascript">// ❌ 일반 함수: this가 호출 방식에 따라 달라짐
const obj1 = {
    name: &quot;일반 함수&quot;,
    regularFunc: function() {
        setTimeout(function() {
            console.log(this.name); // undefined (this가 전역)
        }, 100);
    }
};

// ✅ 화살표 함수: 상위 스코프의 this 사용
const obj2 = {
    name: &quot;화살표 함수&quot;,
    arrowFunc: function() {
        setTimeout(() =&gt; {
            console.log(this.name); // &quot;화살표 함수&quot;
        }, 100);
    }
};

obj1.regularFunc(); // undefined
obj2.arrowFunc();   // &quot;화살표 함수&quot;</code></pre>
<hr>
<h2 id="9-호이스팅-완벽-이해-🎈">9. 호이스팅 완벽 이해 🎈</h2>
<h3 id="🔍-호이스팅이란">🔍 호이스팅이란?</h3>
<p>변수와 함수 선언이 코드 실행 전에 메모리에 저장되어 마치 코드의 최상단으로 끌어올려진 것처럼 동작하는 현상</p>
<h3 id="📝-var-호이스팅">📝 var 호이스팅</h3>
<pre><code class="language-javascript">console.log(x); // undefined (선언은 호이스팅, 할당은 X)
var x = 10;
console.log(x); // 10

// 위 코드는 실제로 이렇게 동작:
// var x;            // 선언 호이스팅
// console.log(x);   // undefined
// x = 10;           // 할당은 원래 위치
// console.log(x);   // 10</code></pre>
<h3 id="🚫-let-const-호이스팅">🚫 let, const 호이스팅</h3>
<pre><code class="language-javascript">// ❌ TDZ (Temporal Dead Zone) 에러
console.log(y); // ReferenceError: Cannot access &#39;y&#39; before initialization
let y = 20;

console.log(z); // ReferenceError: Cannot access &#39;z&#39; before initialization
const z = 30;</code></pre>
<p><strong>TDZ (일시적 사각지대):</strong></p>
<pre><code class="language-javascript">let x = 10;

function example() {
    // TDZ 시작
    console.log(x); // ReferenceError (외부 x가 아닌 내부 x 참조 시도)
    let x = 20;     // TDZ 종료
    // x 사용 가능
}</code></pre>
<h3 id="🔧-함수-호이스팅">🔧 함수 호이스팅</h3>
<pre><code class="language-javascript">// ✅ 함수 선언문: 전체 호이스팅
hello(); // &quot;Hello! 👋&quot; (호출 가능)

function hello() {
    console.log(&quot;Hello! 👋&quot;);
}

// ❌ 함수 표현식: 변수만 호이스팅
greet(); // TypeError: greet is not a function

var greet = function() {
    console.log(&quot;Greet! 🎉&quot;);
};

// ❌ 화살표 함수: 변수만 호이스팅
wave(); // TypeError: wave is not a function

const wave = () =&gt; {
    console.log(&quot;Wave! 🌊&quot;);
};</code></pre>
<hr>
<h2 id="10-실전-활용-패턴-⚡">10. 실전 활용 패턴 ⚡</h2>
<h3 id="🎯-즉시-실행-함수-iife">🎯 즉시 실행 함수 (IIFE)</h3>
<pre><code class="language-javascript">// 전역 스코프 오염 방지
(function() {
    var private = &quot;비공개 변수&quot;;
    console.log(&quot;IIFE 실행! 🚀&quot;);
})();

// console.log(private); // ReferenceError

// 매개변수 전달
(function(name) {
    console.log(`Hello, ${name}! 👋`);
})(&quot;Alice&quot;);</code></pre>
<h3 id="🔒-모듈-패턴">🔒 모듈 패턴</h3>
<pre><code class="language-javascript">const calculator = (function() {
    // Private 변수
    let result = 0;

    // Private 함수
    function log(message) {
        console.log(`[Calculator] ${message}`);
    }

    // Public API 반환
    return {
        add(num) {
            result += num;
            log(`Added ${num}, result: ${result}`);
            return this;
        },
        subtract(num) {
            result -= num;
            log(`Subtracted ${num}, result: ${result}`);
            return this;
        },
        multiply(num) {
            result *= num;
            log(`Multiplied by ${num}, result: ${result}`);
            return this;
        },
        getResult() {
            return result;
        },
        reset() {
            result = 0;
            log(&quot;Reset!&quot;);
            return this;
        }
    };
})();

// 메서드 체이닝
calculator
    .add(10)        // [Calculator] Added 10, result: 10
    .multiply(2)    // [Calculator] Multiplied by 2, result: 20
    .subtract(5);   // [Calculator] Subtracted 5, result: 15

console.log(calculator.getResult()); // 15</code></pre>
<h3 id="🏭-팩토리-패턴">🏭 팩토리 패턴</h3>
<pre><code class="language-javascript">function createUser(name, role) {
    // Private 변수
    let isAuthenticated = false;

    return {
        // Public 메서드
        getName() {
            return name;
        },
        getRole() {
            return role;
        },
        login() {
            isAuthenticated = true;
            console.log(`${name} logged in as ${role} ✅`);
        },
        logout() {
            isAuthenticated = false;
            console.log(`${name} logged out ❌`);
        },
        isLoggedIn() {
            return isAuthenticated;
        }
    };
}

const admin = createUser(&quot;Alice&quot;, &quot;admin&quot;);
const user = createUser(&quot;Bob&quot;, &quot;user&quot;);

admin.login();  // Alice logged in as admin ✅
console.log(admin.isLoggedIn()); // true

user.login();   // Bob logged in as user ✅
console.log(user.isLoggedIn()); // true</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 짧은 간단 지식 정리 - 제너레이터]]></title>
            <link>https://velog.io/@lee_1124/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A7%A7%EC%9D%80-%EA%B0%84%EB%8B%A8-%EC%A7%80%EC%8B%9D-%EC%A0%95%EB%A6%AC-%EC%A0%9C%EB%84%88%EB%A0%88%EC%9D%B4%ED%84%B0</link>
            <guid>https://velog.io/@lee_1124/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A7%A7%EC%9D%80-%EA%B0%84%EB%8B%A8-%EC%A7%80%EC%8B%9D-%EC%A0%95%EB%A6%AC-%EC%A0%9C%EB%84%88%EB%A0%88%EC%9D%B4%ED%84%B0</guid>
            <pubDate>Mon, 23 Dec 2024 07:55:15 GMT</pubDate>
            <description><![CDATA[<h3 id="1-제너레이터generator">1. 제너레이터(Generator)</h3>
<ul>
<li><strong>제너레이터(Generator)</strong> 는 함수 실행을 일시 중지하고 다시 시작할 수 있는 특별한 함수이다.</li>
<li>일반 함수와 달리, 제너레이터는 <strong><code>function*</code> 키워드</strong>로 정의되며, 호출 시 즉시 실행되지 않고 <strong>이터레이터(Iterator)</strong> 객체를 반환한다.<ul>
<li>이를 통해 값을 순차적으로 생성하거나 제어할 수 있는데, 제너레이터는 특히 비동기 처리나 큰 데이터 집합을 단계별로 처리할 때 유용하다.</li>
</ul>
</li>
</ul>
<p>&amp;nbsp</p>
<h3 id="2-iterable-iterator">2. iterable, iterator</h3>
<ul>
<li>제너레이터는 <strong>iterable하고 iterator</strong>한 특징을 가지는데 각 내용은 아래와 같다.</li>
</ul>
<blockquote>
<h4 id="iterable"><code>iterable</code></h4>
</blockquote>
<ul>
<li>JavaScript에서 <code>iterable</code>은 반복 가능한 객체를 의미하며, <code>for...of</code> 루프나 전개 연산자(<code>...</code>) 등과 같은 반복문에서 사용할 수 있는 객체이다.</li>
<li>이 반복 가능한 객체(<code>iterable</code>한 객체)는 항상 <code>Symbol.iterator()</code> 메소드를 구현하고 있어야 하며, 이 메소드는 객체가 어떻게 반복될지 정의한다.</li>
</ul>
<blockquote>
<h4 id="iterator"><code>iterator</code></h4>
</blockquote>
<ul>
<li>반복 작업을 제어하는 객체로, 반복되는 시퀀스의 값을 하나씩 반환할 수 있다.</li>
<li><code>next()</code> 메서드를 호출할 때마다 순차적으로 값을 반환하며, 반복이 끝나면 <code>done: true</code>가 반환된다.</li>
</ul>
<p>&amp;nbsp</p>
<h3 id="3-제너레이터의-특징">3. 제너레이터의 특징</h3>
<ul>
<li>제너레이터 함수를 실행하면, <strong>iterator</strong>한 제너레이터 객체가 반환된다.<ul>
<li>반환된 <code>iterator</code> 객체가 갖고있는 <code>next()</code> 메소드를 사용하면, 가장 가까운 <code>yield</code> 키워드를 만날 때까지 코드를 실행할 수 있다.</li>
</ul>
</li>
</ul>
<h4 id="3-1-제너레이터-함수-정의">3-1. 제너레이터 함수 정의</h4>
<pre><code class="language-js">function* myGenerator() {
    yield 1; 
    yield 2; 
    yield 3; 
}</code></pre>
<ul>
<li><code>function*</code> 키워드와 함께 <code>yield</code> 키워드를 사용해 값을 생성한다.</li>
</ul>
<h4 id="3-2-제너레이터-호출">3-2. 제너레이터 호출</h4>
<pre><code class="language-js">function* fn() {
    console.log(1);
    yield 1;
    console.log(2);
    yield 2;
    console.log(3);
    console.log(4);
    yield 3;
    console.log(5);

    return &quot;finish!!&quot;;
}

// iterator한 제너레이터 객체가 반환됨.
// 이 객체는 내부적으로 next()메소드를 가지고 있어, 사용 가능
const a = fn(); 

// 1 출력 -&gt; next()는 {value:1, done: false} 반환 후 출력
console.log(a.next()); 

// 2 출력 -&gt; next()는 {value:2, done: false} 반환 후 출력
console.log(a.next());

// 3,4 출력 -&gt; next()는 {value:3, done: false} 반환 후 출력
console.log(a.next());

// 5 출력 -&gt; next()는 {value: finish!!, done: true} 반환 후 출력
console.log(a.next());

// next()는 {value: undefined, done: true} 반환 후 출력
console.log(a.next());</code></pre>
<ul>
<li>제너레이터 함수 호출은 iterator 객체를 반환하며, <code>next()</code> 메서드를 호출해 하나씩 값을 얻을 수 있다.</li>
</ul>
<h4 id="3-3-return-메소드를-사용한-제너레이터-중지">3-3. return 메소드를 사용한 제너레이터 중지</h4>
<pre><code class="language-js">function* func() {
    console.log(11);
    yield 1;
    console.log(22);
    yield 2;
    console.log(33);
    console.log(44);
    yield 3;
    console.log(55);

    return &quot;finish!!&quot;;
}

const a = func(); 

// 11 출력 -&gt; next()는 {value:1, done: false} 반환 후 출력
console.log(a.next()); 

// 22 출력 -&gt; next()는 {value:2, done: false} 반환 후 출력
console.log(a.next());

// 33,44 출력 -&gt; next()는 {value: undefined, done: true} 반환 후 출력
console.log(a.return());

// iterator.next().return() 사용으로 함수 실행이 종료됨
// {value: undefined, done: true} 반환 후 출력
console.log(a.next());</code></pre>
<ul>
<li>제너레이터 객체의 return 메소드 <code>func().return()</code>를 사용하면, 즉시 done은 <code>true</code>가 되고, value는 <code>undefined</code>가 된다.<ul>
<li><code>iterator.next().return()</code>을 사용하면, 이후 <code>yield</code> 키워드를 만날 때까지 함수 내부의 코드를 실행하는 것이 아니라, 바로 그 <strong>즉시 함수 실행을 종료</strong>하기 때문에 value가 <code>undefined</code>가 된다.</li>
</ul>
</li>
</ul>
<p>&amp;nbsp</p>
<h3 id="4-제너레이터-활용-예시">4. 제너레이터 활용 예시</h3>
<h4 id="--무한-시퀀스-생성">- 무한 시퀀스 생성</h4>
<pre><code class="language-js">function* infiniteSequence() {
    let i = 0;
    while (true) {
        yield i++;
    }
}

const seq = infiniteSequence();
console.log(seq.next().value); // 0
console.log(seq.next().value); // 1</code></pre>
<h4 id="--iterable-객체-생성">- <code>iterable</code> 객체 생성</h4>
<pre><code class="language-js">function* range(start, end) {
    for (let i = start; i &lt;= end; i++) {
        yield i;
    }
}

for (let value of range(1, 5)) {
    console.log(value); // 1, 2, 3, 4, 5
}</code></pre>
<ul>
<li>제너레이터는 기본적으로 <code>iterable</code> 객체이므로 <code>for...of</code> 루프에서 사용할 수 있다.</li>
<li>이외에도 제너레이터는 <code>yield</code>를 사용해 중간 중간 작업을 멈추고 다시 시작할 수 있어, 비동기 흐름을 관리하기에도 좋다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 짧은 간단 지식 정리 - dependencies와 devDependencies의 차이]]></title>
            <link>https://velog.io/@lee_1124/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A7%A7%EC%9D%80-%EA%B0%84%EB%8B%A8-%EC%A7%80%EC%8B%9D-%EC%A0%95%EB%A6%AC-dependencies%EC%99%80-devDependencies%EC%9D%98-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@lee_1124/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A7%A7%EC%9D%80-%EA%B0%84%EB%8B%A8-%EC%A7%80%EC%8B%9D-%EC%A0%95%EB%A6%AC-dependencies%EC%99%80-devDependencies%EC%9D%98-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Wed, 11 Dec 2024 08:20:21 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>Overview</strong></p>
</blockquote>
<ul>
<li>Node.js 프로젝트에서는 <strong>패키지를 설치</strong>할 때, 해당 패키지가 프로젝트에서 어떻게 사용되는지에 따라 <strong><code>dependencies</code></strong> 또는 <strong><code>devDependencies</code></strong> 로 구분하여 관리한다.</li>
<li>이 두 가지는 <code>package.json</code> 파일의 중요한 필드이며, 설치한 패키지의 용도를 명확히 구분하는 데에 사용된다.</li>
</ul>
<hr>
<h3 id="1-dependencies">1. <code>dependencies</code></h3>
<h4 id="-정의">※ 정의</h4>
<ul>
<li><strong>production</strong> 환경에서 애플리케이션 실행 시 필수적인 패키지이다.</li>
</ul>
<h4 id="-특징">※ 특징</h4>
<ul>
<li><p>런타임에 필요한 라이브러리</p>
<ul>
<li>예: 웹 서버용 라이브러리, 프론트엔드 UI 라이브러리</li>
</ul>
</li>
<li><p>배포 환경에서 <code>dependencies</code>에 명시된 패키지들은 반드시 설치된다.</p>
</li>
</ul>
<h4 id="-설치-방법">※ 설치 방법</h4>
<pre><code class="language-bash">npm install &lt;패키지&gt;</code></pre>
<ul>
<li>혹은 명시적으로 <code>--save</code> 옵션을 사용<pre><code class="language-bash">npm install &lt;패키지&gt; --save </code></pre>
</li>
</ul>
<h4 id="-예시파일--packagejson">※ 예시파일 : <code>package.json</code></h4>
<ul>
<li>프로젝트 &#39;project-name&#39;을 실행하기 위해 필요한 패키지</li>
</ul>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;project-name&quot;,
   // ...
  &quot;dependencies&quot;: {
    &quot;@ckeditor/ckeditor5-react&quot;: &quot;8.0.0&quot;,
    &quot;@emotion/react&quot;: &quot;11.11.4&quot;,
    &quot;@emotion/styled&quot;: &quot;11.11.5&quot;,
    &quot;@mui/icons-material&quot;: &quot;5.15.20&quot;,
    &quot;@mui/material&quot;: &quot;5.15.20&quot;,
    &quot;@mui/styles&quot;: &quot;^5.15.20&quot;,
    &quot;@mui/x-data-grid&quot;: &quot;^7.19.0&quot;,
    &quot;@react-pdf/renderer&quot;: &quot;3.4.4&quot;,
    &quot;@tanstack/react-query&quot;: &quot;5.48.0&quot;,
    &quot;@tomickigrzegorz/react-circular-progress-bar&quot;: &quot;1.1.2&quot;,
    &quot;@types/leaflet&quot;: &quot;1.9.12&quot;,
    &quot;@types/navermaps&quot;: &quot;3.7.5&quot;,
    &quot;@types/proj4&quot;: &quot;2.5.5&quot;,
    &quot;@types/react-leaflet&quot;: &quot;3.0.0&quot;,
    &quot;apexcharts&quot;: &quot;3.49.2&quot;,
    &quot;axios&quot;: &quot;1.7.2&quot;,
    &quot;chart.js&quot;: &quot;4.4.3&quot;,
    &quot;chartjs-plugin-datalabels&quot;: &quot;2.2.0&quot;,
    &quot;ckeditor5&quot;: &quot;42.0.0&quot;,
    &quot;echarts&quot;: &quot;5.5.0&quot;,
    &quot;echarts-for-react&quot;: &quot;3.0.2&quot;,
    &quot;exceljs&quot;: &quot;4.4.0&quot;,
    &quot;form-data&quot;: &quot;4.0.0&quot;,
    &quot;formidable&quot;: &quot;2.1.2&quot;,
    &quot;framer-motion&quot;: &quot;11.2.12&quot;,
    &quot;geolib&quot;: &quot;3.3.4&quot;,
    &quot;html2canvas&quot;: &quot;1.4.1&quot;,
    &quot;jspdf&quot;: &quot;2.5.1&quot;,
    &quot;jwt-decode&quot;: &quot;^4.0.0&quot;,
    &quot;leaflet&quot;: &quot;1.9.4&quot;,
    &quot;leaflet-draw&quot;: &quot;1.0.4&quot;,
    &quot;leaflet-easyprint&quot;: &quot;2.1.9&quot;,
    &quot;lodash&quot;: &quot;^4.17.21&quot;,
    &quot;moment&quot;: &quot;2.30.1&quot;,
    &quot;next&quot;: &quot;14.2.3&quot;,
    &quot;next-auth&quot;: &quot;4.24.7&quot;,
    &quot;ol&quot;: &quot;9.2.4&quot;,
    &quot;proj4&quot;: &quot;2.11.0&quot;,
    &quot;proj4leaflet&quot;: &quot;1.0.2&quot;,
    &quot;react&quot;: &quot;18.3.1&quot;,
    &quot;react-apexcharts&quot;: &quot;1.4.1&quot;,
    &quot;react-barcode&quot;: &quot;^1.5.3&quot;,
    &quot;react-calendar&quot;: &quot;4.8.0&quot;,
    &quot;react-chartjs-2&quot;: &quot;5.2.0&quot;,
    &quot;react-datepicker&quot;: &quot;7.2.0&quot;,
    &quot;react-daum-postcode&quot;: &quot;^3.1.3&quot;,
    &quot;react-dom&quot;: &quot;18.3.1&quot;,
    &quot;react-hook-form&quot;: &quot;7.52.0&quot;,
    &quot;react-icons&quot;: &quot;5.2.1&quot;,
    &quot;react-kakao-maps-sdk&quot;: &quot;1.1.27&quot;,
    &quot;react-leaflet&quot;: &quot;4.2.1&quot;,
    &quot;react-leaflet-cluster&quot;: &quot;2.1.0&quot;,
    &quot;react-leaflet-draw&quot;: &quot;0.20.4&quot;,
    &quot;react-leaflet-easyprint&quot;: &quot;2.0.0&quot;,
    &quot;react-loader-spinner&quot;: &quot;6.1.6&quot;,
    &quot;react-pdf&quot;: &quot;7.7.3&quot;,
    &quot;react-scroll&quot;: &quot;1.9.0&quot;,
    &quot;react-share&quot;: &quot;5.1.0&quot;,
    &quot;react-slick&quot;: &quot;0.30.2&quot;,
    &quot;react-to-print&quot;: &quot;^2.15.1&quot;,
    &quot;react-youtube&quot;: &quot;10.1.0&quot;,
    &quot;read-excel-file&quot;: &quot;5.8.3&quot;,
    &quot;recoil&quot;: &quot;0.7.7&quot;,
    &quot;sass&quot;: &quot;^1.78.0&quot;,
    &quot;slick-carousel&quot;: &quot;1.8.1&quot;,
    &quot;styled-components&quot;: &quot;6.1.11&quot;,
    &quot;swiper&quot;: &quot;11.1.4&quot;,
    &quot;url-parse&quot;: &quot;1.5.10&quot;,
    &quot;xlsx&quot;: &quot;0.18.5&quot;,
    &quot;zod&quot;: &quot;3.23.8&quot;
  },
// ...
}</code></pre>
<hr>
<h3 id="2-devdependencies">2. <code>devDependencies</code></h3>
<h4 id="-정의-1">※ 정의</h4>
<ul>
<li>애플리케이션 <strong>개발 또는 빌드 과정</strong>에서만 필요한 패키지이다.</li>
</ul>
<h4 id="-특징-1">※ 특징</h4>
<ul>
<li><p>테스트, 코드 빌드, 린트, 문법 체크 등 <strong>개발 도구</strong>를 포함한다.<br>  (예: Webpack, ESLint, Jest 등)</p>
</li>
<li><p>배포 환경에서는 설치되지 않는다.<br>  (<code>NODE_ENV=production</code> 설정 시 무시됨)</p>
</li>
</ul>
<h4 id="-설치-방법-1">※ 설치 방법</h4>
<pre><code class="language-bash">npm install &lt;패키지&gt; -save-dev</code></pre>
<h4 id="-예시">※ 예시</h4>
<ul>
<li>Webpack, ESLint와 같은 개발도구<pre><code class="language-json">{
&quot;name&quot;: &quot;project-name&quot;,
 // ...
&quot;devDependencies&quot;: {
  &quot;@types/formidable&quot;: &quot;2.0.5&quot;,
  &quot;@types/jwt-decode&quot;: &quot;^2.2.1&quot;,
  &quot;@types/lodash&quot;: &quot;^4.17.10&quot;,
  &quot;@types/node&quot;: &quot;20.16.1&quot;,
  &quot;@types/react&quot;: &quot;18.3.4&quot;,
  &quot;@types/react-dom&quot;: &quot;18&quot;,
  &quot;@types/uuid&quot;: &quot;^10.0.0&quot;,
  &quot;eslint&quot;: &quot;8&quot;,
  &quot;eslint-config-next&quot;: &quot;14.2.5&quot;,
  &quot;typescript&quot;: &quot;5.5.4&quot;
}
// ...
}</code></pre>
</li>
</ul>
<hr>
<h3 id="3-비교">3. 비교</h3>
<table>
<thead>
<tr>
<th>특징</th>
<th>dependencies</th>
<th>devDependencies</th>
</tr>
</thead>
<tbody><tr>
<td>사용 시점</td>
<td>런타임(프로덕션 환경)</td>
<td>개발 및 빌드 단계</td>
</tr>
<tr>
<td>사용 용도</td>
<td>애플리케이션 실행 시 필수</td>
<td>테스트, 빌드, 린트 등 개발 도구</td>
</tr>
<tr>
<td>예시 패키지</td>
<td>React, Express, Axios</td>
<td>Webpack, Babel, Jest, ESLint</td>
</tr>
<tr>
<td>설치 명령어</td>
<td>npm install &lt;패키지&gt;</td>
<td>npm install &lt;패키지&gt; --save-dev</td>
</tr>
<tr>
<td>프로덕션 환경 설치</td>
<td>포함됨</td>
<td>제외됨</td>
</tr>
</tbody></table>
<hr>
<h3 id="4-실제-사용-예시">4. <strong>실제 사용 예시</strong></h3>
<h4 id="1-dependencies에-추가할-패키지">1. <code>dependencies</code>에 추가할 패키지</h4>
<ul>
<li><strong>React 프로젝트</strong>에서 사용자 인터페이스를 구성하기 위한 React 라이브러리<pre><code class="language-bash">npm install react react-dom</code></pre>
</li>
</ul>
<h4 id="2-devdependencies에-추가할-패키지">2. <code>devDependencies</code>에 추가할 패키지</h4>
<ul>
<li>코드 린트와 테스트를 위한 ESLint와 Jest 설치<pre><code class="language-bash">npm install eslint jest --save-dev</code></pre>
</li>
</ul>
<h4 id="3-배포-환경에서의-설치">3. 배포 환경에서의 설치</h4>
<ul>
<li>배포 시에는 <code>devDependencies</code>를 제외한 <code>dependencies</code>만 설치하도록 설정해야 한다.<pre><code class="language-bash">npm install --production</code></pre>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 학습코스 따라하기 (Chapter 9)]]></title>
            <link>https://velog.io/@lee_1124/9.-Next.js14-%ED%95%99%EC%8A%B5%EC%BD%94%EC%8A%A4-%EB%94%B0%EB%9D%BC%ED%95%98%EA%B8%B0-Chapter-9</link>
            <guid>https://velog.io/@lee_1124/9.-Next.js14-%ED%95%99%EC%8A%B5%EC%BD%94%EC%8A%A4-%EB%94%B0%EB%9D%BC%ED%95%98%EA%B8%B0-Chapter-9</guid>
            <pubDate>Mon, 02 Dec 2024 07:41:15 GMT</pubDate>
            <description><![CDATA[<h1 id="nextjs-streaming과-사용자-경험-개선-가이드">Next.js Streaming과 사용자 경험 개선 가이드</h1>
<p>이전 Chapter에서는 대시보드 페이지를 동적으로 만드는 데 성공했지만, 데이터 Fetch 속도가 느린 경우 애플리케이션의 성능에 어떤 영향을 미칠 수 있는지 논의했습니다.</p>
<p>이번 코스에서는 <strong>느린 데이터 요청이 있을 때 사용자 경험을 어떻게 개선</strong>할 수 있는지에 대해 살펴보겠습니다.</p>
<h2 id="1-기존-ssr의-한계">1. 기존 SSR의 한계</h2>
<p>기존 SSR의 flow를 생각해보면 다음과 같습니다:</p>
<ol>
<li>백엔드 서버(API)로부터 Data를 가져온다</li>
<li>프론트엔드 서버(Next.js)에서 HTML을 렌더링한다</li>
<li>클라이언트(브라우저)에서 HTML을 받는다</li>
<li>클라이언트(브라우저)는 자바스크립트(bundle.js)를 다운로드 한 후, <strong>상호작용성</strong>있는 웹을 만든다</li>
</ol>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/401a280f-c13a-4050-8e36-81e5181b315d/image.png" alt=""></p>
<p><strong>문제점</strong>: 위의 과정이 모두 끝나기 전까지는 사용자가 페이지와 상호작용을 할 수 없습니다. Next.js에서는 이 문제를 해결하기 위해 <strong>Streaming</strong> 기술을 도입했습니다.</p>
<h2 id="2-streaming이란">2. Streaming이란?</h2>
<h3 id="21-streaming의-정의">2.1 Streaming의 정의</h3>
<p>Next.js에서는 <strong>Streaming</strong>을 다음과 같이 정의합니다:</p>
<blockquote>
<p><strong>경로를 더 작은 &quot;청크(chunk)&quot;로 나누어 서버에서 클라이언트로 점진적으로 스트리밍하는 데이터 전송 기술</strong></p>
</blockquote>
<h3 id="22-streaming의-작동-원리">2.2 Streaming의 작동 원리</h3>
<ul>
<li>HTML을 작은 단위로 나누어 <strong>모든 데이터가 로드되기 전에 준비된 컴포넌트는 미리 완성</strong>해 상호작용할 수 있게 합니다</li>
<li><strong>우선순위가 높은 컴포넌트를 먼저 작동</strong>시킬 수 있습니다</li>
</ul>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/5e4bf7d8-a5d7-4089-bfd3-6041647a40f6/image.avif" alt=""></p>
<h3 id="23-streaming의-장점">2.3 Streaming의 장점</h3>
<ul>
<li><strong>느린 데이터 요청이 전체 페이지를 차단하는 것을 방지</strong></li>
<li>사용자는 모든 데이터가 로드될 때까지 기다리지 않고 <strong>페이지의 일부를 보고 상호작용</strong> 가능</li>
<li>각 컴포넌트가 <strong>Hydrating</strong>되기까지의 시간(TTL)을 개별적으로 관리</li>
</ul>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/d3a080ec-f55e-4609-b4f9-f79603de0913/image.avif" alt=""></p>
<h3 id="24-hydrating이란">2.4 Hydrating이란?</h3>
<p><strong>Hydrating</strong>은 Next.js 서버가 <strong>Pre-Rendering</strong>된 웹 페이지를 클라이언트에게 보낸 뒤, React가 bundling된 Javascript 코드들을 chunk 단위로 클라이언트에게 전송하고, 이러한 Javascript 코드들이 이전에 전송된 HTML DOM 요소 위로 리렌더링되는 과정에서 자기 자리를 찾아 매칭되는 과정입니다.</p>
<p>Hydrating을 통해 초기 로딩 시 클라이언트에서 <strong>즉시 상호작용이 가능</strong>하고, 이후에는 일반적인 React 애플리케이션처럼 동작할 수 있습니다.</p>
<hr>
<h2 id="3-nextjs-streaming-구현-방법">3. Next.js Streaming 구현 방법</h2>
<p>Next.js에서 스트리밍을 구현하는 방법은 두 가지입니다:</p>
<ol>
<li>페이지 수준에서 <code>loading.tsx</code> 파일 사용</li>
<li>특정 컴포넌트에서 <code>&lt;Suspense&gt;</code> 사용</li>
</ol>
<hr>
<h2 id="4-페이지-레벨-streaming-loadingtsx">4. 페이지 레벨 Streaming: <code>loading.tsx</code></h2>
<h3 id="41-기본-loading-컴포넌트-생성">4.1 기본 Loading 컴포넌트 생성</h3>
<p><code>/app/dashboard</code> 디렉터리에 <code>loading.tsx</code> 파일을 생성합니다:</p>
<pre><code class="language-tsx">export default function Loading() {
  return &lt;div&gt;Loading...&lt;/div&gt;;
}</code></pre>
<h3 id="42-loading의-작동-원리">4.2 Loading의 작동 원리</h3>
<ul>
<li><code>loading.tsx</code>는 <strong>Suspense를 기반</strong>으로 한 Next.js의 특수 파일입니다</li>
<li>페이지 콘텐츠가 로드되는 동안 <strong>대체 UI</strong>를 보여줍니다</li>
<li><code>&lt;SideNav&gt;</code> 같은 정적 컴포넌트는 즉시 표시되며, 사용자는 동적 콘텐츠가 로드되는 동안에도 상호작용할 수 있습니다</li>
<li><strong>Interruptable navigation</strong>: 페이지가 완전히 로드되기를 기다릴 필요 없이 페이지를 떠날 수 있습니다</li>
</ul>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/78fed9dd-cf2d-4f8b-ba0b-16a49b7efae7/image.png" alt=""></p>
<h3 id="43-loading-skeleton-추가">4.3 Loading Skeleton 추가</h3>
<p>사용자 경험을 개선하기 위해 <strong>Loading Skeleton</strong>을 추가할 수 있습니다:</p>
<pre><code class="language-tsx">import DashboardSkeleton from &quot;../ui/skeletons&quot;;

export default function Loading() {
  return &lt;DashboardSkeleton /&gt;;
}</code></pre>
<h3 id="44-skeleton-ui의-장점">4.4 Skeleton UI의 장점</h3>
<ul>
<li>사용자에게 <strong>서비스가 원활히 작동 중임을 알림</strong></li>
<li>하얀 백지 화면 대신 의미 있는 UI 제공</li>
<li>사용자 이탈 방지 효과</li>
</ul>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/55f19db0-da90-48b3-a7c7-2158be5c2eb8/image.png" alt=""></p>
<hr>
<h2 id="5-route-groups를-활용한-skeleton-버그-수정">5. Route Groups를 활용한 Skeleton 버그 수정</h2>
<h3 id="51-문제-상황">5.1 문제 상황</h3>
<p>현재 <code>loading.tsx</code> 파일의 대시보드 Skeleton UI가 <strong>송장 페이지와 고객 페이지에도 적용</strong>되는 문제가 발생합니다.</p>
<p><strong>원인</strong>: <code>/app/dashboard/loading.tsx</code>가 <code>/app/dashboard/invoices/page.tsx</code> 및 <code>/app/dashboard/customers/page.tsx</code>보다 상위 레벨에 위치</p>
<h3 id="52-route-groups란">5.2 Route Groups란?</h3>
<p><strong>Route Groups</strong>는 폴더를 <strong>경로의 URL 경로에 포함되지 않도록</strong> 표시할 수 있는 기능입니다.</p>
<ul>
<li>URL 경로 구조에 영향을 주지 않으면서 경로 세그먼트와 프로젝트 파일을 논리적인 그룹으로 구성</li>
<li>디렉터리명을 <strong>소괄호 <code>()</code></strong>로 감싸서 생성</li>
</ul>
<h3 id="53-해결-방법">5.3 해결 방법</h3>
<p><code>/app/dashboard</code> 하위에 <code>(overview)</code> 폴더를 생성하고, <code>page.tsx</code>와 <code>loading.tsx</code>를 이동:</p>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/2fee8b1e-5e91-426e-b746-7fed59e3a835/image.png" alt=""></p>
<h3 id="54-route-groups-활용-방법">5.4 Route Groups 활용 방법</h3>
<ol>
<li><strong>URL 경로 구조에 영향을 주지 않고</strong> 파일을 논리적 그룹으로 구성</li>
<li><code>()</code>로 감싼 디렉터리는 URL 경로에 포함되지 않음</li>
<li><code>/dashboard/(overview)/page.tsx</code> → URL: <code>/dashboard</code></li>
<li>애플리케이션을 <strong>Section별</strong> <code>((marketing), (shop))</code> 또는 <strong>팀별</strong>로 분리 가능</li>
</ol>
<hr>
<h2 id="6-컴포넌트-레벨-streaming-suspense">6. 컴포넌트 레벨 Streaming: <code>&lt;Suspense&gt;</code></h2>
<h3 id="61-컴포넌트별-세분화된-streaming">6.1 컴포넌트별 세분화된 Streaming</h3>
<p>전체 페이지가 아닌 <strong>특정 컴포넌트만</strong> 스트리밍하고 싶을 때 <strong>React Suspense</strong>를 사용합니다.</p>
<h3 id="62-suspense의-작동-원리">6.2 Suspense의 작동 원리</h3>
<ul>
<li>일부 조건(데이터 로드)을 충족할 때까지 <strong>애플리케이션 일부의 렌더링을 지연</strong></li>
<li>동적 컴포넌트를 감싸고, 로드되는 동안 <strong>대체 컴포넌트(fallback)</strong> 표시</li>
</ul>
<h3 id="63-실습-revenuechart-컴포넌트-스트리밍">6.3 실습: RevenueChart 컴포넌트 스트리밍</h3>
<h4 id="step-1-기존-코드에서-데이터-fetch-제거">Step 1: 기존 코드에서 데이터 fetch 제거</h4>
<pre><code class="language-tsx">export default async function Page() {
  // const revenue = await fetchRevenue(); // 삭제
  const latestInvoices = await fetchLatestInvoices();
  // ...
}</code></pre>
<h4 id="step-2-suspense로-컴포넌트-감싸기">Step 2: Suspense로 컴포넌트 감싸기</h4>
<pre><code class="language-tsx">import { Suspense } from &#39;react&#39;;

return (
  &lt;main&gt;
    {/* 기존 코드 */}
    &lt;div className=&quot;mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8&quot;&gt;
      &lt;Suspense fallback={&lt;RevenueChartSkeleton /&gt;}&gt;
        &lt;RevenueChart /&gt;
      &lt;/Suspense&gt;
      &lt;LatestInvoices latestInvoices={latestInvoices} /&gt;
    &lt;/div&gt;
  &lt;/main&gt;
);</code></pre>
<h4 id="step-3-컴포넌트-내부에서-데이터-fetch">Step 3: 컴포넌트 내부에서 데이터 fetch</h4>
<pre><code class="language-tsx">import { fetchRevenue } from &quot;@/app/lib/data&quot;;

export default async function RevenueChart() {
  const chartHeight = 350;
  const revenue = await fetchRevenue(); // 추가
  const { yAxisLabels, topLabel } = generateYAxis(revenue);

  if (!revenue || revenue.length === 0) {
    return &lt;p className=&quot;mt-4 text-gray-400&quot;&gt;No data available.&lt;/p&gt;;
  }

  // 컴포넌트 렌더링 로직...
}</code></pre>
<h3 id="64-결과-비교">6.4 결과 비교</h3>
<p><strong>이전 코드 (Suspense 없음)</strong>:</p>
<pre><code class="language-tsx">// 페이지 전체가 차단되고, 모든 데이터 fetch가 완료되어야 화면 표시
&lt;RevenueChart /&gt;</code></pre>
<p><strong>변경 코드 (Suspense 적용)</strong>:</p>
<pre><code class="language-tsx">// 컴포넌트별 스트리밍으로 페이지 전체 차단 없음
// fetchRevenue() 완료 전까지 fallback 컴포넌트 표시
&lt;Suspense fallback={&lt;RevenueChartSkeleton /&gt;}&gt;
  &lt;RevenueChart /&gt;
&lt;/Suspense&gt;</code></pre>
<h3 id="65-로딩-단계별-화면">6.5 로딩 단계별 화면</h3>
<p><strong>1단계</strong>: <code>fetchLatestInvoices()</code>, <code>fetchCardData()</code> 진행 중
<img src="https://velog.velcdn.com/images/lee_1124/post/2a9d1b18-8f22-4527-8f45-257b7a2978dc/image.png" alt=""></p>
<p><strong>2단계</strong>: 위 함수들 완료 후, <code>fetchRevenue()</code> 수행 전
<img src="https://velog.velcdn.com/images/lee_1124/post/87aa829d-1f3e-433e-997a-f1bd4e20d66b/image.png" alt=""></p>
<hr>
<h2 id="7-컴포넌트-그룹핑-card-최적화">7. 컴포넌트 그룹핑: <code>&lt;Card/&gt;</code> 최적화</h2>
<h3 id="71-popping-효과-문제">7.1 Popping 효과 문제</h3>
<p>각 개별 카드에 대한 데이터를 따로 가져오면 <strong>Popping 효과</strong>(화면 요소가 갑자기 바뀌거나 나타나는 효과)가 발생하여 사용자에게 시각적으로 거슬릴 수 있습니다.</p>
<h3 id="72-해결-방안-컴포넌트-그룹화">7.2 해결 방안: 컴포넌트 그룹화</h3>
<p>카드들을 그룹화하여 <strong>종합된 지연 효과</strong>를 만들어 정적인 <code>&lt;SideNav/&gt;</code>가 먼저 표시되고 그 다음에 카드 데이터가 표시되도록 합니다.</p>
<h3 id="73-구현-과정">7.3 구현 과정</h3>
<h4 id="step-1-page-컴포넌트-수정">Step 1: Page 컴포넌트 수정</h4>
<pre><code class="language-tsx">import { Suspense } from &quot;react&quot;;
import {
  CardSkeleton,
  LatestInvoicesSkeleton,
  RevenueChartSkeleton,
} from &quot;@/app/ui/skeletons&quot;;
import CardWrapper from &quot;@/app/ui/dashboard/cards&quot;;

export default async function Page() {
  return (
    &lt;main&gt;
      &lt;h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}&gt;
        Dashboard
      &lt;/h1&gt;

      {/* 카드들을 그룹화하여 Suspense로 감싸기 */}
      &lt;div className=&quot;grid gap-6 sm:grid-cols-2 lg:grid-cols-4&quot;&gt;
        &lt;Suspense fallback={&lt;CardSkeleton /&gt;}&gt;
          &lt;CardWrapper /&gt;
        &lt;/Suspense&gt;
      &lt;/div&gt;

      &lt;div className=&quot;mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8&quot;&gt;
        &lt;Suspense fallback={&lt;RevenueChartSkeleton /&gt;}&gt;
          &lt;RevenueChart /&gt;
        &lt;/Suspense&gt;
        &lt;Suspense fallback={&lt;LatestInvoicesSkeleton /&gt;}&gt;
          &lt;LatestInvoices /&gt;
        &lt;/Suspense&gt;
      &lt;/div&gt;
    &lt;/main&gt;
  );
}</code></pre>
<h4 id="step-2-cardwrapper-컴포넌트-생성">Step 2: CardWrapper 컴포넌트 생성</h4>
<pre><code class="language-tsx">export default async function CardWrapper() {
  const {
    numberOfCustomers,
    numberOfInvoices,
    totalPaidInvoices,
    totalPendingInvoices,
  } = await fetchCardData();

  return (
    &lt;&gt;
      &lt;Card title=&quot;Collected&quot; value={totalPaidInvoices} type=&quot;collected&quot; /&gt;
      &lt;Card title=&quot;Pending&quot; value={totalPendingInvoices} type=&quot;pending&quot; /&gt;
      &lt;Card title=&quot;Total Invoices&quot; value={numberOfInvoices} type=&quot;invoices&quot; /&gt;
      &lt;Card
        title=&quot;Total Customers&quot;
        value={numberOfCustomers}
        type=&quot;customers&quot;
      /&gt;
    &lt;/&gt;
  );
}</code></pre>
<h3 id="74-최종-결과">7.4 최종 결과</h3>
<p><img src="https://velog.velcdn.com/images/lee_1124/post/df92f4d2-9011-4695-b458-91b2d19fbf48/image.png" alt=""></p>
<hr>
<h2 id="8-suspense-경계-설정-가이드">8. Suspense 경계 설정 가이드</h2>
<h3 id="81-고려사항">8.1 고려사항</h3>
<p>Suspense 경계를 어디에 놓을지 결정할 때 고려해야 할 사항들:</p>
<ol>
<li><strong>사용자 경험</strong>: 페이지가 스트리밍되는 동안 사용자가 어떻게 경험하길 원하는가?</li>
<li><strong>우선순위</strong>: 어떤 콘텐츠를 우선순위로 두길 원하는가?</li>
<li><strong>데이터 의존성</strong>: 컴포넌트가 데이터 Fetch에 의존하는가?</li>
</ol>
<h3 id="82-best-practices">8.2 Best Practices</h3>
<h4 id="✅-권장사항">✅ 권장사항</h4>
<ul>
<li><strong>데이터를 필요로 하는 컴포넌트</strong>에 데이터를 불러오는 메소드를 이동</li>
<li>해당 컴포넌트를 <strong>Suspense로 래핑</strong></li>
<li>컴포넌트별 독립적인 로딩 상태 관리</li>
</ul>
<h4 id="⚠️-상황에-따른-선택">⚠️ 상황에 따른 선택</h4>
<ul>
<li><strong>Section 또는 전체 페이지 스트리밍</strong>도 애플리케이션 요구사항에 따라 적절할 수 있음</li>
<li><strong>정답은 없으며</strong>, 프로젝트의 특성과 사용자 요구사항에 맞게 결정</li>
</ul>
<h3 id="83-실습에서-적용한-전략">8.3 실습에서 적용한 전략</h3>
<ol>
<li><strong>처음</strong>: 전체 페이지를 <code>loading.tsx</code>로 스트리밍</li>
<li><strong>개선</strong>: 느린 컴포넌트로 인한 전체 지연을 방지하기 위해 <strong>개별 컴포넌트 스트리밍</strong></li>
<li><strong>최적화</strong>: <code>&lt;Card/&gt;</code> 컴포넌트의 <strong>popping 효과</strong> 방지를 위한 그룹화</li>
</ol>
<hr>
<h2 id="결론">결론</h2>
<p>Next.js의 Streaming 기능을 통해 사용자 경험을 크게 개선할 수 있습니다. 핵심은 <strong>데이터 로딩 상태를 세밀하게 관리</strong>하고, <strong>사용자가 기다리는 시간을 최소화</strong>하면서도 <strong>시각적으로 안정적인 인터페이스</strong>를 제공하는 것입니다.</p>
<h3 id="핵심-포인트">핵심 포인트</h3>
<ul>
<li><strong>페이지 레벨</strong>: <code>loading.tsx</code>로 전체 페이지 스트리밍</li>
<li><strong>컴포넌트 레벨</strong>: <code>&lt;Suspense&gt;</code>로 세밀한 제어</li>
<li><strong>Route Groups</strong>: 경로별 로딩 상태 분리</li>
<li><strong>컴포넌트 그룹화</strong>: 시각적 안정성 확보</li>
</ul>
<p>이러한 기법들을 적절히 조합하여 사용자가 만족할 수 있는 웹 애플리케이션을 구축할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 짧은 간단 지식 정리 - 인라인 레벨 함수와 모듈 레벨 함수의 차이]]></title>
            <link>https://velog.io/@lee_1124/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A7%A7%EC%9D%80-%EA%B0%84%EB%8B%A8-%EC%A7%80%EC%8B%9D-%EC%A0%95%EB%A6%AC-%EC%9D%B8%EB%9D%BC%EC%9D%B8-%EB%A0%88%EB%B2%A8-%ED%95%A8%EC%88%98%EC%99%80-%EB%AA%A8%EB%93%88-%EB%A0%88%EB%B2%A8-%ED%95%A8%EC%88%98%EC%9D%98-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@lee_1124/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A7%A7%EC%9D%80-%EA%B0%84%EB%8B%A8-%EC%A7%80%EC%8B%9D-%EC%A0%95%EB%A6%AC-%EC%9D%B8%EB%9D%BC%EC%9D%B8-%EB%A0%88%EB%B2%A8-%ED%95%A8%EC%88%98%EC%99%80-%EB%AA%A8%EB%93%88-%EB%A0%88%EB%B2%A8-%ED%95%A8%EC%88%98%EC%9D%98-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Fri, 22 Nov 2024 01:53:42 GMT</pubDate>
            <description><![CDATA[<h3 id="1-선언-위치">1. <strong>선언 위치</strong></h3>
<ul>
<li><strong>인라인 함수</strong><ul>
<li>인라인 함수는 일반적으로 해당 함수를 호출하는 곳에 직접 선언된다.</li>
<li>주로 콜백 함수나 짧은 동작을 수행하는 함수 등 간단한 작업에 사용된다.</li>
<li>아래 코드의 <code>function(number) { return number * 2; }</code>는 <code>map</code> 메서드 안에서 인라인으로 선언된 함수이다.<ul>
<li>해당 함수는 간단한 작업을 수행하기 때문에 별도로 분리하지 않고 인라인으로 작성되었다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<pre><code class="language-js">// 배열의 각 요소를 두 배로 만드는 인라인 함수
const numbers = [1, 2, 3, 4];

// 인라인 함수가 map 함수의 콜백으로 사용됨
const doubledNumbers = numbers.map(function(number) {
  return number * 2;
});

console.log(doubledNumbers); // 출력: [2, 4, 6, 8]</code></pre>
<p>&amp;nbsp</p>
<ul>
<li><p><strong>모듈 함수</strong></p>
<ul>
<li><p>모듈 함수는 모듈 파일의 최상위 레벨에서 선언된다. </p>
</li>
<li><p>다른 모듈(별도 파일로 분리해서 선언)에서 불러와 사용할 수 있는 함수이다.</p>
</li>
<li><p>아래 예시에서 <code>add</code>와 <code>multiply</code> 함수는 <code>mathUtils.js</code> 파일에서 모듈 함수로 선언되었으며, <code>app.js</code>에서 이를 가져와 사용했다.</p>
<ul>
<li>이러한 모듈 함수는 여러 곳에서 재사용이 가능하고, 코드의 유지보수성을 높여준다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><code>mathUtils.js</code></p>
<pre><code class="language-js">// 모듈 함수 선언
export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}</code></pre>
<p><code>page.tsx (다른 모듈에서 함수 호출)</code></p>
<pre><code class="language-tsx">import { add, multiply } from &#39;./mathUtils.js&#39;; // 모듈 함수 가져오기

const sum = add(5, 3);
const product = multiply(4, 2);

console.log(sum);      // 출력: 8
console.log(product);  // 출력: 8</code></pre>
<p>&amp;nbsp</p>
<h3 id="2-재사용성">2. <strong>재사용성</strong></h3>
<ul>
<li><strong>인라인 함수</strong><ul>
<li>주로 해당 위치에서만 사용된다.</li>
<li>즉, 다른 곳에서 재사용되지 않고 해당 위치에서만 필요한 작업을 수행한다.</li>
</ul>
</li>
</ul>
<ul>
<li><strong>모듈 함수</strong><ul>
<li>다른 모듈에서 재사용될 수 있으므로 일반적으로 더 큰 범위에서 사용된다. </li>
<li>모듈 함수는 여러 곳에서 호출되어 동일한 작업을 수행할 수 있다.</li>
</ul>
</li>
</ul>
<p>&amp;nbsp</p>
<h3 id="3-스코프">3. <strong>스코프</strong></h3>
<ul>
<li><strong>인라인 함수</strong><ul>
<li>주로 해당 함수를 포함하는 블록의 스코프 내에서만 유효하다.</li>
<li>함수가 선언된 블록을 벗어나면 사용할 수 없다.</li>
</ul>
</li>
</ul>
<ul>
<li><strong>모듈 함수</strong><ul>
<li>모듈 함수는 모듈의 전역 스코프에 정의되며, 해당 모듈 내의 모든 함수에서 사용할 수 있다.</li>
<li>물론, 다른 모듈에서 또다른 모듈을 가져와 사용할 수 있다.</li>
</ul>
</li>
</ul>
<p>&amp;nbsp</p>
<h3 id="4-성능">4. <strong>성능</strong></h3>
<ul>
<li><strong>인라인 함수</strong><ul>
<li>호출할 때마다 함수가 다시 정의되므로, 반복적으로 호출되는 경우 성능에 영향을 줄 수 있다.</li>
</ul>
</li>
</ul>
<ul>
<li><strong>모듈 함수</strong><ul>
<li>모듈 함수는 한 번 정의되고 다시 사용되므로 상황에 따라 성능 상의 이점이 있을 수 있다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 짧은 간단 지식 정리 - 반응형 웹과 적응형 웹]]></title>
            <link>https://velog.io/@lee_1124/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A7%A7%EC%9D%80-%EA%B0%84%EB%8B%A8-%EC%A7%80%EC%8B%9D-%EC%A0%95%EB%A6%AC-%EB%B0%98%EC%9D%91%ED%98%95-%EC%9B%B9%EA%B3%BC-%EC%A0%81%EC%9D%91%ED%98%95-%EC%9B%B9</link>
            <guid>https://velog.io/@lee_1124/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A7%A7%EC%9D%80-%EA%B0%84%EB%8B%A8-%EC%A7%80%EC%8B%9D-%EC%A0%95%EB%A6%AC-%EB%B0%98%EC%9D%91%ED%98%95-%EC%9B%B9%EA%B3%BC-%EC%A0%81%EC%9D%91%ED%98%95-%EC%9B%B9</guid>
            <pubDate>Fri, 22 Nov 2024 01:50:04 GMT</pubDate>
            <description><![CDATA[<p>반응형 웹(Responsive Web)과 적응형 웹(Adaptive Web)은 화면 크기나 기기에 따라 웹 페이지가 사용자에게 <strong>최적화된 화면</strong>을 제공하는 방법입니다. 하지만 이 두 접근 방식은 <strong>화면을 최적화하는 방식</strong>에서 차이가 있습니다.</p>
<hr>
<h2 id="1-반응형-웹-responsive-web-🌊">1. 반응형 웹 (Responsive Web) 🌊</h2>
<h3 id="🎯-개념">🎯 개념</h3>
<p>반응형 웹은 <strong>CSS의 미디어 쿼리(media query)</strong>를 사용하여 화면 크기와 관계없이 <strong>유동적으로</strong> 레이아웃을 변경합니다.</p>
<p>하나의 HTML과 CSS 코드 기반을 사용해 화면 크기에 맞게 요소들이 자동으로 재배치되고 크기가 조정되므로, 사용자가 어떤 기기(모바일, 태블릿, 데스크톱 등)를 사용하든 <strong>일관된 경험</strong>을 제공할 수 있습니다.</p>
<h3 id="💻-코드-예시">💻 코드 예시</h3>
<pre><code class="language-css">/* 데스크톱 스타일 */
body {
  font-size: 16px;
}

@media (max-width: 768px) {
  /* 태블릿 스타일 */
  body {
    font-size: 14px;
  }
}

@media (max-width: 480px) {
  /* 모바일 스타일 */
  body {
    font-size: 12px;
  }
}</code></pre>
<h3 id="✨-특징">✨ 특징</h3>
<table>
<thead>
<tr>
<th>특징</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>유동적인 레이아웃</strong></td>
<td>화면 크기에 따라 요소의 크기와 위치가 비율에 맞춰 <strong>자동으로 조정</strong></td>
</tr>
<tr>
<td><strong>미디어 쿼리 사용</strong></td>
<td>CSS에서 화면의 가로 너비를 기준으로 스타일을 조정</td>
</tr>
<tr>
<td><strong>코드의 일관성</strong></td>
<td>HTML 구조가 하나만 존재하므로, 유지 보수가 용이하고 코드 중복 감소</td>
</tr>
<tr>
<td><strong>처리 주체</strong></td>
<td><strong>클라이언트 측</strong>에서 화면 상황에 따라 요소들의 배치 변화 처리</td>
</tr>
</tbody></table>
<h3 id="👍-장점">👍 장점</h3>
<ul>
<li>✅ <strong>유지 보수가 편리함</strong>: 코드가 하나만 있어 관리가 쉬움</li>
<li>✅ <strong>유연한 대응</strong>: 화면 크기 변화에 유연하게 대응 가능</li>
<li>✅ <strong>일관된 UX</strong>: 다양한 디바이스에서 일관된 사용자 경험 제공</li>
<li>✅ <strong>코드 중복 최소화</strong>: 단일 HTML/CSS 코드베이스 사용</li>
</ul>
<h3 id="👎-단점">👎 단점</h3>
<ul>
<li>⚠️ <strong>구현 복잡도</strong>: 모든 화면 크기에서 작동하도록 고려해야 하므로 구현이 복잡해질 수 있음</li>
<li>⚠️ <strong>설계 시간 증가</strong>: 해상도별로 보여질 화면을 꼼꼼히 정의해야 하므로 화면 설계에 많은 시간 소요<ul>
<li>하나의 템플릿으로 모든 기기에 대응해야 하기 때문</li>
</ul>
</li>
<li>⚠️ <strong>UX 이슈 가능성</strong>: 화면이 커지거나 작아질 때 컨텐츠 배치나 크기가 자연스럽게 조정되지 않으면 비효율적이거나 보기 불편할 수 있음</li>
</ul>
<hr>
<h2 id="2-적응형-웹-adaptive-web-🎛️">2. 적응형 웹 (Adaptive Web) 🎛️</h2>
<h3 id="🎯-개념-1">🎯 개념</h3>
<p>적응형 웹은 여러 개의 <strong>고정된 레이아웃</strong>을 만들어 놓고, 사용자가 접속한 화면 크기에 맞는 레이아웃을 <strong>선택하여 표시</strong>하는 방식입니다.</p>
<p>반응형 웹과 달리 미리 정의된 레이아웃 중에서 선택하기 때문에 <strong>특정 화면 크기별로 세부적인 최적화</strong>가 가능합니다.</p>
<h3 id="💻-코드-예시-1">💻 코드 예시</h3>
<pre><code class="language-html">&lt;!-- Desktop layout --&gt;
&lt;div class=&quot;desktop&quot;&gt;This is for desktop&lt;/div&gt;

&lt;!-- Tablet layout --&gt;
&lt;div class=&quot;tablet&quot;&gt;This is for tablet&lt;/div&gt;

&lt;!-- Mobile layout --&gt;
&lt;div class=&quot;mobile&quot;&gt;This is for mobile&lt;/div&gt;</code></pre>
<pre><code class="language-css">/* 각 레이아웃별로 CSS를 조정 */
.desktop { display: block; }
.tablet, .mobile { display: none; }

@media (max-width: 768px) {
  .desktop { display: none; }
  .tablet { display: block; }
}

@media (max-width: 480px) {
  .tablet { display: none; }
  .mobile { display: block; }
}</code></pre>
<h3 id="✨-특징-1">✨ 특징</h3>
<table>
<thead>
<tr>
<th>특징</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>고정된 레이아웃</strong></td>
<td>몇 가지 화면 크기(모바일, 태블릿, 데스크톱)에 맞춰 미리 정의된 레이아웃 사용</td>
</tr>
<tr>
<td><strong>서버 기반 선택 가능</strong></td>
<td>서버가 사용자의 화면 크기를 감지하고, 해당 크기에 맞는 HTML 제공 가능</td>
</tr>
<tr>
<td><strong>처리 주체</strong></td>
<td><strong>클라이언트 측</strong>, <strong>서버 측</strong> 둘 다 배치 변화 처리 가능</td>
</tr>
<tr>
<td><strong>렌딩(Landing)</strong></td>
<td>특정 기기에 맞는 레이아웃을 선택하고 로딩하는 과정</td>
</tr>
</tbody></table>
<h3 id="🔄-렌딩landing-프로세스">🔄 렌딩(Landing) 프로세스</h3>
<p>적응형 웹은 특정 기기에 맞는 레이아웃을 선택하고 로딩하는 <strong>렌딩 과정</strong>을 거칩니다.</p>
<ul>
<li>화면 크기와 해상도에 따라 <strong>다양한 고정된 레이아웃</strong>을 준비</li>
<li>사용자가 웹 페이지에 접속할 때 <strong>그 중 하나를 선택</strong></li>
<li>서버나 클라이언트 측에서 기기에 맞는 버전으로 <strong>redirect 가능</strong></li>
</ul>
<h4 id="💡-실제-예시-다음daum-웹사이트">💡 실제 예시: 다음(Daum) 웹사이트</h4>
<pre><code>PC 접속 시
  └─→ daum.net으로 렌딩

모바일 접속 시
  └─→ 서버가 사용자 기기 확인
      └─→ 모바일 전용 페이지로 redirect
          └─→ m.daum.net으로 렌딩</code></pre><h3 id="👍-장점-1">👍 장점</h3>
<ul>
<li>✅ <strong>최적화된 디자인</strong>: 화면 크기에 최적화된 디자인을 제공하여 성능 최적화 용이</li>
<li>✅ <strong>맞춤형 UI</strong>: 특정 디바이스에 맞춤형 UI를 적용하기 쉬움</li>
<li>✅ <strong>완벽한 기기별 UX</strong>: 기기별로 사용자 경험을 완벽하게 조정 가능</li>
<li>✅ <strong>세밀한 제어</strong>: 각 디바이스에 특화된 기능과 레이아웃 구현 가능</li>
</ul>
<h3 id="👎-단점-1">👎 단점</h3>
<ul>
<li>⚠️ <strong>유지 보수 복잡</strong>: 각 화면 크기에 맞는 레이아웃을 각각 디자인해야 하므로 유지 보수가 복잡함<ul>
<li>각 디바이스 유형에 따라 <strong>독립적인 템플릿</strong> 제작 필요</li>
<li>각 디바이스에 맞는 페이지를 <strong>별도로 제작</strong>해야 함</li>
</ul>
</li>
<li>⚠️ <strong>확장성 이슈</strong>: 화면 크기나 해상도가 추가될 때마다 새 레이아웃 작성 필요</li>
<li>⚠️ <strong>코드 중복</strong>: 여러 버전의 HTML/CSS를 관리해야 함</li>
</ul>
<hr>
<h2 id="3-비교-요약-⚖️">3. 비교 요약 ⚖️</h2>
<h3 id="📊-핵심-차이점">📊 핵심 차이점</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>반응형 웹 🌊</th>
<th>적응형 웹 🎛️</th>
</tr>
</thead>
<tbody><tr>
<td><strong>레이아웃 방식</strong></td>
<td>유동적 (Fluid)</td>
<td>고정형 (Fixed)</td>
</tr>
<tr>
<td><strong>코드 베이스</strong></td>
<td>단일 HTML/CSS</td>
<td>디바이스별 독립적 템플릿</td>
</tr>
<tr>
<td><strong>처리 위치</strong></td>
<td>클라이언트 측</td>
<td>클라이언트 측 + 서버 측</td>
</tr>
<tr>
<td><strong>주요 기술</strong></td>
<td>CSS 미디어 쿼리</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>
<h3 id="🎨-시각적-비교">🎨 시각적 비교</h3>
<h4 id="반응형-웹의-동작-방식">반응형 웹의 동작 방식</h4>
<pre><code>┌─────────────────────────────────┐
│   하나의 HTML/CSS 코드베이스    │
└─────────────────────────────────┘
              ↓
    ┌─────────┴─────────┐
    ↓                   ↓
┌─────────┐      ┌──────────┐
│ 📱 모바일 │      │ 💻 데스크톱 │
│ 자동조정 │      │  자동조정  │
└─────────┘      └──────────┘</code></pre><h4 id="적응형-웹의-동작-방식">적응형 웹의 동작 방식</h4>
<pre><code>┌───────────────────────────────────┐
│  여러 개의 독립적인 레이아웃 준비  │
└───────────────────────────────────┘
         ↓          ↓          ↓
    ┌────────┐ ┌────────┐ ┌────────┐
    │ 📱 모바일│ │ 📱 태블릿│ │💻 PC │
    │  전용   │ │  전용   │ │ 전용  │
    └────────┘ └────────┘ └────────┘
         ↓          ↓          ↓
      선택 및 렌딩 (필요시 redirect)</code></pre><h3 id="🤔-어떤-것을-선택해야-할까">🤔 어떤 것을 선택해야 할까?</h3>
<h4 id="반응형-웹을-선택하는-경우-✅">반응형 웹을 선택하는 경우 ✅</h4>
<ul>
<li>유지 보수가 중요한 프로젝트</li>
<li>다양한 화면 크기에 대응해야 하는 경우</li>
<li>개발 리소스가 제한적인 경우</li>
<li>단일 코드베이스 관리를 원하는 경우</li>
</ul>
<h4 id="적응형-웹을-선택하는-경우-✅">적응형 웹을 선택하는 경우 ✅</h4>
<ul>
<li>각 디바이스에 특화된 UX가 필요한 경우</li>
<li>성능 최적화가 매우 중요한 경우</li>
<li>디바이스별로 다른 기능을 제공해야 하는 경우</li>
<li>개발 리소스가 충분한 경우</li>
</ul>
<hr>
<h2 id="4-실전-팁-💡">4. 실전 팁 💡</h2>
<h3 id="🎯-반응형-웹-개발-팁">🎯 반응형 웹 개발 팁</h3>
<pre><code class="language-css">/* 모바일 우선(Mobile First) 접근 */
/* 기본 스타일 (모바일) */
.container {
  width: 100%;
  padding: 10px;
}

/* 태블릿 이상 */
@media (min-width: 768px) {
  .container {
    width: 750px;
    margin: 0 auto;
  }
}

/* 데스크톱 이상 */
@media (min-width: 1024px) {
  .container {
    width: 960px;
  }
}</code></pre>
<h3 id="🎛️-적응형-웹-개발-팁">🎛️ 적응형 웹 개발 팁</h3>
<pre><code class="language-javascript">// 서버 측 기기 감지 예시 (Node.js)
app.get(&#39;/&#39;, (req, res) =&gt; {
  const userAgent = req.headers[&#39;user-agent&#39;];

  if (/mobile/i.test(userAgent)) {
    res.redirect(&#39;/mobile&#39;);
  } else if (/tablet/i.test(userAgent)) {
    res.redirect(&#39;/tablet&#39;);
  } else {
    res.redirect(&#39;/desktop&#39;);
  }
});</code></pre>
<h3 id="🔧-주요-브레이크포인트">🔧 주요 브레이크포인트</h3>
<table>
<thead>
<tr>
<th>디바이스</th>
<th>화면 크기</th>
<th>미디어 쿼리</th>
</tr>
</thead>
<tbody><tr>
<td>📱 스마트폰</td>
<td>&lt; 576px</td>
<td><code>@media (max-width: 575px)</code></td>
</tr>
<tr>
<td>📱 큰 스마트폰</td>
<td>576px - 767px</td>
<td><code>@media (min-width: 576px)</code></td>
</tr>
<tr>
<td>📱 태블릿</td>
<td>768px - 991px</td>
<td><code>@media (min-width: 768px)</code></td>
</tr>
<tr>
<td>💻 데스크톱</td>
<td>992px - 1199px</td>
<td><code>@media (min-width: 992px)</code></td>
</tr>
<tr>
<td>🖥️ 대형 화면</td>
<td>≥ 1200px</td>
<td><code>@media (min-width: 1200px)</code></td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 짧은 간단 지식 정리 - 자바스크립트의 일반 함수와 화살표 함수]]></title>
            <link>https://velog.io/@lee_1124/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A7%A7%EC%9D%80-%EA%B0%84%EB%8B%A8-%EC%A7%80%EC%8B%9D-%EC%A0%95%EB%A6%AC-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%9D%98-%EC%9D%BC%EB%B0%98-%ED%95%A8%EC%88%98%EC%99%80-%ED%99%94%EC%82%B4%ED%91%9C-%ED%95%A8%EC%88%98</link>
            <guid>https://velog.io/@lee_1124/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A7%A7%EC%9D%80-%EA%B0%84%EB%8B%A8-%EC%A7%80%EC%8B%9D-%EC%A0%95%EB%A6%AC-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%9D%98-%EC%9D%BC%EB%B0%98-%ED%95%A8%EC%88%98%EC%99%80-%ED%99%94%EC%82%B4%ED%91%9C-%ED%95%A8%EC%88%98</guid>
            <pubDate>Fri, 15 Nov 2024 00:15:00 GMT</pubDate>
            <description><![CDATA[<ul>
<li>자바스크립트에서는 함수 정의 방법은 일반 함수와 화살표 함수의 두 가지 주요 스타일이 있다.<ul>
<li>일반 함수와 화살표 함수는 무엇이고, 이 둘의 차이는 무엇인지 알아보도록 하자.</li>
</ul>
</li>
</ul>
<p>&amp;nbsp</p>
<h3 id="1-일반-함수">1. 일반 함수</h3>
<ul>
<li><p>일반 함수는 <code>function</code> 키워드를 사용하여, 함수를 정의하는 방법이다. </p>
<pre><code class="language-js">function regularFunction(param1, param2) { 
// 함수 본
}</code></pre>
</li>
<li><p>특징은 아래와 같다.</p>
</li>
</ul>
<ol>
<li><strong><code>this</code> 바인딩</strong><ul>
<li>일반 함수는 호출되는 컨텍스트에 따라 <code>this</code>가 다르게 바인딩된다. </li>
<li>함수가 호출되는 위치에 따라 <code>this</code>의 값이 달라질 수 있다.</li>
</ul>
</li>
</ol>
<ol start="2">
<li><strong><code>arguments</code> 객체 o</strong><ul>
<li>일반 함수는 <code>arguments</code> 객체를 사용할 수 있다.</li>
<li>이 객체는 함수에 전달된 인수들을 배열 형태로 제공한다.</li>
</ul>
</li>
</ol>
<ol start="3">
<li><strong>생성자 함수 사용 가능</strong><ul>
<li>일반 함수는 <code>new</code> 키워드를 사용하여 생성자 함수로도 사용할 수 있다.</li>
</ul>
</li>
</ol>
<ol start="4">
<li><strong>이름 붙은 함수 표현식</strong><ul>
<li>일반 함수는 이름을 붙여 정의할 수 있다.</li>
</ul>
</li>
</ol>
<p>&amp;nbsp</p>
<h3 id="2-화살표-함수">2. 화살표 함수</h3>
<ul>
<li><p>화살표 함수는 함수를 간단하게 표현할 수 있는 ES6 문법이다.</p>
<ul>
<li>화살표 함수는 <code>=&gt;</code> 구문을 사용하여 함수를 정의하는 방법이다.<pre><code class="language-js">const arrowFunction = (param1, param2) =&gt; {
// 함수 본문
};</code></pre>
</li>
</ul>
</li>
<li><p>일반함수는 <code>this</code>가 호출하는 대상에 따라, 동적으로 바인딩된다. 그러나, 화살표함수를 사용했을 때의 this는 상위 스코프의 this를 가리키게 된다.</p>
<ul>
<li>이를 <strong>Lexical this</strong>라고 한다.</li>
</ul>
</li>
</ul>
<ul>
<li>특징은 아래와 같다.</li>
</ul>
<ol>
<li><strong><code>this</code> 바인딩</strong><ul>
<li>화살표 함수는 자신만의 <code>this</code>를 가지지 않으며, 외부 함수의 <code>this</code>를 그대로 사용한다. (함수가 호출되는 위치에 따라 <code>this</code>의 값이 달라질 수 있다.)</li>
<li>이 때문에 콜백 함수나 이벤트 핸들러에서 유용하게 사용할 수 있다.</li>
</ul>
</li>
</ol>
<ol start="2">
<li><strong><code>arguments</code> 객체 x</strong><ul>
<li>화살표 함수는 <code>arguments</code> 객체를 사용할 수 없다.</li>
<li><code>arguments</code> 객체를 사용하지 못하는 대신, <code>rest parameter</code>를 사용할 수 있다.</li>
</ul>
</li>
</ol>
<ol start="3">
<li><strong>생성자 함수 사용 불가</strong><ul>
<li>화살표 함수는 생성자 함수로 사용할 수 없다.</li>
<li>즉, <code>new</code> 키워드와 함께 사용할 수 없다.</li>
</ul>
</li>
</ol>
<ol start="4">
<li><strong>짧은 문법</strong><ul>
<li>함수 본문이 한 줄인 경우 중괄호와 <code>return</code> 키워드 없이도 사용할 수 있다.<pre><code class="language-js">const add = (a, b) =&gt; a + b;</code></pre>
</li>
</ul>
</li>
</ol>
<p>&amp;nbsp</p>
<h3 id="3-요약">3. 요약</h3>
<ol>
<li><strong><code>this</code> 바인딩</strong>: 일반 함수는 동적으로 <code>this</code>를 바인딩하지만, 화살표 함수는 외부의 <code>this</code>를 그대로 사용한다.</li>
<li><strong><code>arguments</code> 객체</strong>: 일반 함수는 <code>arguments</code> 객체를 제공하지만, 화살표 함수는 제공하지 않는다.</li>
<li><strong>생성자 함수</strong>: 일반 함수는 생성자 함수로 사용할 수 있지만, 화살표 함수는 사용할 수 없다.</li>
<li><strong>문법</strong>: 화살표 함수는 더 간결한 문법을 제공한다.</li>
</ol>
]]></description>
        </item>
    </channel>
</rss>