<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>white0_0.log</title>
        <link>https://velog.io/</link>
        <description>포기란 없습니다.</description>
        <lastBuildDate>Fri, 22 Nov 2024 06:19:02 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. white0_0.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/white0_0" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[JS]프로토타입과 프로토타입 체인]]></title>
            <link>https://velog.io/@white0_0/JS%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85%EA%B3%BC-%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85-%EC%B2%B4%EC%9D%B8</link>
            <guid>https://velog.io/@white0_0/JS%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85%EA%B3%BC-%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85-%EC%B2%B4%EC%9D%B8</guid>
            <pubDate>Fri, 22 Nov 2024 06:19:02 GMT</pubDate>
            <description><![CDATA[<h3 id="프로토타입">프로토타입</h3>
<p>JavaScript의 모든 객체는 <strong>프로토타입</strong>이라는 객체를 참조합니다.
프로토타입은 객체가 <strong>공유할 속성이나 메서드</strong>를 정의하는 데 사용되며, 새 객체가 생성될 때 해당 객체는 자신의 프로토타입을 상속받습니다.</p>
<p><strong>예시</strong></p>
<pre><code>function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}`);
};

const Hong = new Person(&#39;Hong&#39;);
Hong.sayHello(); // 출력: Hello, my name is Hong
</code></pre><ul>
<li>여기서 <code>Person.prototype</code>에 정의된 <code>sayHello</code> 메서드는 <code>Hong</code> 객체가 상속받아 사용합니다.</li>
<li>이는 <strong>메모리 절약</strong>에도 유리합니다. 메서드를 공유하므로 각 객체마다 동일한 메서드를 복사하지 않아도 됩니다.</li>
</ul>
<h3 id="프로토타입-체인">프로토타입 체인</h3>
<p>프로토타입 체인은 객체가 속성이나 메서드를 찾는 과정을 설명합니다.
객체에서 특정 속성이나 메서드를 찾으려고 할 때, 자바스크립트는 다음 단계를 따릅니다</p>
<ol>
<li>해당 객체에 속성이 있는지 확인합니다.</li>
<li>없다면 그 객체의 프로토타입을 확인합니다.</li>
<li>계속해서 상위 프로토타입으로 검색을 이어갑니다.</li>
<li>최상위 <code>Object.prototype</code>까지 확인하며, 거기서도 찾지 못하면 <code>undefined</code>를 반환합니다.<pre><code>console.log(Hong.toString()); 
// `Hong` 객체에는 `toString` 메서드가 없음 → `Person.prototype` 확인
// `Person.prototype`에도 없음 → `Object.prototype`에서 찾음 → 실행
</code></pre></li>
</ol>
<pre><code>
### 프로토타입과 상속
JavaScript에서는 프로토타입 기반 상속을 통해 객체 간에 속성과 메서드를 공유할 수 있습니다.
ES6의 `class` 문법도 결국 내부적으로는 프로토타입을 활용한 상속을 활용합니다.</code></pre><p>function Animal(name) {
  this.name = name;
}</p>
<p>Animal.prototype.move = function() {
  console.log(<code>${this.name} is moving</code>);
};</p>
<p>function Dog(name, breed) {
  Animal.call(this, name); // Animal의 생성자 호출
  this.breed = breed;
}</p>
<p>Dog.prototype = Object.create(Animal.prototype); // 프로토타입 상속
Dog.prototype.constructor = Dog;</p>
<p>Dog.prototype.bark = function() {
  console.log(<code>${this.name} says: Woof!</code>);
};</p>
<p>const myDog = new Dog(&#39;Buddy&#39;, &#39;Golden Retriever&#39;);
myDog.move(); // Buddy is moving (Animal의 메서드)
myDog.bark(); // Buddy says: Woof! (Dog의 메서드)</p>
<pre><code>
**코드 설명**

**1. Animal 생성자 함수**</code></pre><p>function Animal(name) {
  this.name = name;
}</p>
<pre><code>- `Animal`은 생성자 함수로, `new` 키워드로 호출하면 새로운 객체를 생성합니다.
- `name`이라는 속성을 초기화하며, 생성된 객체마다 고유의 값을 가질 수 있습니다.

**2. Animal의 프로토타입 메서드**</code></pre><p>Animal.prototype.move = function() {
  console.log(<code>${this.name} is moving</code>);
};</p>
<pre><code>- `Animal.prototype`에 정의된 `move` 메서드는 모든 `Animal` 객체가 상속받아 사용할 수 있습니다.
- 프로토타입에 메서드를 정의하면 메모리를 절약할 수 있습니다. 모든 인스턴스가 이 메서드를 공유합니다.

**3. Dog 생성자 함수**</code></pre><p>function Dog(name, breed) {
  Animal.call(this, name); // Animal의 생성자 호출
  this.breed = breed;
}</p>
<pre><code>
- `Dog`는 또 다른 생성자 함수입니다.
- `Animal.call(this, name)`를 사용해 `Animal`**의 생성자**를 호출하고, Dog 생성자에서 초기화 작업을 수행합니다.
  - 여기서 `this`는 `Dog` 인스턴스를 가리키며, 이를 통해 `name` 속성을 `Animal` 생성자에서 초기화합니다.
- `breed`라는 속성을 추가로 정의해 `Dog`에 고유한 속성을 설정합니다.

**4. Dog의 프로토타입 상속**</code></pre><p>Dog.prototype = Object.create(Animal.prototype); // 프로토타입 상속
Dog.prototype.constructor = Dog;</p>
<pre><code>- `Object.create(Animal.prototype)`
  - `Animal.prototype`을 프로토타입으로 가지는 새로운 객체를 생성합니다.
  - 이를 통해 `Dog`는 `Animal`의 프로토타입 체인을 상속받아 `move` 메서드를 사용할 수 있습니다.
- `Dog.prototype.constructor = Dog`
  - 프로토타입을 새로 설정하면 `constructor`가 기본적으로 `Animal`을 가리킵니다.
  - 이를 다시 `Dog`로 명시적으로 설정하여 정확한 생성자 정보를 유지합니다.


**5. Dog의 프로토타입 메서드**</code></pre><p>Dog.prototype.bark = function() {
  console.log(<code>${this.name} says: Woof!</code>);
};</p>
<pre><code>- Dog.prototype에 bark 메서드를 추가합니다.
- 이 메서드는 Dog의 인스턴스에서만 사용 가능한 고유 메서드입니다.

**6. 객체 생성 및 메서드 호출**</code></pre><p>const myDog = new Dog(&#39;Buddy&#39;, &#39;Golden Retriever&#39;);</p>
<pre><code>- `new Dog(&#39;Buddy&#39;, &#39;Golden Retriever&#39;)`
  - 새로운 `Dog` 객체를 생성합니다.
  - `Animal` 생성자를 호출하여 `name` 속성을 `Buddy`로 초기화하고, `breed` 속성을 `Golden Retriever`로 설정합니다.
</code></pre><p>myDog.move(); // Buddy is moving (Animal의 메서드)
myDog.bark(); // Buddy says: Woof! (Dog의 메서드)</p>
<pre><code>
- `myDog.move()`
  - `move` 메서드는 `Animal.prototype`에서 상속받은 메서드입니다.
  - `this.name`은 `myDog` 객체의 `name` 속성`(Buddy)`를 참조합니다.
- `myDog.bark()`
  - `bark` 메서드는 `Dog.prototype`에서 정의된 메서드입니다.
  - `this.name`은 역시 `myDog` 객체의 `name` 속성을 참조합니다.


JavaScript의 프로토타입은 객체 지향 프로그래밍을 이해하는 데 핵심적인 개념이며, 효율적인 코드 작성과 메모리 관리의 기반이 된다.

프로토타입 기반 상속은 클래스 문법으로도 사용되지만, 내부적으로는 동일한 동작 방식을 따르니 이 개념을 정확히 이해하는 것이 중요하다.</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] 콜 스택, 이벤트 루프, (마이크로) 테스크 큐]]></title>
            <link>https://velog.io/@white0_0/JS-%EC%BD%9C-%EC%8A%A4%ED%83%9D-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%ED%85%8C%EC%8A%A4%ED%81%AC-%ED%81%90</link>
            <guid>https://velog.io/@white0_0/JS-%EC%BD%9C-%EC%8A%A4%ED%83%9D-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%ED%85%8C%EC%8A%A4%ED%81%AC-%ED%81%90</guid>
            <pubDate>Thu, 21 Nov 2024 15:18:54 GMT</pubDate>
            <description><![CDATA[<h3 id="콜-스택">콜 스택</h3>
<p>자바스크립트 콜 스택은 자바스크립트 엔진에서 코<strong>드 실행 흐름을 관리하는 메커니즘</strong>으로, 함수 호출과 실행을 추적하는 데 사용됩니다. </p>
<p>콜 스택은 <strong>LIFO(Last In, First Out)</strong>방식으로 작동하는 데이터 구조입니다. 즉, 가장 마지막에 추가된 작업이 가장 먼저 제거됩니다.
<img src="https://velog.velcdn.com/images/white0_0/post/c11d36bd-1f26-467f-9129-8bfcce93ffe7/image.png" alt=""></p>
<p>아래에 코드가 실행되면 콜 스택에는 어떤 변화가 생길까요? </p>
<pre><code>function first() {
  console.log(&quot;First function start&quot;);
  second();
  console.log(&quot;First function end&quot;);
}

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

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

first();</code></pre><p><strong>실행 흐름</strong></p>
<ol>
<li>first() 호출 → first 함수가 콜 스택에 푸시.</li>
<li>console.log 실행 → 메시지 출력.</li>
<li>second() 호출 → second 함수가 콜 스택에 푸시.</li>
<li>third() 호출 → third 함수가 콜 스택에 푸시.</li>
<li>console.log 실행 → 메시지 출력 후 third 함수가 스택에서 팝.</li>
<li>second 함수가 나머지 실행을 마친 후 팝.</li>
<li>first 함수도 실행을 완료한 뒤 팝.</li>
</ol>
<p><strong>콜 스택 변화</strong></p>
<table>
<thead>
<tr>
<th align="left">단계</th>
<th align="left">콜 스택 내용</th>
</tr>
</thead>
<tbody><tr>
<td align="left">초기</td>
<td align="left">(비어 있음)</td>
</tr>
<tr>
<td align="left">1</td>
<td align="left">first</td>
</tr>
<tr>
<td align="left">2</td>
<td align="left">first -&gt; second</td>
</tr>
<tr>
<td align="left">3</td>
<td align="left">first + second -&gt; third</td>
</tr>
<tr>
<td align="left">4</td>
<td align="left">first -&gt; second</td>
</tr>
<tr>
<td align="left">5</td>
<td align="left">first</td>
</tr>
<tr>
<td align="left">6</td>
<td align="left">(비어 있음)</td>
</tr>
</tbody></table>
<p>여기서 확인했듯이 자바스크립트 엔진은 한 번에 하나의 태스크만 실행할 수 있는 <strong>싱글 스레드 방식</strong>으로 동작하기 때문에 단 하나의 실행 컨텍스트 스택(콜 스택)을 갖습니다. </p>
<p>싱글 스레드 방식은 한 번에 하나의 태스크만 실행할 수 있기 때문에 처리에 시간이 걸리는 태스크를 실행하는 경우 <strong>블로킹(작업중단)</strong>이 발생한니다. </p>
<p>아래의 코드를 실행시키면 two 함수는 sleep 함수의 실행이 종료된 이후 호출되므로 3초 이상 호출되지 못하고 블로킹됩니다. </p>
<p>이렇게 현재 실행 중인 태스크가 종료할 때까지 다음에 실행될 테스크가 대기하는 방식을 <strong>동기처리</strong>라고 합니다. 실행 순서가 보장된다는 장점이 있지만, 앞선 태스크가 종료될 때까지 이후 태스크들이 블로킹되는 단점이 존재합니다.</p>
<pre><code>function sleep(func, delay) {
  const delayUntil = Date.now() + delay;

  while (Date.now() &lt; delayUntil);

  func();
}

function one() {
  console.log(&quot;one&quot;);
}

function two() {
  console.log(&quot;two&quot;);
}

sleep(one, 3 * 1000);
two();

// 출력 결과
one
two
</code></pre><p>아래는 sleep 함수와 비슷한 기능을 수행하는 setTimeout 함수를 사용해보겠습니다.
출력 결과를 보면 setTimeout을 사용한 코드는 코드를 블로킹하지 않고 곧바로 실행합니다. 이러첨 현재 실행 중인 태스크가 종료되지 않은 상태라 해도 다음 태스크를 곧바로 실행하는 방식을 <strong>비동기처리</strong>라고 합니다.</p>
<pre><code>function one() {
  console.log(&quot;one&quot;);
}

function two() {
  console.log(&quot;two&quot;);
}

setTimeout(one, 3 * 1000);
two();

// 출력 결과
two
one</code></pre><p>그런데 위에서 <strong>자바스크립트 엔진은 싱글 스레드 방식으로 동작</strong>한다고 했는데 어떻게 <strong>비동기처리</strong>가 가능한걸까요?
이어서 알아보겠습니다. </p>
<p><strong>타이머 함수인 setTimeout과 setInterval, HTTP 요청, 이벤트 핸들러는 비동기 처리 방식으로 동작</strong>합니다. <strong>비동기 처리는 이벤트 루프와 태스크 큐와 깊은 관계</strong>가 있습니다.</p>
<p>앞서 살펴본 것처럼 싱글 스레드 방식은 한 번에 하나의 태스크만 처리할 수 있다는 것을 의미합니다. 하지만 브라우저가 동작하는 것을 보면 많은 태스크가 동시에 처리되는 것처럼 느껴집니다. 프로그램을 다운 받으면서 웹툰을 본다던가 노래를 들으면서 친구와 대화를 나눌수도 있습니다. <strong>이처럼 자바스크립트의 동시성을 지원하는 것이 바로 이벤트 루프입니다. 이벤트 루프는 브라우저에 내장되어 있는 기능 중 하나</strong>입니다.</p>
<h3 id="자바스크립트-런타임-환경">자바스크립트 런타임 환경</h3>
<p><img src="https://velog.velcdn.com/images/white0_0/post/e0e4eb52-4831-41fe-a7ad-a66b39c7c0e1/image.png" alt=""></p>
<p>위 그림에서 Js Engine(자바스크립트 엔진) 부분을 보겠습니다. 자바스크립트 엔진은 크게 2개의 영역으로 구분할 수 있습니다. </p>
<p>** 힙(Heap)**</p>
<p> <strong>설명</strong>: 힙은 자바스크립트 엔진에서 사용하는 메모리 영역으로, 동적으로 할당된 객체와 데이터를 저장하는 공간입니다.</p>
<ul>
<li>동적으로 생성된 변수나 객체(예: 배열, 객체 등)의 메모리 관리를 담당합니다.</li>
</ul>
<ul>
<li><p>힙에 저장된 데이터는 고정된 크기가 아니며,  필요에 따라 크기를 동적으로 조정할 수 있습니다.</p>
</li>
<li><p>가비지 컬렉터(Garbage Collector)에 의해 더 이상 참조되지 않는 데이터는 자동으로 제거됩니다.</p>
</li>
</ul>
<p><strong>콜 스택(Call Stack)</strong></p>
<p><strong>설명</strong>: 콜 스택은 자바스크립트 함수 호출과 실행 순서를 관리하는 LIFO(Last In, First Out) 방식의 데이터 구조입니다.</p>
<ul>
<li><p>함수가 호출되면 해당 함수의 실행 컨텍스트가 스택에 쌓이고, 실행이 완료되면 스택에서 제거됩니다.</p>
</li>
<li><p>코드의 실행 흐름을 제어하며, 동기적으로 작동합니다.</p>
</li>
</ul>
<p>콜 스택과 힙으로 구성되어 있는 자바스크립트 엔진은 단순히 태스크가 요청되면 콜 스택을 통해 요청된 작업을 순차적으로 실행할 뿐입니다. 비동기 처리에서 <strong>소스코드의 평가와 실행을 제외한 모든 처리는 자바스크립트 엔진을 구동하는 환경인 브라우저 또는 Node.js가 담당</strong>합니다. 예를 들어, <strong>비동기 방식으로 동작하는 setTimeout의 콜백 함수의 평가와 실행은 자바스크립트 엔진이 담당</strong>하지만 <strong>호출 스케줄링을 위한 타이머 설정과 콜백 함수의 등록은 브라우저 또는 Node.js가 담당</strong>한다. 이를 위해 브라우저 환경은 태스크 큐와 이벤트 루프를 제공합니다. </p>
<h3 id="태스크-큐와-이벤트-루프">태스크 큐와 이벤트 루프</h3>
<p><strong>태스크 큐(Task Queue)</strong></p>
<ul>
<li><p>태스크 큐는 브라우저에서 실행 대기 중인 비<strong>동기 작업의 콜백 함수를 저장</strong>하는 대기열입니다.</p>
</li>
<li><p>브라우저가 제공하는 Web APIs(ex. <code>setTimeout</code>, <code>setInterval</code>, <code>DOM Events</code> 등)를 통해 비동기로 처리해야 할 작업이 완료되면, 해당 작업의 콜백 함수가 <strong>태스크 큐</strong>에 추가됩니다.</p>
</li>
</ul>
<p><strong>동작 방식</strong></p>
<ol>
<li><p>브라우저는 특정 비동기 작업(ex. 타이머, 이벤트 리스너)의 실행이 완료되면 그 콜백을 태스크 큐에 넣습니다.</p>
</li>
<li><p>태스크 큐에 담긴 함수들은 <strong>콜 스택이 비었을 때</strong> 실행됩니다. (즉, 현재 실행 중인 코드가 끝난 후 실행)</p>
</li>
</ol>
<pre><code>console.log(&#39;1&#39;);

setTimeout(() =&gt; {
  console.log(&#39;2&#39;); // 태스크 큐에 들어감
}, 0);

console.log(&#39;3&#39;);

// 출력 결과
1
3
2</code></pre><p><code>setTimeout</code>의 콜백은 태스크 큐에 추가되므로 <code>1, 3</code>이 <strong>먼저 출력</strong>되고, 이후 콜 스택이 비었을 때 <code>2</code>가 출력됩니다.</p>
<p><strong>이벤트 루프(Event Loop)</strong></p>
<ul>
<li><p>이벤트 루프는 자바스크립트 런타임의 핵심 메커니즘으로, <strong>콜 스택(Call Stack)</strong>과 <strong>태스크 큐(Task Queue)</strong>를 연결하여 비동기 작업을 관리합니다.</p>
</li>
<li><p>이벤트 루프는 계속 실행되면서 다음을 반복합니다:
콜 스택이 비어 있는지 확인.
비어 있다면, 태스크 큐에서 대기 중인 작업을 가져와 콜 스택에 추가하여 실행.</p>
</li>
</ul>
<p><strong>동작 방식</strong></p>
<ol>
<li>자바스크립트는 싱글 스레드로 작동하며, 콜 스택에 작업을 하나씩 처리합니다.</li>
</ol>
<ol start="2">
<li><p>동기 코드는 즉시 실행되지만, 비동기 코드는 Web APIs에서 처리된 후 태스크 큐에 대기합니다.</p>
</li>
<li><p>이벤트 루프가 태스크 큐를 확인하여, 콜 스택이 비었을 경우 태스크 큐의 작업을 실행합니다.</p>
</li>
</ol>
<h3 id="마이크로-태스큐-큐">마이크로 태스큐 큐</h3>
<p>마이크로 태스크 큐(Microtask Queue)는 프로그래밍, 특히 자바스크립트와 같은 이벤트 기반 언어에서 비동기 작업을 처리하는 데 중요한 역할을 하는 개념입니다. 이는 <strong>Event Loop</strong>의 일부로, 태스크를 관리하고 실행 순서를 제어하는 메커니즘입니다.</p>
<p><strong>주요 특징</strong></p>
<ol>
<li><p><strong>작업의 우선 순위</strong>
마이크로 태스크 큐는 콜 스택 큐보다 우선 실행됩니다. </p>
</li>
<li><p><strong>처리 순서</strong></p>
</li>
</ol>
<ul>
<li>이벤트 루프에서 현재 실행 중인 태스크가 완료되면, 먼저 마이크로 태스크 큐에 있는 모든 작업이 처리됩니다.</li>
<li>이후에 콜 스택 큐 큐로 넘어갑니다.</li>
</ul>
<ol start="3">
<li><strong>주요 예</strong> 
다음은 마이크로 태스크 큐를 사용하는 주요 비동기 작업입니다.</li>
</ol>
<ul>
<li>Promise.then(), Promise.catch()</li>
<li>MutationObserver</li>
<li>queueMicrotask() (명시적으로 큐에 추가하는 방법)</li>
</ul>
<p><strong>아래 코드에 실행 결과를 스스로 예측해봅시다.</strong></p>
<pre><code>Promise.resolve().then(() =&gt; console.log(1));

setTimeout(() =&gt; console.log(2), 10);

queueMicrtask(() =&gt; {
  console.log(3)
  queueMicrotask(() =&gt; console.log(4))
});

console.log(5)</code></pre><p><strong>실행 과정</strong></p>
<ol>
<li><strong>초기 작업</strong>: 모든 명령은 처음에는 <strong>콜 스택</strong>에 들어가 실행됩니다.</li>
</ol>
<ul>
<li><code>Promise.resolve()</code> → 마이크로태스크 큐에 <code>.then</code> 추가</li>
<li><code>setTimeout()</code> → 10ms 후 실행될 콜백을 태스크 큐에 추가</li>
<li><code>queueMicrotask()</code> → 마이크로태스크 큐에 작업 추가</li>
<li><code>console.log(5)</code> → 즉시 실행</li>
</ul>
<ol start="2">
<li><p>** 콜 스택 실행** 후: 메인 스택이 비면** 마이크로태스크가 실행**됩니다. 마이크로태스크는 큐에서 FIFO로 실행되며, 실행 중 새로운 마이크로태스크가 추가되면 즉시 큐에 들어갑니다.</p>
</li>
<li><p><strong>매크로태스크 처리</strong>: 마이크로태스크가 모두 처리된 후, 매크로태스크가 실행됩니다.</p>
</li>
</ol>
<p><strong>실행 순서 (콜 스택 기준)</strong></p>
<p><strong>1단계: 초기 실행 (콜 스택에서 처리)</strong></p>
<ul>
<li><code>Promise.resolve().then(...)</code> → 마이크로태스크 큐에 추가</li>
<li><code>setTimeout(...)</code> → 태스큐 큐에 추가</li>
<li><code>queueMicrotask(...)</code> → 마이크로태스크 큐에 추가</li>
<li><code>console.log(5)</code> → 출력: 5</li>
</ul>
<p><strong>2단계: 마이크로태스크 큐 처리</strong></p>
<ol>
<li><p><code>Promise.then(...)</code> 실행 → 출력: 1</p>
</li>
<li><p>첫 번째 <code>queueMicrotask(...)</code> 실행 → 출력: 3</p>
<ul>
<li>이때, 내부의 <code>queueMicrotask(...)</code>가 또 마이크로태스크 큐에 추가됩니다.</li>
</ul>
</li>
<li><p>두 번째 <code>queueMicrotask(...)</code> 실행 → 출력: 4</p>
</li>
</ol>
<p><strong>3단계: 태스크 큐 처리</strong></p>
<ul>
<li>setTimeout(...) 실행 → 출력: 2</li>
</ul>
<p><strong>최종 출력</strong></p>
<pre><code>5
1
3
4
2
</code></pre><p>이번 글을 통해 자바스크립트의** 비동기 처리, 이벤트 루프, 그리고 콜 스택의 작동 원리**에 대해 알아보았습니다. 이러한 기초 지식은 복잡한 비동기 로직을 작성하거나 디버깅할 때 큰 도움이 된다고 생각합니다. 자바스크립트 런타임 환경과 이벤트 루프의 작동 방식을 깊이 이해하면 더 효율적이고 안정적인 코드를 작성할 수 있습니다. 비동기 코드에서 발생할 수 있는 문제를 예방하려면 이 원리를 숙지해야 합니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 성능 테스트: 로컬과 배포 환경에서 LCP 개선하기]]></title>
            <link>https://velog.io/@white0_0/Next.js-%EC%84%B1%EB%8A%A5-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%A1%9C%EC%BB%AC%EA%B3%BC-%EB%B0%B0%ED%8F%AC-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-LCP-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@white0_0/Next.js-%EC%84%B1%EB%8A%A5-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%A1%9C%EC%BB%AC%EA%B3%BC-%EB%B0%B0%ED%8F%AC-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-LCP-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 12 Nov 2024 15:36:56 GMT</pubDate>
            <description><![CDATA[<p>팀 프로젝트가 마무리 단계에 접어 들면서 코드도 실제 배포 환경에서 에러가 발생하는 부분을 고치고 리팩토링하면서 무엇을 더 할 수 있는게 생각하다가 최적화에 대해서 궁금해졌다. </p>
<p>Next.js 공식 문서에서 프로덕션에 배포하기 전에, <code>next build</code> 명령어를 사용해 로컬에서 애플리케이션을 빌드하고 빌드 오류가 있는지 확인할 수 있고, <code>next start</code> 명령어로 실제 프로덕션 환경과 유사한 상태에서 애플리케이션 성능을 측정할 수 있다고 한다.</p>
<p>웹 성능을 측정하기 위해 Lighthouse를 사용할 것이다. <code>Lighthouse</code>란 <strong>웹 사이트의 성능, 접근성, SEO, 그리고 웹 애플리케이션의 최적화 상태를 자동으로 분석해주는 오픈 소스 도구</strong>이다.
<a href="https://developer.chrome.com/docs/lighthouse/overview?hl=ko">LIghthouse 설치 및 실행 설명 링크</a></p>
<p>이번 글에서는 세부 측정 항목과 항목들에 대한 내용들에 대해서는 다루지 않고 실제 내 페이지에서 LightHouse를 실행했을 때의 결과를 토대로 어떻게 측정 점수를 올렸는지 적어보겠다.</p>
<p>일단 <strong>npm run dev로 개발모드</strong>를 실행시켜서 개발자 도구에서 console탭을 눌렀을 때 메시지를  확인하겠다. 
부분은 홈 페이지 경로에 대한 모임 목록들인데 경고메시지를 보겠다.</p>
<p><img src="https://velog.velcdn.com/images/white0_0/post/45a2de61-50ca-4d05-bb07-e056e4d55f82/image.png" alt="">
<img src="https://velog.velcdn.com/images/white0_0/post/d5ef8175-cb27-4f12-b245-61ce67e51305/image.jpg" alt="">
<code>LCP</code>라는 메시지만 기억하고 아래 <code>LightHouse</code> 실행결과를 이어서 확인하자.
참고로 <code>npm run build</code>로 빌드하고 <code>npm run start</code>로 실행한 뒤 <code>시크릿 모드</code>로 접속하여 LightHouse를 실행하자!</p>
<p><strong>시크릿 모드에서 진행하는 이유!</strong></p>
<ol>
<li><p><strong>브라우저 확장 프로그램의 간섭 방지</strong>: 설치된 확장 프로그램들이 <code>Lighthouse</code>의 성능 측정에 영향을 줄 수 있다. 예를 들어, 광고 차단기나 스크립트 차단 확장 프로그램이 페이지 로딩 속도나 성능 평가에 부정확한 결과를 초래할 수 있
다.</p>
</li>
<li><p><strong>로그인 및 캐시 배제</strong>: 비밀모드는 캐시나 로그인 정보가 저장되지 않기 때문에, 실제 사용자가 처음 웹사이트에 접속하는 환경과 비슷한 조건에서 테스트를 진행할 수 있다. 캐시된 데이터나 로그인 세션이 있으면 <code>Lighthouse</code> 테스트 결과가 실제와 다르게 나올 수 있으므로, 비밀모드를 사용하는 것이 더 권장된다.</p>
</li>
</ol>
<p><a href="https://nextjs.org/docs/app/building-your-application/deploying/production-checklist#before-going-to-production">시크릿 모드 권고 링크1</a>
<a href="https://nextjs.org/learn-pages-router/seo/improve/lighthouse">시크릿 모드 권고 링크2</a></p>
<h3 id="lighthouse-실행-및-결과">Lighthouse 실행 및 결과</h3>
<p>시크릿 모드에서 개발자 도구를 열고 <strong>Device는 Desktop</strong>을 클릭하고 우측 상단에 <strong>Analyze page load</strong> 버튼을 클릭한다. (1분 정도 소요되는 것 같다.)
<img src="https://velog.velcdn.com/images/white0_0/post/c5c9fbdb-a70e-4f0b-8df9-241f191c432c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/white0_0/post/6299d5fa-2de8-4fe4-911a-0cfa7691a61c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/white0_0/post/6ca8abfc-82c0-487b-843f-2d773038c9ff/image.jpg" alt=""></p>
<p>우선 LCP는 최대 콘텐츠 렌더링 시간을 나타내고 보고서의 성능(Performance) 섹션에서 추적되는 측정항목 중 하나이다. 구체적으로 LCP는 사용자가 페이지 로드를 시작한 시점부터 표시 영역 내에서 가장 큰 이미지나 텍스트 블록이 렌더링될 때까지의 시간을 측정한다.</p>
<p><a href="https://developer.chrome.com/docs/lighthouse/performance/lighthouse-largest-contentful-paint?utm_source=lighthouse&amp;utm_medium=devtools&amp;hl=ko">LCP 설명 링크</a></p>
<p>우선 LCP를 구성하는 하위 4가지 카테고리를 알아보자.
<img src="https://velog.velcdn.com/images/white0_0/post/63e10b96-a073-4302-8665-2592e321ed4a/image.png" alt=""></p>
<ol>
<li><strong>첫 바이트까지의 시간(TTFB)</strong> </li>
</ol>
<ul>
<li><p>사용자가 페이지 로드를 시작한 시점부터 브라우저가 실행될 때까지의 시간 </p>
</li>
<li><p>HTML 문서 응답의 첫 바이트를 수신</p>
</li>
</ul>
<ol start="2">
<li><strong>리소스 로드 지연</strong></li>
</ol>
<ul>
<li><p><strong>서버에서 첫 번째 응답이 도착한 후</strong>, 브라우저가 <strong>LCP리소스</strong>(이미지나 폰트 같은 주요 리소스)를 <strong>로드하기 시작할 때까지 걸리는 시간</strong></p>
</li>
<li><p>즉,** 리소스를 로드하는 준비가 되기까지의 대기 **시간</p>
</li>
</ul>
<ol start="3">
<li><strong>리소스 로드 시간</strong></li>
</ol>
<ul>
<li><p>브라우저가 <strong>LCP 리소스를 실제로 다운로드하는 데 걸리는 시간</strong></p>
</li>
<li><p>리소스 로드 지연 후, 브라우저가 해당 리소스를 로드하기 시작하면, 서버로부터 그 리소스를 가져오는 데 걸리는 시간이 리소스 로드 시간</p>
</li>
<li><p>예를 들어, 이미지 파일이나 폰트 파일을 서버에서 다운받는 시간이 여기에 해당</p>
</li>
</ul>
<ol start="4">
<li><strong>렌더링 지연</strong></li>
</ol>
<ul>
<li>웹페이지의 특정 요소나 콘텐츠가 화면에 보이기 전까지 발생하는 시간 지연을 의미</li>
</ul>
<p><img src="https://velog.velcdn.com/images/white0_0/post/c50ecdda-b890-4ccc-bc2d-b4602a1a4b9e/image.png" alt="">
<a href="https://web.dev/articles/optimize-lcp?hl=ko#2_eliminate_element_render_delay">이미지 참고 및 4가지  카테고리 링크</a></p>
<p>다시 위에 처음 콘손에 나온 메시지를 보자.
<strong>&quot;Please add the &quot;priority&quot; property if this image is above the fold&quot;</strong></p>
<p><strong>&quot;above the fold&quot;란?</strong></p>
<ul>
<li><p><strong>above the fold</strong>는 사용자가 페이지를 열었을 때 <strong>스크롤하지 않아도 바로 보이는 영역을 의미</strong>한다. 즉, 화면 상단에 위치한 중요한 콘텐츠들이다.</p>
</li>
<li><p>반대로 <strong>below the fold</strong>는 스크롤해야 보이는 영역</p>
</li>
</ul>
<p><strong>&quot;priority&quot; 속성이란?</strong></p>
<ul>
<li><code>priority</code> 속성은 Next.js에서 이미지를 <strong>우선적으로 로드</strong>하라고 지시하는 역할을 한다. 페이지의 로딩 성능을 최적화하는 데 중요한 요소다.</li>
</ul>
<p><a href="https://nextjs.org/docs/pages/api-reference/components/image#priority">Next.js image 컴포넌트 priority 속성</a></p>
<p><strong>메시지의 의미</strong></p>
<p>이미지가 <strong>above the fold</strong>에 위치해 있기 때문에, 사용자가 페이지를 로드하자마자 바로 보이는 중요한 이미지라는 것을 뜻한다. 따라서 <code>priority</code> 속성을 추가하여 해당 이미지를 빠르게 로드하게 함으로써 페이지의 첫 화면 렌더링 속도를 개선하라는 제안이다.</p>
<p><strong>적용 코드</strong>
첫 번째 이미지에서 priority 속성을 추가하라는 메시지가 나타났기 때문에 index 값이 0인(첫 번째 요소) 경우에 priority 프롭스를 boolean 값으로 ProgressCard 컴포넌트에 전달해주었다. </p>
<pre><code>     {data.pages.flat().map((gathering, index) =&gt; (
       &lt;ProgressCard key={gathering.id} gathering={gathering} priority={index === 0} /&gt;
     ))}
</code></pre><h3 id="progresscard-컴포넌트-코드-일부분">ProgressCard 컴포넌트 코드 일부분</h3>
<pre><code>  &lt;Image
    src={gathering.image || &#39;/card-image2.png&#39;}
    alt=&quot;Image&quot;
    fill
    className=&quot;object-cover rounded-t-3xl md:rounded-t-none&quot;
    priority={priority}
  /&gt;</code></pre><h3 id="실제-베포-환경-최종-lighthouse-결과">실제 베포 환경 최종 Lighthouse 결과</h3>
<p><img src="https://velog.velcdn.com/images/white0_0/post/a7767016-dd18-4bf9-ae61-d1743efcffb4/image.png" alt=""></p>
<p>이렇게 보는 거와 같이 최종 결과는 performance 점수가 크게 상향하였고 <code>LCP</code> 응답 시간이 2.1s에서 0.3s로 감소하였다. 결론적으로 <code>priority</code> 속성만 추가하면 해결된 부분이였지만 이 과정 속에서 LCP가 무엇인지 어떻게 구성되어있는지 해결방법에 어떻게 접근했는지 배워간 부분이 많다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[번역]Next.js Authentication]]></title>
            <link>https://velog.io/@white0_0/%EB%B2%88%EC%97%ADNext.js-Authentication</link>
            <guid>https://velog.io/@white0_0/%EB%B2%88%EC%97%ADNext.js-Authentication</guid>
            <pubDate>Thu, 31 Oct 2024 03:56:54 GMT</pubDate>
            <description><![CDATA[<h3 id="인증authentication">인증(Authentication)</h3>
<p>인증에 대한 이해는 애플리케이션의 데이터를 보호하는 데 매우 중요합니다. 이 페이지에서는 인증을 구현할 때 React와 Next.js의 어떤 기능을 사용해야 하는지 안내합니다.</p>
<p>시작하기 전에, 과정을 세 가지 개념으로 나눠보면 도움이 됩니다.</p>
<ol>
<li><p><strong>인증 (Authentication)</strong>: 사용자가 본인이 맞는지 확인하는 과정으로, 주로 사용자 이름과 비밀번호 같은 정보로 신원을 증명합니다.</p>
</li>
<li><p><strong>세션 관리(Session Management)</strong>: 여러 요청 간에 사용자의 인증 상태를 유지하고 추적합니다.</p>
</li>
<li><p><strong>권한 부여(Authorization)</strong>: 사용자가 접근할 수 있는 경로와 데이터를 결정합니다.</p>
</li>
</ol>
<p>이 다이어그램은 React와 Next.js 기능을 사용한 인증 흐름을 보여줍니다.</p>
<p><img src="https://velog.velcdn.com/images/white0_0/post/6e1b8583-a953-43fc-92e7-9b4694b55790/image.png" alt=""></p>
<p>이 페이지의 예제에서는 교육적인 목적을 위해 기본적인 사용자 이름과 비밀번호 인증 과정을 설명합니다. 사용자 맞춤 인증 솔루션을 구현할 수도 있지만, 보안과 간편함을 위해 <strong>인증 라이브러리 사용을 권장</strong>합니다. 이러한 라이브러리는 인증, 세션 관리, 권한 부여를 위한 내장 솔루션을 제공하며, 소셜 로그인, 다단계 인증, 역할 기반 접근 제어와 같은 추가 기능도 포함되어 있습니다. 인증 라이브러리 목록은 ‘<a href="https://nextjs.org/docs/app/building-your-application/authentication#auth-libraries">인증 라이브러리</a>’ 섹션에서 확인할 수 있습니다.</p>
<h3 id="가입-및-로그인-기능">가입 및 로그인 기능</h3>
<p>React의 서버 액션과 <code>useFormState</code>를 활용하여 <code>&lt;form&gt;</code> 요소를 사용하여 사용자 자격 증명을 캡처하고, 폼 필드를 검증하며, 인증 공급자의 API 또는 데이터베이스를 호출할 수 있습니다.</p>
<p>서버 액션은 항상 서버에서 실행되므로 인증 로직을 처리하는 안전한 환경을 제공합니다.</p>
<p>다음은 회원가입/로그인 기능을 구현하는 단계입니다:</p>
<ol>
<li><strong>사용자 자격 증명 입력 받기</strong>
사용자 자격 증명을 입력받기 위해, 제출 시 서버 액션을 호출하는 폼을 만듭니다. 예를 들어, 사용자의 이름, 이메일, 비밀번호를 입력받는 회원가입 폼은 다음과 같습니다:</li>
</ol>
<p><strong>app/ui/signup-form.tsx</strong></p>
<pre><code>import { signup } from &#39;@/app/actions/auth&#39;

export function SignupForm() {
  return (
    &lt;form action={signup}&gt;
      &lt;div&gt;
        &lt;label htmlFor=&quot;name&quot;&gt;Name&lt;/label&gt;
        &lt;input id=&quot;name&quot; name=&quot;name&quot; placeholder=&quot;Name&quot; /&gt;
      &lt;/div&gt;
      &lt;div&gt;
        &lt;label htmlFor=&quot;email&quot;&gt;Email&lt;/label&gt;
        &lt;input id=&quot;email&quot; name=&quot;email&quot; type=&quot;email&quot; placeholder=&quot;Email&quot; /&gt;
      &lt;/div&gt;
      &lt;div&gt;
        &lt;label htmlFor=&quot;password&quot;&gt;Password&lt;/label&gt;
        &lt;input id=&quot;password&quot; name=&quot;password&quot; type=&quot;password&quot; /&gt;
      &lt;/div&gt;
      &lt;button type=&quot;submit&quot;&gt;Sign Up&lt;/button&gt;
    &lt;/form&gt;
  )
}</code></pre><p><strong>app/actions/auth.tsx</strong></p>
<pre><code>export async function signup(formData: FormData) {}</code></pre><ol start="2">
<li><strong>서버에서 폼 필드 검증</strong>
서버에서 폼 필드를 검증하기 위해 서버 액션을 사용합니다. 만약 인증 공급자가 폼 검증을 제공하지 않는다면, <a href="https://zod.dev/">Zod</a>나 <a href="https://github.com/jquense/yup">Yup</a>과 같은 스키마 검증 라이브러리를 사용할 수 있습니다.</li>
</ol>
<p>예를 들어, <code>Zod</code>를 사용하여 적절한 오류 메시지와 함께 폼 스키마를 정의할 수 있습니다:</p>
<p><strong>app/lib/definitions.ts</strong></p>
<pre><code>import { z } from &#39;zod&#39;

export const SignupFormSchema = z.object({
  name: z
    .string()
    .min(2, { message: &#39;Name must be at least 2 characters long.&#39; })
    .trim(),
  email: z.string().email({ message: &#39;Please enter a valid email.&#39; }).trim(),
  password: z
    .string()
    .min(8, { message: &#39;Be at least 8 characters long&#39; })
    .regex(/[a-zA-Z]/, { message: &#39;Contain at least one letter.&#39; })
    .regex(/[0-9]/, { message: &#39;Contain at least one number.&#39; })
    .regex(/[^a-zA-Z0-9]/, {
      message: &#39;Contain at least one special character.&#39;,
    })
    .trim(),
})

export type FormState =
  | {
      errors?: {
        name?: string[]
        email?: string[]
        password?: string[]
      }
      message?: string
    }
  | undefined</code></pre><p>인증 공급자의 API나 데이터베이스에 불필요한 호출을 방지하기 위해, 정의된 스키마와 일치하지 않는 폼 필드가 있을 경우 서버 액션에서 조기에 반환할 수 있습니다.</p>
<p><strong>app/actions/auth.ts</strong></p>
<pre><code>import { SignupFormSchema, FormState } from &#39;@/app/lib/definitions&#39;

export async function signup(state: FormState, formData: FormData) {
  // Validate form fields
  const validatedFields = SignupFormSchema.safeParse({
    name: formData.get(&#39;name&#39;),
    email: formData.get(&#39;email&#39;),
    password: formData.get(&#39;password&#39;),
  })

  // If any form fields are invalid, return early
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }

  // Call the provider or db to create a user...
}</code></pre><p>다시 <code>&lt;SignupForm /&gt;</code>으로 돌아가서, React의 <code>useFormState</code> 훅을 사용하여 폼 제출 중에 검증 오류를 표시할 수 있습니다</p>
<p><strong>app/ui/signup-form.tsx</strong></p>
<pre><code>&#39;use client&#39;

import { useFormState, useFormStatus } from &#39;react-dom&#39;
import { signup } from &#39;@/app/actions/auth&#39;

export function SignupForm() {
  const [state, action] = useFormState(signup, undefined)

  return (
    &lt;form action={action}&gt;
      &lt;div&gt;
        &lt;label htmlFor=&quot;name&quot;&gt;Name&lt;/label&gt;
        &lt;input id=&quot;name&quot; name=&quot;name&quot; placeholder=&quot;Name&quot; /&gt;
      &lt;/div&gt;
      {state?.errors?.name &amp;&amp; &lt;p&gt;{state.errors.name}&lt;/p&gt;}

      &lt;div&gt;
        &lt;label htmlFor=&quot;email&quot;&gt;Email&lt;/label&gt;
        &lt;input id=&quot;email&quot; name=&quot;email&quot; placeholder=&quot;Email&quot; /&gt;
      &lt;/div&gt;
      {state?.errors?.email &amp;&amp; &lt;p&gt;{state.errors.email}&lt;/p&gt;}

      &lt;div&gt;
        &lt;label htmlFor=&quot;password&quot;&gt;Password&lt;/label&gt;
        &lt;input id=&quot;password&quot; name=&quot;password&quot; type=&quot;password&quot; /&gt;
      &lt;/div&gt;
      {state?.errors?.password &amp;&amp; (
        &lt;div&gt;
          &lt;p&gt;Password must:&lt;/p&gt;
          &lt;ul&gt;
            {state.errors.password.map((error) =&gt; (
              &lt;li key={error}&gt;- {error}&lt;/li&gt;
            ))}
          &lt;/ul&gt;
        &lt;/div&gt;
      )}
      &lt;SubmitButton /&gt;
    &lt;/form&gt;
  )
}

function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    &lt;button disabled={pending} type=&quot;submit&quot;&gt;
      Sign Up
    &lt;/button&gt;
  )
}</code></pre><p><strong>참고 사항</strong></p>
<ul>
<li><p>이 예제에서는 Next.js 앱 라우터에 포함된 React의 <code>useFormState</code> 훅을 사용합니다. React 19를 사용 중이라면 <code>useActionState</code>를 대신 사용하세요. 자세한 내용은 <a href="https://react.dev/reference/react/useActionState">React</a> 문서를 참조하세요.</p>
</li>
<li><p>React 19에서는 <code>useFormStatus</code>가 반환하는 객체에 <code>data</code>, <code>method</code>, <code>action</code>과 같은 추가 키가 포함됩니다. React 19가 아닌 경우에는 <code>pending</code> 키만 사용할 수 있습니다. 또한 React 19에서는 <code>useActionState</code>가 반환된 상태에 <code>pending</code> 키도 포함합니다.</p>
</li>
<li><p>데이터를 수정하기 전에 사용자가 해당 작업을 수행할 권한이 있는지도 항상 확인해야 합니다. 자세한 내용은 <a href="https://nextjs.org/docs/app/building-your-application/authentication#authorization">&#39;인증 및 권한 부여&#39;</a>를 참조하세요.</p>
</li>
</ul>
<ol start="3">
<li><strong>사용자 생성 또는 사용자 자격 증명 확인</strong>
폼 필드를 검증한 후, 인증 공급자의 API나 데이터베이스를 호출하여 새로운 사용자 계정을 생성하거나 사용자가 존재하는지 확인할 수 있습니다.</li>
</ol>
<p>이전 예제에서 이어서 설명</p>
<p><strong>app/actions/auth.ts</strong></p>
<pre><code>export async function signup(state: FormState, formData: FormData) {
  // 1. Validate form fields
  // ...

  // 2. Prepare data for insertion into database
  const { name, email, password } = validatedFields.data
  // e.g. Hash the user&#39;s password before storing it
  const hashedPassword = await bcrypt.hash(password, 10)

  // 3. Insert the user into the database or call an Auth Library&#39;s API
  const data = await db
    .insert(users)
    .values({
      name,
      email,
      password: hashedPassword,
    })
    .returning({ id: users.id })

  const user = data[0]

  if (!user) {
    return {
      message: &#39;An error occurred while creating your account.&#39;,
    }
  }

  // TODO:
  // 4. Create user session
  // 5. Redirect user
}</code></pre><p>사용자 계정을 성공적으로 생성하거나 자격 증명을 확인한 후, 사용자의 인증 상태를 관리하기 위한 세션을 생성할 수 있습니다. 세션 관리 방식에 따라 세션은 쿠키나 데이터베이스, 또는 두 곳 모두에 저장될 수 있습니다. 더 자세한 내용은<a href="https://nextjs.org/docs/app/building-your-application/authentication#session-management"> &#39;세션 관리&#39;</a> 섹션에서 확인하세요.</p>
<p><strong>팁</strong></p>
<ul>
<li><p>위 예제는 교육 목적으로 인증 단계를 상세히 설명하여 다소 장황해 보일 수 있습니다. 이를 통해 안전한 인증 솔루션을 직접 구현하는 과정이 얼마나 복잡해질 수 있는지 강조하고 있으며, <a href="https://nextjs.org/docs/app/building-your-application/authentication#auth-libraries">인증 라이브러리</a>를 사용하여 과정을 단순화하는 것도 고려해 보세요.</p>
</li>
<li><p>또한 사용자 경험을 개선하기 위해, 회원가입 단계 초기에 중복 이메일이나 사용자 이름을 미리 확인하는 것이 좋습니다. 예를 들어, 사용자가 사용자 이름을 입력하는 중이거나 입력란이 포커스를 잃을 때 확인을 수행할 수 있습니다. 이를 통해 불필요한 폼 제출을 방지하고 즉각적인 피드백을 제공할 수 있습니다. 요청 빈도 관리는 <a href="https://www.npmjs.com/package/use-debounce">use-debounce</a>와 같은 라이브러리를 사용하여 디바운스 처리로 가능합니다.</p>
</li>
</ul>
<h3 id="세션-관리">세션 관리</h3>
<p>세션 관리는 사용자 인증 상태가 여러 요청 간에 유지되도록 보장하는 역할을 합니다. 여기에는 세션이나 토큰을 생성하고, 저장하고, 갱신하고, 삭제하는 과정이 포함됩니다.</p>
<p>세션에는 두 가지 유형이 있습니다.</p>
<ol>
<li><strong>무상태(Stateless)</strong>: 세션 데이터(또는 토큰)가 브라우저의 쿠키에 저장됩니다. 이 쿠키는 요청마다 서버로 전송되어 세션을 검증할 수 있도록 합니다. 이 방식은 비교적 간단하지만, 올바르게 구현되지 않으면 보안이 취약할 수 있습니다.</li>
</ol>
<ol start="2">
<li><strong>데이터 베이스(Database)</strong>: 세션 데이터가 데이터베이스에 저장되고, 사용자의 브라우저에는 암호화된 세션 ID만 전달됩니다. 이 방식은 더 안전하지만, 구현이 복잡하고 서버 자원을 더 많이 사용할 수 있습니다.</li>
</ol>
<p><strong>참고 사항</strong>
두 가지 방식 중 하나 또는 둘 다 사용할 수 있지만, <a href="https://github.com/vvo/iron-session">iron-session</a>이나 <a href="https://github.com/panva/jose">Jose</a>와 같은 세션 관리 라이브러리를 사용하는 것을 권장합니다.</p>
<h3 id="무상태stateless-세션">무상태(Stateless) 세션</h3>
<p>무상태 세션을 생성하고 관리하려면 다음 단계들을 따라야 합니다.</p>
<ol>
<li><p>세션 서명에 사용할 비밀 키를 생성하고, <a href="https://nextjs.org/docs/app/building-your-application/configuring/environment-variables">환경 변수</a>로 저장합니다.</p>
</li>
<li><p>세션 관리 라이브러리를 사용해 세션 데이터를 암호화/복호화하는 로직을 작성합니다.</p>
</li>
<li><p>Next.js의 쿠키 API를 사용하여 <a href="https://nextjs.org/docs/app/api-reference/functions/cookies">쿠키</a>를 관리합니다.</p>
</li>
</ol>
<p>위 내용에 더해, 사용자가 애플리케이션으로 돌아올 때 세션을 <a href="https://nextjs.org/docs/app/building-your-application/authentication#updating-or-refreshing-sessions">갱신</a>하는 기능과, 로그아웃 시 세션을 <a href="https://nextjs.org/docs/app/building-your-application/authentication#deleting-the-session">삭제</a>하는 기능도 추가하는 것을 고려해 보세요.</p>
<p><strong>참고사항</strong>
인증 라이브러리에 세션 관리가 포함되어 있는지 확인하세요.</p>
<ol>
<li><p><strong>비밀 키 생성</strong></p>
<p>비밀 키 생성 세션을 서명하기 위해 비밀 키를 생성하는 방법에는 여러 가지가 있습니다. 예를 들어, 터미널에서 openssl 명령어를 사용하여 생성할 수 있습니다</p>
</li>
</ol>
<p>** openssl rand -base64 32**</p>
<p>이 명령어는 32자 길이의 랜덤 문자열을 생성하며, 이를 비밀 키로 사용하고 환경 변수 파일에 저장할 수 있습니다.</p>
<p><strong>.env</strong></p>
<pre><code>SESSION_SECRET=your_secret_key``</code></pre><p>그런 다음 세션 관리 로직에서 이 키를 참조할 수 있습니다.
<strong>app/lib/session.js</strong></p>
<pre><code>const secretKey = process.env.SESSION_SECRET</code></pre><ol start="2">
<li><p><strong>세션 암호화 및 복호화</strong></p>
<p>다음으로, 선호하는 세션 관리 라이브러리를 사용하여 세션을 암호화하고 복호화할 수 있습니다. 이전 예제에서 이어서, <a href="https://nextjs.org/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes"><code>Edge Runtime</code></a>과 호환되는 <code>Jose</code>와 React의 <a href="https://www.npmjs.com/package/server-only">서버 전용</a> 패키지를 사용하여 세션 관리 로직이 서버에서만 실행되도록 합니다.&quot;</p>
</li>
</ol>
<p>** app/lib/session.ts**</p>
<pre><code>import &#39;server-only&#39;
import { SignJWT, jwtVerify } from &#39;jose&#39;
import { SessionPayload } from &#39;@/app/lib/definitions&#39;

const secretKey = process.env.SESSION_SECRET
const encodedKey = new TextEncoder().encode(secretKey)

export async function encrypt(payload: SessionPayload) {
  return new SignJWT(payload)
    .setProtectedHeader({ alg: &#39;HS256&#39; })
    .setIssuedAt()
    .setExpirationTime(&#39;7d&#39;)
    .sign(encodedKey)
}

export async function decrypt(session: string | undefined = &#39;&#39;) {
  try {
    const { payload } = await jwtVerify(session, encodedKey, {
      algorithms: [&#39;HS256&#39;],
    })
    return payload
  } catch (error) {
    console.log(&#39;Failed to verify session&#39;)
  }
}</code></pre><p><strong>팁</strong>
페이로드에는 이후 요청에서 사용될 최소한의 고유 사용자 데이터, 예를 들어 사용자 ID, 역할 등을 포함해야 합니다. 개인 식별 정보(전화번호, 이메일 주소, 신용카드 정보 등)나 비밀번호와 같은 민감한 데이터는 포함해서는 안 됩니다.</p>
<ol start="3">
<li><p><strong>쿠키 설정(권장 옵션)</strong></p>
<p>세션을 쿠키에 저장하려면 Next.js의 쿠키 API를 사용합니다. 쿠키는 서버에서 설정해야 하며, 다음과 같은 권장 옵션을 포함해야 합니다.</p>
</li>
</ol>
<ul>
<li><p><strong>HttpOnly</strong>: 클라이언트 측 JavaScript가 쿠키에 접근하는 것을 방지합니다.</p>
</li>
<li><p><strong>Secure</strong>: 쿠키를 전송할 때 HTTPS를 사용합니다.</p>
</li>
<li><p><strong>SameSite</strong>: 쿠키가 교차 사이트 요청과 함께 전송될 수 있는지를 지정합니다.</p>
</li>
<li><p>** Max-Age 또는 Expires**: 일정 기간 후 쿠키를 삭제합니다.</p>
</li>
<li><p><strong>Path</strong>: 쿠키의 URL 경로를 정의합니다.&quot;</p>
</li>
</ul>
<p>각 옵션에 대한 자세한 내용은 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies">MDN</a>을 참조하세요.</p>
<p><strong>app/lib/session.ts</strong></p>
<pre><code>import &#39;server-only&#39;
import { cookies } from &#39;next/headers&#39;

export async function createSession(userId: string) {
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)

   // 2. Encrypt the session ID
  const session = await encrypt({ userId, expiresAt })

  // 3. Store the session in cookies for optimistic auth checks
  const cookieStore = await cookies()
  cookieStore().set(&#39;session&#39;, session, {
    httpOnly: true,
    secure: true,
    expires: expiresAt,
    sameSite: &#39;lax&#39;,
    path: &#39;/&#39;,
  })
}</code></pre><p>서버 액션에서 <code>createSession()</code> 함수를 호출하고, <code>redirect()</code> API를 사용하여 사용자를 적절한 페이지로 리디렉션할 수 있습니다.</p>
<p><strong>app/actions/auth.ts</strong></p>
<pre><code>import { createSession } from &#39;@/app/lib/session&#39;

export async function signup(state: FormState, formData: FormData) {
  // Previous steps:
  // 1. Validate form fields
  // 2. Prepare data for insertion into database
  // 3. Insert the user into the database or call an Library API

  // Current steps:
  // 4. Create user session
  await createSession(user.id)
  // 5. Redirect user
  redirect(&#39;/profile&#39;)
}</code></pre><p><strong>팁</strong></p>
<ul>
<li>쿠키는 클라이언트 측의 변조를 방지하기 위해 서버에서 설정해야 합니다.</li>
<li>🎥 시청하기: Next.js로 무상태 세션 및 인증에 대해 더 알아보기 → <a href="https://www.youtube.com/watch?v=DJvM2lSPn6w">YouTube</a></li>
</ul>
<h4 id="세션-업데이트또는-갱신">세션 업데이트(또는 갱신)</h4>
<p>세션의 만료 시간을 연장할 수도 있습니다. 이는 사용자가 애플리케이션에 다시 접근했을 때 로그인이 유지되도록 하는 데 유용합니다. </p>
<p><strong>app/lib/session.ts</strong></p>
<pre><code>import &#39;server-only&#39;
import { cookies } from &#39;next/headers&#39;
import { decrypt } from &#39;@/app/lib/session&#39;

export async function updateSession() {
  const session = (await cookies()).get(&#39;session&#39;)?.value
  const payload = await decrypt(session)

  if (!session || !payload) {
    return null
  }

  const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)

  const cookieStore = await cookies()
  cookieStore().set(&#39;session&#39;, session, {
    httpOnly: true,
    secure: true,
    expires: expires,
    sameSite: &#39;lax&#39;,
    path: &#39;/&#39;,
  })
}</code></pre><p>팁: 사용자 세션을 확장하는 데 사용할 수 있는 <code>새로 고침(refresh) 토큰</code> 인증 라이브러리가 지원하는지 확인하세요.</p>
<h4 id="세션-삭제">세션 삭제</h4>
<p>세션을 삭제하려면 쿠키를 삭제하세요.</p>
<p><strong>app/lib/session.ts</strong></p>
<pre><code>import &#39;server-only&#39;
import { cookies } from &#39;next/headers&#39;

export async function deleteSession() {
  const cookieStore = await cookies()
  cookieStore().delete(&#39;session&#39;)
}</code></pre><p>그런 다음 애플리케이션에서 <code>deleteSession()</code> 함수를 재사용할 수 있습니다(예: 로그아웃 시)</p>
<p><strong>app/actions/auth.ts</strong></p>
<pre><code>import { cookies } from &#39;next/headers&#39;
import { deleteSession } from &#39;@/app/lib/session&#39;

export async function logout() {
  deleteSession()
  redirect(&#39;/login&#39;)
}</code></pre><h3 id="데이터베이스-세션">데이터베이스 세션</h3>
<p>데이터베이스 세션을 생성하고 관리하려면 다음 단계를 따라야 합니다.</p>
<ol>
<li><p>세션 데이터를 저장할 데이터베이스에 테이블을 생성합니다(또는 인증 라이브러리가 이를 처리하는지 확인합니다).</p>
</li>
<li><p>세션을 삽입, 업데이트 및 삭제하는 기능을 구현합니다.</p>
</li>
<li><p>세션 ID를 사용자의 브라우저에 저장하기 전에 암호화하고, 데이터베이스와 쿠키가 동기화되도록 합니다(선택 사항이지만 <a href="https://nextjs.org/docs/app/building-your-application/authentication#optimistic-checks-with-middleware-optional">미들웨어</a>에서 낙관적인 인증 검사를 위해 권장됨).&quot;</p>
</li>
</ol>
<p>예시</p>
<p><strong>app/lib/session.ts</strong></p>
<pre><code>import cookies from &#39;next/headers&#39;
import { db } from &#39;@/app/lib/db&#39;
import { encrypt } from &#39;@/app/lib/session&#39;

export async function createSession(id: number) {
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)

  // 1. Create a session in the database
  const data = await db
    .insert(sessions)
    .values({
      userId: id,
      expiresAt,
    })
    // Return the session ID
    .returning({ id: sessions.id })

  const sessionId = data[0].id

  // 2. Encrypt the session ID
  const session = await encrypt({ sessionId, expiresAt })

  // 3. Store the session in cookies for optimistic auth checks
  const cookieStore = await cookies()
  cookieStore().set(&#39;session&#39;, session, {
    httpOnly: true,
    secure: true,
    expires: expiresAt,
    sameSite: &#39;lax&#39;,
    path: &#39;/&#39;,
  })
}</code></pre><p><strong>팁</strong></p>
<ul>
<li><strong>빠른 데이터 검색을 위해</strong> Vercel Redis와 같은 데이터베이스를 사용하는 것을 고려해 보세요. 하지만 기본 데이터베이스에 세션 데이터를 유지하고 데이터 요청을 결합하여 쿼리 수를 줄이는 방법도 가능합니다.</li>
<li><strong>더 고급 사용 사례를 위해</strong> 데이터베이스 세션을 사용할 수 있습니다. 예를 들어 사용자가 마지막으로 로그인한 시간, 활성 장치 수를 추적하거나 사용자가 모든 장치에서 로그아웃할 수 있는 기능을 제공하는 등의 용도로 활용할 수 있습니다.</li>
</ul>
<p>세션 관리 구현 후, 사용자가 애플리케이션 내에서 접근할 수 있는 것과 수행할 수 있는 작업을 제어하기 위해 <strong>권한 부여 로직</strong>을 추가해야 합니다. <a href="https://nextjs.org/docs/app/building-your-application/authentication#authorization">권한 부여 섹션</a>으로 이동하여 더 자세히 알아보세요.</p>
<h3 id="권한-부여authorization">권한 부여(Authorization)</h3>
<p>사용자가 인증되고 세션이 생성되면, 애플리케이션 내에서 사용자가 접근하고 수행할 수 있는 권한을 제어하기 위해 권한 부여(authorization)를 구현할 수 있습니다.</p>
<p>주요 권한 부여 확인 유형은 두 가지입니다:</p>
<ol>
<li><p><strong>낙관적(Optimistic)</strong>: <strong>사용자가 쿠키에 저장된 세션 데이터를 사용</strong>하여 특정 경로에 접근하거나 행동을 수행할 수 있는 권한이 있는지 확인하는 방법입니다. 이러한 확인은 UI 요소를 보여주거나 숨기거나, 권한이나 역할에 따라 사용자를 리디렉션하는 등의 빠른 작업에 유용합니다.</p>
</li>
<li><p><strong>안전(Secure)</strong>: <strong>사용자가 데이터베이스에 저장된 세션 데이터를 사용</strong>하여 특정 경로에 접근하거나 행동을 수행할 수 있는 권한이 있는지 확인하는 방법입니다. 이러한 체크는 더 안전하며, 민감한 데이터에 접근하거나 특정 작업을 수행해야 하는 경우에 사용됩니다.</p>
</li>
</ol>
<p>두 경우 모두 다음을 권장합니다.</p>
<ul>
<li><p>권한 부여 로직을 중앙 집중화하기 위한 <strong>데이터 접근 계층</strong>(<a href="https://nextjs.org/docs/app/building-your-application/authentication#creating-a-data-access-layer-dal">Data Access Layer)</a> 생성</p>
</li>
<li><p>필요한 데이터만 반환하기 위한 <strong>데이터 전송 객체</strong><a href="https://nextjs.org/docs/app/building-your-application/authentication#using-data-transfer-objects-dto">(Data Transfer Objects, DTO)</a> 사용</p>
</li>
<li><p>옵션으로 미들웨어를 사용하여 <a href="https://nextjs.org/docs/app/building-your-application/authentication#optimistic-checks-with-middleware-optional">낙관적인</a> 체크 수행</p>
</li>
</ul>
<h4 id="미들웨어를-통한-낙관적인-체크-선택-사항">미들웨어를 통한 낙관적인 체크 (선택 사항)</h4>
<p>미들웨어를 사용하여 사용자의 권한에 따라 리다이렉트하는 경우가 있습니다. 여기 몇 가지 상황이 있습니다:</p>
<ul>
<li><p><strong>낙관적인 체크 수행</strong>: 미들웨어는 모든 경로에서 실행되므로, 리다이렉트 로직을 중앙 집중화하고 인가되지 않은 사용자를 미리 필터링하는 좋은 방법입니다.</p>
</li>
<li><p><strong>사용자 간 데이터 공유가 있는 정적 경로 보호</strong>: 예를 들어, 유료 콘텐츠 뒤에 숨겨진 데이터와 같은 경우에 해당합니다.</p>
</li>
</ul>
<p>그러나 미들웨어는 모든 경로에서 실행되기 때문에, <a href="https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching">미리 가져온 경로</a>를 포함하여 성능 문제를 방지하기 위해 데이터베이스 체크는 피하고 세션을 쿠키에서만 읽는 것이 중요합니다.</p>
<p><strong>예시</strong></p>
<p><strong>middleware.ts</strong></p>
<pre><code>import { NextRequest, NextResponse } from &#39;next/server&#39;
import { decrypt } from &#39;@/app/lib/session&#39;
import { cookies } from &#39;next/headers&#39;

// 1. Specify protected and public routes
const protectedRoutes = [&#39;/dashboard&#39;]
const publicRoutes = [&#39;/login&#39;, &#39;/signup&#39;, &#39;/&#39;]

export default async function middleware(req: NextRequest) {
  // 2. Check if the current route is protected or public
  const path = req.nextUrl.pathname
  const isProtectedRoute = protectedRoutes.includes(path)
  const isPublicRoute = publicRoutes.includes(path)

  // 3. Decrypt the session from the cookie
  const cookie = (await cookies()).get(&#39;session&#39;)?.value
  const session = await decrypt(cookie)

  // 4. Redirect to /login if the user is not authenticated
  if (isProtectedRoute &amp;&amp; !session?.userId) {
    return NextResponse.redirect(new URL(&#39;/login&#39;, req.nextUrl))
  }

  // 5. Redirect to /dashboard if the user is authenticated
  if (
    isPublicRoute &amp;&amp;
    session?.userId &amp;&amp;
    !req.nextUrl.pathname.startsWith(&#39;/dashboard&#39;)
  ) {
    return NextResponse.redirect(new URL(&#39;/dashboard&#39;, req.nextUrl))
  }

  return NextResponse.next()
}

// Routes Middleware should not run on
export const config = {
  matcher: [&#39;/((?!api|_next/static|_next/image|.*\\.png$).*)&#39;],
}</code></pre><p>미들웨어는 초기 체크에 유용할 수 있지만, 데이터를 보호하는 데 있어 유일한 방어선이 되어서는 안 됩니다. 보안 검사는 데이터 소스에 최대한 가깝게 수행해야 하며, 더 많은 정보는 <strong>데이터 접근 계층</strong><a href="https://nextjs.org/docs/app/building-your-application/authentication#creating-a-data-access-layer-dal">(Data Access Layer)</a>을 참조하세요.</p>
<p><strong>팁:</strong></p>
<ul>
<li><p>미들웨어에서는 req.cookies.get(&#39;session&#39;).value를 사용하여 쿠키를 읽을 수 있습니다.</p>
</li>
<li><p>미들웨어는 엣지 런타임(Edge Runtime)을 사용하므로, 인증(auth) 라이브러리와 세션 관리 라이브러리가 호환되는지 확인하세요.</p>
</li>
<li><p>미들웨어의 <code>matcher</code> 속성을 사용하여 미들웨어가 실행될 경로를 지정할 수 있습니다. 하지만 인증(auth)의 경우, 모든 경로에서 미들웨어가 실행되는 것이 권장됩니다.</p>
</li>
</ul>
<h3 id="데이터-액세스-계층dal-생성">데이터 액세스 계층(DAL) 생성</h3>
<p>데이터 요청과 인증 로직을 중앙에서 관리할 수 있도록 DAL(Data Access Layer)을 생성하는 것이 좋습니다.</p>
<p>DAL에는 사용자가 애플리케이션과 상호작용할 때 세션을 확인하는 기능이 포함되어야 합니다. 최소한 이 기능은 세션의 유효성을 검사하고, 이후 요청을 처리할 때 필요한 사용자 정보를 반환하거나 리다이렉트 하는 역할을 해야 합니다.</p>
<p>예를 들어, <code>verifySession()</code> 함수를 포함한 별도의 파일을 만들어 DAL을 구성할 수 있습니다. 그런 다음 React의 <a href="https://react.dev/reference/react/cache"><code>cache</code></a> API를 사용하여 React 렌더링 동안 해당 함수의 반환 값을 메모이제이션하여 성능을 최적화할 수 있습니다.</p>
<p><strong>app/lib/dal.ts</strong></p>
<pre><code>import &#39;server-only&#39;

import { cookies } from &#39;next/headers&#39;
import { decrypt } from &#39;@/app/lib/session&#39;

export const verifySession = cache(async () =&gt; {
  const cookie = (await cookies()).get(&#39;session&#39;)?.value
  const session = await decrypt(cookie)

  if (!session?.userId) {
    redirect(&#39;/login&#39;)
  }

  return { isAuth: true, userId: session.userId }
})</code></pre><p>그런 다음 데이터 요청, 서버 작업(Server Actions), 경로 핸들러(Route Handlers)에서 <code>verifySession()</code> 함수를 호출할 수 있습니다.</p>
<p><strong>app/lib/dal.ts</strong></p>
<pre><code>export const getUser = cache(async () =&gt; {
  const session = await verifySession()
  if (!session) return null

  try {
    const data = await db.query.users.findMany({
      where: eq(users.id, session.userId),
      // Explicitly return the columns you need rather than the whole user object
      columns: {
        id: true,
        name: true,
        email: true,
      },
    })

    const user = data[0]

    return user
  } catch (error) {
    console.log(&#39;Failed to fetch user&#39;)
    return null
  }
})</code></pre><p><strong>팁:</strong></p>
<ul>
<li><p>DAL(Data Access Layer)은 요청 시점에 데이터를 보호하는 데 사용할 수 있습니다. 하지만 사용자가 공유하는 정적 라우트의 경우, 데이터는 요청 시점이 아닌 빌드 시점에 가져옵니다. 정적 라우트를 보호하려면 <a href="https://nextjs.org/docs/app/building-your-application/authentication#optimistic-checks-with-middleware-optional">미들웨어</a>를 사용하세요.</p>
</li>
<li><p>안전한 검사를 위해, 세션 ID를 데이터베이스와 비교하여 세션이 유효한지 확인할 수 있습니다. React의 <a href="https://react.dev/reference/react/cache">캐시 함수(cache function)</a>를 사용하여 렌더링 중 중복된 데이터베이스 요청을 방지하세요.</p>
</li>
<li><p>관련된 데이터 요청을 JavaScript 클래스에 통합하고, 메서드 호출 전에 <code>verifySession()</code>을 실행하도록 설정하는 것도 좋은 방법입니다.</p>
</li>
</ul>
<h3 id="데이터-전송-개체dto-사용">데이터 전송 개체(DTO) 사용</h3>
<p>데이터를 가져올 때, 애플리케이션에서 실제로 사용하는 필요한 데이터만 반환하는 것이 좋습니다. 예를 들어, 사용자 데이터를 가져올 때 비밀번호, 전화번호 등 전체 사용자 객체가 아니라 사용자 ID와 이름 같은 필요한 정보만 반환하는 것이 좋습니다.</p>
<p>하지만 반환되는 데이터 구조를 제어할 수 없거나, 팀 작업 중 전체 객체가 클라이언트에 전달되지 않도록 하고 싶을 때는 클라이언트에 노출해도 안전한 필드를 지정하는 전략을 사용할 수 있습니다.</p>
<p><strong>app/lib/dto.ts</strong></p>
<pre><code>import &#39;server-only&#39;
import { getUser } from &#39;@/app/lib/dal&#39;

function canSeeUsername(viewer: User) {
  return true
}

function canSeePhoneNumber(viewer: User, team: string) {
  return viewer.isAdmin || team === viewer.team
}

export async function getProfileDTO(slug: string) {
  const data = await db.query.users.findMany({
    where: eq(users.slug, slug),
    // Return specific columns here
  })
  const user = data[0]

  const currentUser = await getUser(user.id)

  // Or return only what&#39;s specific to the query here
  return {
    username: canSeeUsername(currentUser) ? user.username : null,
    phonenumber: canSeePhoneNumber(currentUser, user.team)
      ? user.phonenumber
      : null,
  }
}</code></pre><p>데이터 요청과 인증 로직을 데이터 접근 계층(DAL)에 중앙 집중화하고 DTO(Data Transfer Object)를 사용하면, 모든 데이터 요청의 보안과 일관성을 보장할 수 있습니다. 이를 통해 애플리케이션이 확장되더라도 유지보수, 감사, 디버깅이 쉬워집니다.</p>
<p><strong>참고사항</strong></p>
<ul>
<li><p>DTO(Data Transfer Object)를 정의하는 방법에는 <code>toJSON()</code> 메서드를 사용하는 것부터 개별 함수, JS 클래스를 사용하는 것까지 다양한 방식이 있습니다. 이는 JavaScript 패턴이지 React나 Next.js의 기능이 아니므로, 애플리케이션에 가장 적합한 패턴을 찾기 위해 연구해 보는 것이 좋습니다. </p>
</li>
<li><p>Next.js 보안 모범 사례에 대해 더 알고 싶다면, 우리의 <a href="https://nextjs.org/blog/security-nextjs-server-components-actions">&quot;Next.js 보안&quot;</a> 기사에서 자세히 알아보세요.</p>
</li>
</ul>
<h3 id="서버-컴포넌트">서버 컴포넌트</h3>
<p><a href="https://nextjs.org/docs/app/building-your-application/rendering/server-components">서버 컴포넌트</a>에서의 인증 검사는 역할 기반 접근 제어에 유용합니다. 예를 들어, 사용자의 역할에 따라 조건부로 컴포넌트를 렌더링할 수 있습니다:</p>
<p><strong>app/dashboard/page.tsx</strong></p>
<pre><code>import { verifySession } from &#39;@/app/lib/dal&#39;

export default function Dashboard() {
  const session = await verifySession()
  const userRole = session?.user?.role // Assuming &#39;role&#39; is part of the session object

  if (userRole === &#39;admin&#39;) {
    return &lt;AdminDashboard /&gt;
  } else if (userRole === &#39;user&#39;) {
    return &lt;UserDashboard /&gt;
  } else {
    redirect(&#39;/login&#39;)
  }
}</code></pre><p>예시에서는 DAL에서 제공하는 verifySession() 함수를 사용하여 &#39;admin&#39;, &#39;user&#39;, 및 권한 없는 역할을 확인합니다. 이 패턴은 각 사용자가 자신의 역할에 적합한 컴포넌트와만 상호작용하도록 보장합니다.</p>
<h3 id="레이아웃-및-인증-확인">레이아웃 및 인증 확인</h3>
<p><a href="https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#4-partial-rendering">부분 렌더링(Partial Rendering)</a> 때문에 레이아웃에서 체크를 수행할 때 주의해야 합니다. 레이아웃은 내비게이션 시 다시 렌더링되지 않으므로, 사용자의 세션이 모든 경로 변경 시마다 체크되지 않습니다.</p>
<p>대신, 데이터 자원이나 조건부로 렌더링될 컴포넌트에 가까운 곳에서 체크를 수행해야 합니다.</p>
<p>예를 들어, 사용자 데이터를 가져와서 내비게이션에 사용자 이미지를 표시하는 공유 레이아웃을 고려해 보세요. 인증 검사를 레이아웃에서 수행하는 대신, 레이아웃에서 사용자 데이터를 가져오는 <code>getUser()</code>를 호출하고, 인증 체크는 DAL에서 수행해야 합니다.</p>
<p>이렇게 하면 애플리케이션 내에서 <code>getUser()</code>가 호출되는 모든 위치에서 인증 검사가 수행되므로, 개발자가 사용자가 데이터에 접근할 권한이 있는지 확인하는 것을 잊는 것을 방지할 수 있습니다.</p>
<p><strong>app/layout.tsx</strong></p>
<pre><code>export default async function Layout({
  children,
}: {
  children: React.ReactNode;
}) {
  const user = await getUser();

  return (
    // ...
  )
}</code></pre><p><strong>app/lib/dal.ts</strong></p>
<pre><code>export const getUser = cache(async () =&gt; {
  const session = await verifySession()
  if (!session) return null

  // Get user ID from session and fetch data
})</code></pre><p><strong>참고사항</strong></p>
<ul>
<li>단일 페이지 애플리케이션(SPA)에서 일반적으로 사용되는 패턴은 사용자가 인증되지 않은 경우 <strong>레이아웃</strong>이나 <strong>최상위 컴포넌트</strong>에서 <strong>null을 반환하는 것</strong>입니다. 하지만 이 패턴은 권장되지 않습니다. Next.js 애플리케이션은 여러 진입점을 가지므로, 중첩된 경로 세그먼트나 서버 액세스(Server Actions)에 대한 접근을 차단하지 못합니다.</li>
</ul>
<h3 id="서버-액션server-actions">서버 액션(Server Actions)</h3>
<p><a href="https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations">서버 액션</a>을 공개 API 엔드포인트와 동일한 보안 고려 사항으로 취급하고, 사용자가 <strong>변형(mutation)</strong>을 수행할 수 있는 권한이 있는지 확인해야 합니다. </p>
<p><strong>app/lib/actions.ts</strong></p>
<pre><code>&#39;use server&#39;
import { verifySession } from &#39;@/app/lib/dal&#39;

export async function serverAction(formData: FormData) {
  const session = await verifySession()
  const userRole = session?.user?.role

  // Return early if user is not authorized to perform the action
  if (userRole !== &#39;admin&#39;) {
    return null
  }

  // Proceed with the action for authorized users
}</code></pre><h3 id="라우트-핸들러">라우트 핸들러</h3>
<p>라우트 핸들러를 공개 API 엔드포인트와 동일한 보안 고려 사항으로 취급하고, 사용자가 해당 라우트 핸들러에 접근할 수 있는 권한이 있는지 확인해야 합니다. </p>
<p><strong>예시</strong></p>
<p><strong>app/api/route.ts</strong></p>
<pre><code>import { verifySession } from &#39;@/app/lib/dal&#39;

export async function GET() {
  // User authentication and role verification
  const session = await verifySession()

  // Check if the user is authenticated
  if (!session) {
    // User is not authenticated
    return new Response(null, { status: 401 })
  }

  // Check if the user has the &#39;admin&#39; role
  if (session.user.role !== &#39;admin&#39;) {
    // User is authenticated but does not have the right permissions
    return new Response(null, { status: 403 })
  }

  // Continue for authorized users
}</code></pre><p>위의 예시는 두 단계의 보안 검사를 포함한 라우트 핸들러를 보여줍니다. 먼저 활성 세션이 있는지 확인한 후, 로그인한 사용자가 &#39;admin&#39;인지 검증합니다.</p>
<h3 id="context-providers">Context Providers</h3>
<p>인증을 위한 <code>컨텍스트 제공자(context providers)</code>를 사용하는 것은 <code>중첩 구조(interleaving)</code> 덕분에 효과적입니다. 하지만 React 컨텍스트는 <strong>서버 컴포넌트(Server Components)</strong>에서 지원되지 않기 때문에 <strong>클라이언트 컴포넌트</strong>(Client Components)에만 적용 가능합니다.</p>
<p>이러한 방식은 동작하지만, 모든 자식 서버 컴포넌트는 먼저 서버에서 렌더링되며, 이 경우 컨텍스트 제공자의 세션 데이터에 접근할 수 없습니다.</p>
<p><strong>app/layout.ts</strong></p>
<pre><code>import { ContextProvider } from &#39;auth-lib&#39;

export default function RootLayout({ children }) {
  return (
    &lt;html lang=&quot;en&quot;&gt;
      &lt;body&gt;
        &lt;ContextProvider&gt;{children}&lt;/ContextProvider&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  )
}</code></pre><pre><code>&quot;use client&quot;;

import { useSession } from &quot;auth-lib&quot;;

export default function Profile() {
  const { userId } = useSession();
  const { data } = useSWR(`/api/user/${userId}`, fetcher)

  return (
    // ...
  );
}</code></pre><p>클라이언트 컴포넌트에서 세션 데이터가 필요한 경우(예: 클라이언트 측 데이터 페칭을 위한 경우), React의 <a href="https://react.dev/reference/react/experimental_taintUniqueValue">taintUniqueValue API</a>를 사용하여 민감한 세션 데이터가 클라이언트에 노출되는 것을 방지해야 합니다.</p>
<p><a href="https://nextjs.org/docs/app/building-your-application/authentication#authorization">Next.js 공식 문서</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[useInfiniteQuery에서 initialData와 쿼리 키 문제 해결하기: 필터 변경 시 데이터 동기화 방법]]></title>
            <link>https://velog.io/@white0_0/useInfiniteQuery%EC%97%90%EC%84%9C-initialData%EC%99%80-%EC%BF%BC%EB%A6%AC-%ED%82%A4-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-%ED%95%84%ED%84%B0-%EB%B3%80%EA%B2%BD-%EC%8B%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%8F%99%EA%B8%B0%ED%99%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@white0_0/useInfiniteQuery%EC%97%90%EC%84%9C-initialData%EC%99%80-%EC%BF%BC%EB%A6%AC-%ED%82%A4-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-%ED%95%84%ED%84%B0-%EB%B3%80%EA%B2%BD-%EC%8B%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%8F%99%EA%B8%B0%ED%99%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Wed, 16 Oct 2024 14:33:14 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-발생">문제 발생</h3>
<p>초기에 SSR(Server-Side Rendering)로 데이터를 10개 받아와 이를 초기 데이터(<code>initialData</code>)로 전달한 후, <code>useInfiniteQuery</code>를 이용해 데이터를 불러오고 있었다. 하지만 <code>staleTime</code> 속성을 설정한 후, 필터 값을 변경할 때마다 필터 값은 정상적으로 변하지만 데이터 목록은 전혀 변하지 않는 문제가 발생했다.</p>
<p>아래를 보면 변화가 없는 것을 알 수 있다. 그렇다면 왜 stateTime을 설정하면 이런 일이 발생할까 생각을 해보니 초기에 오는 데이터와 일치한다는 점에서 initialData 속성에 대해 뭔가 잘못 알고있는 부분이 있다고 생각했다. </p>
<p>아래의 예시를 보면 필터를 변경해도 데이터가 변하지 않는 상황을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/white0_0/post/22b4fc98-07b3-4c4a-8f03-5171ea1e4d05/image.gif" alt=""></p>
<p>문제가 발생한 이유는 <code>staleTime</code>이 설정된 상태에서 필터 값을 변경해도 초기에 받아온 <code>initialData</code>가 계속 사용되기 때문이었다. 필터 타입도 쿼리 키의 일부분으로 사용하고 있었으나, 쿼리 키가 변경되어도 여전히 같은 <code>initialData</code>가 적용되는 문제로 인해 데이터가 업데이트되지 않았던 것이다.</p>
<p>처음에는 <code>initialData</code>가 말 그대로 초기 데이터로만 사용된다고 생각했으나, 쿼리 키가 변경되더라도 계속해서 동일한 데이터를 참조하고 있었던 것이 문제였다.</p>
<h3 id="해결-방법">해결 방법</h3>
<p>이 문제를 해결하기 위해서는 <strong>최초 쿼리 키와 일치할 때만</strong> <code>initialData</code>를 사용하도록 설정하는 것이 중요하다.</p>
<p>아래 코드를 통해 해결 과정을 설명하겠다.</p>
<ol>
<li><p>먼저, <strong>초기 쿼리 키</strong>를 상수로 저장한다.</p>
</li>
<li><p>현재 쿼리 키가 <strong>초기 쿼리 키와 일치하는지 여부를 확인</strong>합니다.</p>
</li>
<li><p>쿼리 키가 최초 쿼리 키와 같을 경우에만 <code>initialData</code>에 초기 데이터를 전달한다. 그 외의 경우에는 새로운 데이터를 캐싱하고, 이를 기반으로 데이터가 업데이트된다.</p>
</li>
</ol>
<pre><code>
const INITIAL_QUERY_KEY = 최초 쿼리 키 값

const isInitialQuery = 현재 쿼리 키 값 === INITIAL_QUERY_KEY

  initialData: isInitialQuery
      ? {
          pages: [gatherings],
          pageParams: [0],
        }
      : undefined,</code></pre><p>이렇게 설정하면 <strong>초기 쿼리 키와 일치하는 경우에만</strong> <code>initialData</code>가 사용되며, 필터를 변경할 때마다 정상적으로 새로운 데이터를 요청하고 캐싱하여 필터링 기능이 올바르게 작동하는 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/white0_0/post/ba2025d1-82fb-4b03-bf40-0bb9e7fdb8b5/image.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Query에서 Hydration 상태와 로딩 중복 표시 문제 해결 방법]]></title>
            <link>https://velog.io/@white0_0/React-Query%EC%97%90%EC%84%9C-Hydration-%EC%83%81%ED%83%9C%EC%99%80-%EB%A1%9C%EB%94%A9-%EC%A4%91%EB%B3%B5-%ED%91%9C%EC%8B%9C-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@white0_0/React-Query%EC%97%90%EC%84%9C-Hydration-%EC%83%81%ED%83%9C%EC%99%80-%EB%A1%9C%EB%94%A9-%EC%A4%91%EB%B3%B5-%ED%91%9C%EC%8B%9C-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Fri, 11 Oct 2024 01:16:02 GMT</pubDate>
            <description><![CDATA[<h3 id="발생한-문제">발생한 문제</h3>
<p>사용자가 찜한 모임 목록을 서버에서 불러오는 기능을 구현 중이었다. 처음에는 <code>&#39;hydrated&#39;</code> 상태가 완료되지 않았을 때 로딩 메시지가 한 번 나타나고, 그 뒤로도 <code>&#39;isLoading&#39;</code> 상태에서 또 다른 로딩 메시지가 나타나는 문제가 있었다. 이러한 중복 로딩 표시가 사용자 경험을 저해할 수 있어 해결책을 찾고자 했다.</p>
<p>Zustand로 찜 목록을 관리하는 중, <code>hydration</code> 문제를 해결하기 위해 hydrated 값을 사용하여, 아직 <code>hydration</code>이 완료되지 않았을 때는 사용자에게 대체 메시지를 보여주는 방식을 구현했다.</p>
<p>그런데 아래 html 요소와 속성을 같게 줘도 글자가 위아래로 움직이는 화면이 나타났다. 그리고 아래와 같은 방법은 실제로 구현하면서도 이 방법은 아니것이라고 확신이 들었다.</p>
<p>처음에는 다음과 같은 코드를 사용했다.</p>
<pre><code>if (!hydrated) {
  return (
    &lt;div className=&quot;flex items-center justify-center h-full min-h-500pxr&quot;&gt;
      &lt;p className=&quot;h-10 text-sm font-medium text-center text-gray-500&quot;&gt;찜 목록 확인 중...&lt;/p&gt;
    &lt;/div&gt;
  );
}
let content;

if (isLoading) {
  content = (
    &lt;div className=&quot;flex items-center justify-center h-full min-h-500pxr&quot;&gt;
      &lt;p className=&quot;h-10 text-sm font-medium text-center text-gray-500&quot;&gt;데이터 불러오는 중...&lt;/p&gt;
    &lt;/div&gt;
  );
}</code></pre><p>하지만 <code>hydrated</code>가 <code>false</code>일 때와 <code>isLoading</code> 상태에서 두 개의 유사한 메시지를 보여주는 동안, 페이지에서 글자가 약간의 차이로 위아래로 움직이는 현상이 발생했다. HTML 구조와 속성은 같았지만, 이러한 화면 변화로 인해 사용자 경험이 떨어진다는 것을 느꼈다. 그리고 이 방법이 최선이 아니라는 확신이 들었다.</p>
<p>따라서 이 문제를 해결하기 위해, 두 상태(hydration과 isLoading)를 통합하여 메시지를 일관되게 유지하고 불필요한 화면 움직임을 방지하는 방식으로 개선할 필요가 있었다.</p>
<h4 id="수정-전">수정 전</h4>
<p><img src="https://velog.velcdn.com/images/white0_0/post/b10161a1-c9dc-4e04-a862-9a05e65c4cb9/image.gif" alt=""></p>
<h3 id="해결-방법">해결 방법</h3>
<p>그리고 실제 목록들은 useQuery를 이용하여 데이터를 요청해서 가져오고 있다.</p>
<p>먼저, <code>react-query</code>에서 <code>enabled</code> 옵션을 사용해 <code>hydrated</code> 상태가 완료될 때까지 쿼리가 실행되지 않도록 제어했다. 또한, <code>hydrated</code>가 완료되지 않았을 때와 <code>isLoading</code> 상태를 구분해 로딩 메시지를 한 번만 표시하도록 코드를 수정했다.</p>
<pre><code>useQuery({
  queryKey,
  queryFn: async () =&gt; {
    // ...
  },
  enabled: hydrated,  // hydrated가 true일 때만 쿼리 실행
});

  if (!hydrated || isLoading) {
    return (
      &lt;div className=&quot;flex items-center justify-center h-full min-h-500pxr&quot;&gt;
        &lt;p className=&quot;h-10 text-sm font-medium text-center text-gray-500&quot;&gt;찜 목록 확인 중...&lt;/p&gt;
      &lt;/div&gt;
    );
  }</code></pre><h4 id="수정-후">수정 후</h4>
<p><img src="https://velog.velcdn.com/images/white0_0/post/fb4e0188-41fd-44aa-b548-0c5a042635a6/image.gif" alt=""></p>
<p>이렇게 코드를 수정한 후, 처음 <code>hydration</code>이 완료되지 않았을 때만 로딩 메시지가 표시되고, 데이터를 불러올 때는 중복된 로딩 메시지가 사라지게 되었다. 결과적으로 사용자 경험이 개선되었다.</p>
<p>이 방법은 다른 비동기 데이터 로딩 상황에서도 쿼리 실행 조건을 제어할 때 유용하다고 생각한다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] Uncontrolled Component, Controlled Component]]></title>
            <link>https://velog.io/@white0_0/React-Uncontrolled-Component-Controlled-Component</link>
            <guid>https://velog.io/@white0_0/React-Uncontrolled-Component-Controlled-Component</guid>
            <pubDate>Thu, 10 Oct 2024 15:00:28 GMT</pubDate>
            <description><![CDATA[<h3 id="uncontrolled-component">Uncontrolled Component</h3>
<p><code>Uncontrolled Component</code>는** DOM에서 직접 데이터를 제어하는 방식**으로 작동한다. React에서는 <code>ref</code>를 사용하여 DOM 요소에 직접 접근할 수 있는데, 이 방식이 바로 Uncontrolled Component의 특징이다.</p>
<h4 id="예시-코드">예시 코드</h4>
<pre><code>import { useRef } from &#39;react&#39;;

const UncontrolledComponent = () =&gt; {
  const inputRef = useRef(null); // useRef를 통해 input에 직접 접근

  const handleSubmit = e =&gt; {
    e.preventDefault();
    console.log(inputRef.current.value); // DOM을 통해 값에 접근
  };

  return (
    &lt;form&gt;
      &lt;input type=&#39;text&#39; placeholder=&#39;uncontrolled&#39; ref={inputRef} /&gt; 
      &lt;button onClick={handleSubmit}&gt;Submit&lt;/button&gt;
    &lt;/form&gt;
  );
};

export default UncontrolledComponent;
</code></pre><p><strong>주요 특징</strong></p>
<ul>
<li><p>ref를 통해 DOM 요소에 접근하여 값을 가져옴</p>
</li>
<li><p>컴포넌트 내부의 state가 아닌 DOM에서 관리되는 값을 사용하는 방식</p>
</li>
<li><p>양식 유효성 검사를 하려면 값을 읽은 후 추가적인 검사를 해야 하며, 값이 유효하지 않을 가능성이 존재</p>
</li>
<li><p>상태가 예측 불가능할 수 있으며, 컴포넌트가 DOM의 참조를 잃을 경우 데이터 제어가 어려워질 수 있음</p>
</li>
</ul>
<h3 id="controlled-component">Controlled Component</h3>
<p><code>Controlled Component</code>는 React의 state를 통해 데이터를 제어하는 방식이다. 즉, 입력 값이 React 컴포넌트의 상태에 의해 관리된다.</p>
<h4 id="예시코드">예시코드</h4>
<pre><code>import { useState } from &#39;react&#39;;

const ControlledComponent = () =&gt; {
  const [inputText, setInputText] = useState(&#39;&#39;); // state를 사용하여 값 관리

  const handleSubmit = e =&gt; {
    e.preventDefault();
    console.log(inputText); // state에 저장된 값 출력
  };

  return (
    &lt;form&gt;
      &lt;input
        type=&#39;text&#39;
        placeholder=&#39;controlled&#39;
        value={inputText} // 상태가 값으로 관리됨
        onChange={e =&gt; setInputText(e.target.value)} // 입력에 따라 state 업데이트
      /&gt;
      &lt;button onClick={handleSubmit}&gt;Submit&lt;/button&gt;
    &lt;/form&gt;
  );
};

export default ControlledComponent;
</code></pre><p><strong>주요 특징</strong></p>
<ul>
<li><p>state를 통해 폼 필드 값을 제어</p>
</li>
<li><p>컴포넌트 내부의 state가 아닌 DOM에서 관리되는 값을 사용하는 방식</p>
</li>
<li><p>입력 값이 state에 저장되고, 입력 값의 변동은 onChange 이벤트를 통해 관리</p>
</li>
<li><p>상태가 예측 불가능할 수 있으며, 컴포넌트가 DOM의 참조를 잃을 경우 데이터 제어가 어려워질 수 있음</p>
</li>
<li><p>React의 컴포넌트 수명 주기와 상태가 잘 연동되어 있으며, 폼 요소와 데이터 제어가 매우 일관됨</p>
</li>
</ul>
<h3 id="controlled-component와-uncontrolled-component의-차이점">Controlled Component와 Uncontrolled Component의 차이점</h3>
<table>
<thead>
<tr>
<th align="left">구분</th>
<th align="left">Controlled Component</th>
<th align="center">Uncontrolled Component</th>
</tr>
</thead>
<tbody><tr>
<td align="left">데이터 제어 방식</td>
<td align="left">상태(state)를 통해 데이터 제어</td>
<td align="center">DOM에서 직접 데이터를 제어 (Ref를 통해 접근)</td>
</tr>
<tr>
<td align="left">유효성 검사</td>
<td align="left">간단하게 상태를 이용해 실시간 유효성 검사가 가능</td>
<td align="center">DOM에서 값을 읽은 후 추가 검사를 해야 함</td>
</tr>
<tr>
<td align="left">컴포넌트와 데이터의 연동성</td>
<td align="left">React의 상태와 데이터가 연동되어 예측 가능하고 관리가 용이</td>
<td align="center">컴포넌트가 참조를 잃거나, 상태가 예측 불가능할 수 있음</td>
</tr>
<tr>
<td align="left">복잡도</td>
<td align="left">입력값 제어와 유효성 검사를 동시에 쉽게 처리 가능</td>
<td align="center">상대적으로 간단하나 유효성 검사가 어렵고, 예측이 불가능할 수 있음</td>
</tr>
</tbody></table>
<h3 id="스스로-질문">스스로 질문</h3>
<h4 id="질문-1-controlled-component와-uncontrolled-component의-차이점은-무엇인가요">질문 1: Controlled Component와 Uncontrolled Component의 차이점은 무엇인가요?</h4>
<p>Controlled Component는 상태(<code>state</code>)를 통해 입력 필드의 값을 제어하는 반면, Uncontrolled Component는 <code>DOM</code> 요소에서 직접 값을 가져온다.  Controlled Component는 onChange 이벤트를 사용해 입력 값을 실시간으로 업데이트하며, 양식 유효성 검사가 용이합니다. 반면 Uncontrolled Component는 <code>ref</code>를 사용해 DOM에 접근하여 데이터를 읽어오며, 유효성 검사를 위해 추가적인 작업이 필요하다.</p>
<h3 id="질문-2-controlled-component와-uncontrolled-component-중-어느-것이-더-좋은가요">질문 2: Controlled Component와 Uncontrolled Component 중 어느 것이 더 좋은가요?</h3>
<p>상황에 따라 다르다. 일반적으로** Controlled Component<strong>가 더 권장된다. 상태를 통해 값이 관리되기 때문에 예측 가능하며, 실시간 유효성 검사와 데이터 관리를 용이하게 할 수 있다. 그러나 간단한 폼이나 DOM 직접 접근이 필요한 경우에는 **Uncontrolled Component</strong>를 사용할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 오픈채팅방]]></title>
            <link>https://velog.io/@white0_0/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%98%A4%ED%94%88%EC%B1%84%ED%8C%85%EB%B0%A9</link>
            <guid>https://velog.io/@white0_0/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%98%A4%ED%94%88%EC%B1%84%ED%8C%85%EB%B0%A9</guid>
            <pubDate>Wed, 09 Oct 2024 03:32:22 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-설명">문제 설명</h3>
<h3 id="오픈채팅방">오픈채팅방</h3>
<p>카카오톡 오픈채팅방에서는 친구가 아닌 사람들과 대화를 할 수 있는데, 본래 닉네임이 아닌 가상의 닉네임을 사용하여 채팅방에 들어갈 수 있다.</p>
<p>신입사원인 김크루는 카카오톡 오픈 채팅방을 개설한 사람을 위해, 다양한 사람들이 들어오고, 나가는 것을 지켜볼 수 있는 관리자창을 만들기로 했다. 채팅방에 누군가 들어오면 다음 메시지가 출력된다.</p>
<p>&quot;[닉네임]님이 들어왔습니다.&quot;</p>
<p>채팅방에서 누군가 나가면 다음 메시지가 출력된다.</p>
<p>&quot;[닉네임]님이 나갔습니다.&quot;</p>
<p>채팅방에서 <strong>닉네임을 변경하는 방법</strong>은 다음과 같이 두 가지이다.</p>
<ul>
<li><p>채팅방을 나간 후, 새로운 닉네임으로 다시 들어간다.</p>
</li>
<li><p>채팅방에서 닉네임을 변경한다.
닉네임을 변경할 때는 기존에 채팅방에 출력되어 있던 메시지의 닉네임도 전부 변경된다.</p>
</li>
</ul>
<p>예를 들어, 채팅방에 &quot;Muzi&quot;와 &quot;Prodo&quot;라는 닉네임을 사용하는 사람이 순서대로 들어오면 채팅방에는 다음과 같이 메시지가 출력된다.</p>
<p>&quot;Muzi님이 들어왔습니다.&quot;
&quot;Prodo님이 들어왔습니다.&quot;</p>
<p>채팅방에 있던 사람이 나가면 채팅방에는 다음과 같이 메시지가 남는다.</p>
<p>&quot;Muzi님이 들어왔습니다.&quot;
&quot;Prodo님이 들어왔습니다.&quot;
&quot;Muzi님이 나갔습니다.&quot;</p>
<p>Muzi가 나간후 다시 들어올 때, Prodo 라는 닉네임으로 들어올 경우 기존에 채팅방에 남아있던 Muzi도 Prodo로 다음과 같이 변경된다.</p>
<p>&quot;Prodo님이 들어왔습니다.&quot;
&quot;Prodo님이 들어왔습니다.&quot;
&quot;Prodo님이 나갔습니다.&quot;
&quot;Prodo님이 들어왔습니다.&quot;</p>
<p>채팅방은 중복 닉네임을 허용하기 때문에, 현재 채팅방에는 Prodo라는 닉네임을 사용하는 사람이 두 명이 있다. 이제, 채팅방에 두 번째로 들어왔던 Prodo가 Ryan으로 닉네임을 변경하면 채팅방 메시지는 다음과 같이 변경된다.</p>
<p>&quot;Prodo님이 들어왔습니다.&quot;
&quot;Ryan님이 들어왔습니다.&quot;
&quot;Prodo님이 나갔습니다.&quot;
&quot;Prodo님이 들어왔습니다.&quot;</p>
<p>채팅방에 들어오고 나가거나, 닉네임을 변경한 기록이 담긴 문자열 배열 record가 매개변수로 주어질 때, 모든 기록이 처리된 후, <strong>최종적으로 방을 개설한 사람이 보게 되는 메시지를 문자열 배열 형태로 return</strong> 하도록 solution 함수를 완성하라.</p>
<h3 id="제한사항">제한사항</h3>
<ul>
<li><p>record는 다음과 같은 문자열이 담긴 배열이며, 길이는 1 이상 100,000 이하이다.</p>
</li>
<li><p>다음은 record에 담긴 문자열에 대한 설명이다.</p>
<ul>
<li>모든 유저는 [유저 아이디]로 구분한다.</li>
<li>[유저 아이디] 사용자가 [닉네임]으로 채팅방에 입장 - &quot;Enter [유저 아이디] [닉네임]&quot; (ex. &quot;Enter uid1234 Muzi&quot;)</li>
<li>[유저 아이디] 사용자가 채팅방에서 퇴장 - &quot;Leave [유저 아이디]&quot; (ex. &quot;Leave uid1234&quot;)</li>
<li>[유저 아이디] 사용자가 닉네임을 [닉네임]으로 변경 - &quot;Change [유저 아이디] [닉네임]&quot; (ex. &quot;Change uid1234 Muzi&quot;)</li>
<li>첫 단어는 Enter, Leave, Change 중 하나이다.</li>
<li>각 단어는 공백으로 구분되어 있으며, 알파벳 대문자, 소문자, 숫자로만 이루어져있다.</li>
<li>유저 아이디와 닉네임은 알파벳 대문자, 소문자를 구별한다.</li>
<li>유저 아이디와 닉네임의 길이는 1 이상 10 이하이다.</li>
<li>채팅방에서 나간 유저가 닉네임을 변경하는 등 잘못 된 입력은 주어지지 않는다.</li>
</ul>
</li>
</ul>
<h3 id="입출력-예">입출력 예</h3>
<p><img src="https://velog.velcdn.com/images/white0_0/post/c1dee0d2-155d-496b-ba86-ab898b85a921/image.png" alt=""></p>
<h3 id="정답코드">정답코드</h3>
<pre><code>function solution(record) {
  const answer = [];
  const person = {};

   for (let i = 0; i &lt; record.length; i++) {
    const [action, userId, username] = record[i].split(&quot; &quot;);

    if (action === &quot;Enter&quot; || action === &quot;Change&quot;) {
      person[userId] = username;
    }
  }

   for (let i = 0; i &lt; record.length; i++) {
    const [action, userId, username] = record[i].split(&quot; &quot;);

    if (action === &quot;Enter&quot;) {
      answer.push(`${person[userId]}님이 들어왔습니다.`);
    } else if (action === &quot;Leave&quot;) {
      answer.push(`${person[userId]}님이 나갔습니다.`);
    }

  }
  return answer;
}</code></pre><h3 id="코드설명">코드설명</h3>
<p>이 코드의 핵심 아이디어는 <strong>유저의 아이디는 변하지 않지만, 닉네임은 변경될 수 있다는 점</strong>을 활용하는 것이다. 따라서, 유저의 행동을 처리하면서 <strong>아이디를 키로 하고, 최신 닉네임을 값으로 하는 객체</strong>를 유지하여 최종 닉네임을 기록한다.</p>
<ol>
<li><p>첫 번째 <code>for</code>문에서는 <code>Enter</code>(입장) 또는 <code>Change</code>(닉네임 변경) 동작이 있을 때만, 해당 유저의 아이디를 <code>person</code> 객체에 저장하거나 업데이트한다. <code>Leave</code><strong>(퇴장) 행동은 닉네임과 무관</strong>하므로 고려할 필요가 없다.</p>
</li>
<li><p>두 번째 <code>for</code>문에서는 각 기록을 다시 순회하면서, <strong>최종적으로 저장된 유저의 닉네임</strong>을 기반으로 입장(<code>Enter</code>) 또는 퇴장(<code>Leave</code>) 메시지를 생성하여 <code>answer</code> 배열에 추가한다. <code>Change</code>는 유저의 닉네임을 업데이트하는 데 사용될뿐이므로 고려하지 않는다.</p>
</li>
</ol>
<p> 위와 같은 과정을 거쳐서 최종적으로 방에서 보게 되는 입장/퇴장 메시지 목록이 반환된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Cypress] Cypress & Jest 타입 충돌 ]]></title>
            <link>https://velog.io/@white0_0/Typescript-Cypress-Type-%EC%97%90%EB%9F%AC</link>
            <guid>https://velog.io/@white0_0/Typescript-Cypress-Type-%EC%97%90%EB%9F%AC</guid>
            <pubDate>Tue, 08 Oct 2024 01:30:23 GMT</pubDate>
            <description><![CDATA[<h3 id="타입-에러-발생">타입 에러 발생</h3>
<p><strong>Cypress</strong>로 <strong>E2E(End-to-end) 테스트</strong>를 시작하기 위해 Cypress를 설치한 후, 기본 예제 코드를 실행해 보았다. 공식 문서에서 제공하는 간단한 테스트 코드를 작성하고 작동을 시도했는데, 예상치 못한 타입 에러가 발생했다.</p>
<p>End-to-end tests
<strong>공식 문서 예제 코드</strong></p>
<pre><code>
describe(&#39;My First Test&#39;, () =&gt; {
  it(&#39;Does not do much!&#39;, () =&gt; {
    expect(true).to.equal(true);
  });
});
</code></pre><p>하지만 실행 결과는 다음과 같았다. 아래 사진처럼 빨간 줄이 그어지며 타입 오류가 발생했다.</p>
<p><img src="https://velog.velcdn.com/images/white0_0/post/2825f7d1-4dae-49bb-a658-70e5fb1f1bfe/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/white0_0/post/ad7f5b3b-8649-4a0c-9042-258cee303f8f/image.png" alt=""></p>
<p>설정에 문제가 있나 싶어 다양한 해결 방법을 찾아보니, 대부분의 사람들이 두 가지 방법을 제시하고 있었다.</p>
<h4 id="1-jestglobals에서-expect를-import-해오기">1. @jest/globals에서 expect를 import 해오기</h4>
<p><code>import { expect } from &#39;@jest/globals&#39;;</code></p>
<h4 id="2-tsconfigjson에서-cypressconfigts를-제외-설정하기">2. tsconfig.json에서 cypress.config.ts를 제외 설정하기</h4>
<p><code>&quot;exclude&quot;: [&quot;node_modules&quot;, &quot;cypress.config.ts&quot;, ]</code></p>
<p>두 가지 방법 전부 타입 에러 문제가 해결되지 않았다.</p>
<h3 id="타입-충돌-원인">타입 충돌 원인</h3>
<p>지금 사용하기를 <code>Cypress</code> 테스트 코드를 대상으로 <code>TypeScript</code> 타입 검사가 이뤄져야 하는데 Jest를 사용하고 있기 때문에 Cypress expect.to가 존재하지 않는다고 나오는 것이였다.</p>
<p><code>Cypress</code> 공식 문서의 타입 스크립트 섹션에서 다음과 같이 말해주고 있었다. </p>
<blockquote>
<p>동일한 프로젝트에서 <strong>Jest</strong>와 <strong>Cypress</strong>를 모두 사용하는 경우 두 테스트 러너에서 전역적으로 등록된 <strong>TypeScript</strong> 유형이 충돌할 수 있다</p>
</blockquote>
<p>테스트 러너(Test Runner)는 코드를 실행하고 테스트를 자동으로 수행하는 도구다. 개발자가 작성한 테스트 스크립트를 실행해 주고, 그 결과를 보여주는 역할을 한다. 또한 테스트가 실패한 경우 오류 메시지를 제공하여 문제를 해결할 수 있도록 도와준다. 대표적인 테스트 러너로는 <strong>Jest</strong>와 <strong>Cypress</strong>가 있으며, 각각 다른 용도로 사용된다.</p>
<ul>
<li><strong>Jest</strong>: 주로 <strong>유닛 테스트(Unit Test)</strong>나 <strong>통합 테스트(Integration Test)</strong>에 사용된다. 자바스크립트 환경에서 동작하며, UI 없이 코드의 로직을 테스트하는 데 적합</li>
<li><strong>Cypress</strong>: 주로 <strong>엔드 투 엔드 테스트(End-to-End Test)</strong>에 사용된다. 웹 애플리케이션의 실제 브라우저 동작을 테스트하는 데 유용하며, 사용자 상호작용을 시뮬레이션</li>
</ul>
<h3 id="해결-방법-tsconfigjson-설정">해결 방법 (tsconfig.json 설정)</h3>
<p>해결방법은 정말 간단한다. 타입 충돌이 발생하기 때문에 Cypress E2E 테스트를 위해 별도의 tsconfig.json을 설정한다. </p>
<ol>
<li><p>cypress를 설치하면서 생긴 cypress 폴더 하위에 tsconfig.json 파일을 생성한다. </p>
</li>
<li><p>아래 코드를 복사, 봍여 넣기를 진행한다.</p>
<pre><code>{
&quot;extends&quot;: &quot;../tsconfig.json&quot;,
&quot;compilerOptions&quot;: {
 &quot;noEmit&quot;: true,
 // be explicit about types included
 // to avoid clashing with Jest types
 &quot;types&quot;: [&quot;cypress&quot;]
},
&quot;include&quot;: [
 &quot;../node_modules/cypress&quot;,
 &quot;./**/*.ts&quot;
]
}</code></pre></li>
</ol>
<p><strong>설정의 의미</strong></p>
<ol>
<li><p><code>&quot;extends&quot;: &quot;../tsconfig.json&quot;</code>
이 설정은 프로젝트의 기존의 <strong>상위 폴더에 있는</strong> <code>tsconfig.json</code> <strong>파일을 확장</strong>한다는 뜻이다. 즉, 상위 설정을 그대로 가져오고, 필요한 부분만 덮어써서 사용한다. 이를 통해 공통 설정을 유지하고, 필요한 부분만 수정할 수 있다.</p>
</li>
<li><p><code>&quot;compilerOptions&quot;: { &quot;noEmit&quot;: true }</code>
<code>&quot;noEmit&quot;: true</code>는 TypeScript 컴파일러가 실제로 JavaScript 파일을 생성하지 않도록 하는 설정한다.</p>
</li>
<li><p><code>&quot;types&quot;: [&quot;cypress&quot;]</code></p>
</li>
</ol>
<p><strong>Cypress</strong>와 <strong>Jest</strong> 같은 테스트 러너를 함께 사용할 경우, <strong>Jest</strong>와 <strong>Cypress</strong>가 각각 전역으로 타입을 정의하기 때문에 타입 충돌이 발생할 수 있다. 이를 방지하기 위해 <strong>Cypress</strong> 관련 타입만 명시적으로 포함하겠다는 의미이다.</p>
<ol start="4">
<li><code>&quot;include&quot;: [&quot;../node_modules/cypress&quot;, &quot;./**/*.ts&quot;]</code>
이 설정은 컴파일에 포함할 파일 및 디렉터리를 지정한다.</li>
</ol>
<ul>
<li><code>&quot;../node_modules/cypress&quot;</code>: Cypress가 설치된 디렉터리 경로를 포함</li>
<li><code>&quot;./**/*.ts&quot;</code>: 현재 디렉터리 및 하위 디렉터리에 있는 <strong>모든 TypeScript 파일(.ts)</strong>을 포함
<img src="https://velog.velcdn.com/images/white0_0/post/0275c352-f224-4ef1-a89d-5eb512ae6eeb/image.png" alt=""></li>
</ul>
<p><a href="https://docs.cypress.io/guides/tooling/typescript-support#Clashing-types-with-Jest">Cypress 타입 충돌 관련 공식 문서</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Networking] HTTP와 HTTPS 차이점]]></title>
            <link>https://velog.io/@white0_0/Networking-HTTP%EC%99%80-HTTPS-%EC%B0%A8%EC%9D%B4%EC%A0%90</link>
            <guid>https://velog.io/@white0_0/Networking-HTTP%EC%99%80-HTTPS-%EC%B0%A8%EC%9D%B4%EC%A0%90</guid>
            <pubDate>Sun, 06 Oct 2024 13:21:07 GMT</pubDate>
            <description><![CDATA[<h3 id="http와-https의-차이점-왜-https가-중요한가">HTTP와 HTTPS의 차이점: 왜 HTTPS가 중요한가?</h3>
<p>인터넷을 통해 웹사이트에 접속할 때, 우리는 대부분 <strong>HTTP</strong> 또는 <strong>HTTPS</strong>를 사용한다. 이 두 프로토콜은 웹 브라우저와 서버 간의 통신 방식을 결정한다. 하지만 둘의 차이점과, 왜 HTTPS가 더 안전한지에 대해 자세히 알아볼 필요가 있다. 이번 글에서는 HTTP와 HTTPS의 차이점에 대해 설명하고, 왜 HTTPS가 중요한지 알아보겠다.</p>
<p><strong>프로토콜</strong>: <strong>프로토콜</strong>(Protocol)은 통신 규약을 의미하며, 컴퓨터나 네트워크 상에서 데이터를 주고받을 때 사용하는 규칙과 절차의 모음이다. 이를 통해 서로 다른 시스템이 일관된 방식으로 데이터를 교환하고 이해할 수 있다.</p>
<h3 id="http">HTTP</h3>
<p>** HTTP란 ?**</p>
<p><strong>HTTP(HyperText Transfer Protocol)</strong>는 웹 브라우저와 서버가 데이터를 주고받는 표준 프로토콜이다. 이 프로토콜을 통해 웹 페이지, 이미지, 동영상 등을 주고받을 수 있다. 하지만 HTTP는 <strong>암호화되지 않은 평문(Plain Text)</strong> 방식으로 데이터를 전송한다.</p>
<p>즉, HTTP를 사용할 경우 다음과 같은 문제점이 발생할 수 있다.</p>
<ul>
<li><p><strong>보안 취약성</strong>: HTTP는 데이터를 암호화하지 않기 때문에, 중간에 누군가가 데이터를 가로챌 수 있습니다. 이를 <strong>중간자 공격</strong>(Man-in-the-Middle Attack)이라고 부른다.</p>
</li>
<li><p><strong>신원 인증 부족</strong>: HTTP는 서버가 실제로 신뢰할 수 있는 서버인지 확인할 수 있는 방법이 없다. 따라서 피싱 사이트에 속을 가능성이 있다.</p>
</li>
</ul>
<p><code>클라이언트 &lt;---- 암호화 없음 ----&gt; 서버</code></p>
<h3 id="https">HTTPS</h3>
<p><strong>HTTPS란 무엇인가?</strong></p>
<p>HTTPS(HyperText Transfer Protocol Secure)는 HTTP의 보안 버전이다. <strong>SSL(Secure Sockets Layer)</strong> 또는 <strong>TLS(Transport Layer Security)</strong>라는 암호화 프로토콜을 사용하여, 데이터를 암호화한 상태로 주고받는다. 이를 통해, 클라이언트와 서버 간의 통신을 더 안전하게 만들 수 있다.</p>
<p><strong>HTTPS의 주요 기능</strong></p>
<ul>
<li><p><strong>암호화</strong>: 서버와 클라이언트 간 주고받는 데이터를 암호화하여, 중간에서 누군가 데이터를 가로채더라도 내용을 알 수 없게 만듬</p>
</li>
<li><p><strong>데이터 무결성</strong>: 전송 중에 데이터가 수정되거나 손상되지 않도록 보장</p>
</li>
<li><p><strong>서버 신원 인증</strong>: 클라이언트는 서버가 신뢰할 수 있는 기관에 의해 인증된 서버인지 확인할 수 있다. 이는 SSL/TLS 인증서를 통해 이루어진다.</p>
</li>
</ul>
<p><code>클라이언트 &lt;---- 암호화 ----&gt; 서버</code></p>
<h3 id="http와-https의-차이점">HTTP와 HTTPS의 차이점</h3>
<table>
<thead>
<tr>
<th align="left">특징</th>
<th align="left">HTTP</th>
<th align="center">HTTPS</th>
</tr>
</thead>
<tbody><tr>
<td align="left">보안</td>
<td align="left">암호화되지 않은 평문으로 데이터를 전송</td>
<td align="center">SSL/TLS를 사용하여 데이터를 암호화</td>
</tr>
<tr>
<td align="left">데이터 무결성</td>
<td align="left">데이터 무결성을 보장하지 않음</td>
<td align="center">전송 중 데이터가 손상되거나 수정되지 않음</td>
</tr>
<tr>
<td align="left">서버 신원 인증</td>
<td align="left">서버 신원을 보장하지 않음</td>
<td align="center">SSL/TLS 인증서를 통해 서버 신원을 확인 가능</td>
</tr>
<tr>
<td align="left">주요 사용 사례</td>
<td align="left">보안이 필요하지 않은 웹사이트 (예: 공개 자료)</td>
<td align="center">민감한 정보를 다루는 웹사이트 (예: 로그인, 결제)</td>
</tr>
<tr>
<td align="left">속도</td>
<td align="left">빠름</td>
<td align="center">약간 더 느릴 수 있음 (암호화 처리 때문에)</td>
</tr>
</tbody></table>
<h4 id="https의-중요성">HTTPS의 중요성</h4>
<p>오늘날 대부분의 웹사이트는 <strong>HTTPS</strong>를 사용한다. 특히 로그인 정보, 결제 정보 등과 같은 민감한 데이터를 주고받는 웹사이트에서 <strong>HTTPS</strong>는 필수적입니다. 구글 크롬과 같은 주요 웹 브라우저는 <strong>HTTP</strong>만 사용하는 웹사이트에 대해 &quot;안전하지 않음&quot; 경고를 표시한다.</p>
<h4 id="http에서-https로의-전환">HTTP에서 HTTPS로의 전환</h4>
<p>웹사이트 소유자는 <strong>SSL/TLS 인증서</strong>를 발급받아야 <strong>HTTPS</strong>로 전환할 수 있다. 이 과정은 간단하며, 무료로 인증서를 발급해주는 Let’s Encrypt와 같은 서비스도 있다. HTTPS로 전환하면 사용자의 신뢰도를 높이고, 검색 엔진 최적화(SEO) 측면에서도 유리하다.</p>
<h4 id="결론">결론</h4>
<p><strong>HTTP</strong>는 빠르고 간단한 프로토콜이지만, 보안 측면에서 큰 취약점을 가지고 있다. 반면, <strong>HTTPS</strong>는 <strong>데이터 암호화, 서버 신원 인증, 데이터 무결성을 보장</strong>하여 더 안전한 웹 환경을 제공한다. 민감한 정보를 다루는 웹사이트라면 반드시 HTTPS를 사용하는 것이 필수적이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TypeScript] Any 타입 Vs Unknown 타입]]></title>
            <link>https://velog.io/@white0_0/TypeScript-Any-%ED%83%80%EC%9E%85-Vs-Unknown-%ED%83%80%EC%9E%85</link>
            <guid>https://velog.io/@white0_0/TypeScript-Any-%ED%83%80%EC%9E%85-Vs-Unknown-%ED%83%80%EC%9E%85</guid>
            <pubDate>Sun, 06 Oct 2024 08:16:37 GMT</pubDate>
            <description><![CDATA[<p>TypeScript는 강력한 타입 시스템을 제공하여 코드의 타입 안정성을 보장한다. 그중에서도 <code>any</code>와 <code>unknown</code> 타입은 &#39;타입을 모르는&#39; 상태를 표현할 때 사용된다. 하지만 두 타입은 중요한 차이점을 가지고 있으며, 이를 올바르게 이해하고 사용하는 것이 중요하다. 이번 글에서는 <code>any</code>와 <code>unknown</code>의 차이점을 살펴보겠다.</p>
<h3 id="any-타입">any 타입</h3>
<ul>
<li><p><code>any</code> 타입은 TypeScript에서 &#39;모든 타입&#39;을 의미</p>
</li>
<li><p>어떤 타입이든 될 수 있으며, 해당 값에 대해 아무런 제약 없이 자유롭게 사용 가능</p>
</li>
<li><p><code>any</code>를 사용하면 컴파일러가 타입 검사를 하지 않기 때문에 타입 안정성을 잃음</p>
</li>
</ul>
<pre><code>let value: any;
value = 42;
value = &quot;hello&quot;;
value = true;

// 위에 value는 전부 any 타입
// 타입 검사 없이 사용 가능
console.log(value.toUpperCase());  // 런타임 에러가 발생할 수 있음

// Property &#39;toUpperCase&#39; does not exist on type &#39;boolean&#39; 에러 발생
</code></pre><p>위 코드에서 보듯이, <code>any</code>는 매우 자유롭게 사용할 수 있지만, 그만큼 실수를 감지하지 못하고 런타임에서 에러를 일으킬 수 있다. <code>any</code>는 매우 편리하지만, 타입 안정성을 희생하는 결과를 초래할 수 있다.</p>
<h3 id="unknown-타입">unknown 타입</h3>
<ul>
<li><p><code>unknown</code> 타입은 <code>any</code>와 유사하게 &#39;모든 타입이 될 수 있다&#39;는 의미</p>
</li>
<li><p>중요한 차이점은, <code>unknown</code> 타입으로 선언된 값은 타입이 확정되지 않으면 사용이 제한된다는 점</p>
</li>
<li><p>이를 통해 타입 안전성을 보장</p>
</li>
</ul>
<pre><code>let value: unknown;
value = 42;
value = &quot;hello&quot;;

// 바로 사용 불가
// console.log(value.toUpperCase());  // 에러

&#39;value&#39; is of type &#39;unknown&#39;.(18046) 에러 메시지


// 타입 검사를 통해 안전하게 사용 가능
if (typeof value === &quot;string&quot;) {
    console.log(value.toUpperCase());  // 정상 동작
}
</code></pre><p><code>unknown</code>은 항상 타입 검사를 통해 값의 타입을 확정해야만 사용할 수 있기 때문에, <code>any</code>보다 안전한 방식으로 값을 다룰 수 있다. 이는 <code>unknown</code> 타입의 주요 장점으로, 런타임 에러를 줄이는 데 도움이 된다.</p>
<table>
<thead>
<tr>
<th align="left">특징</th>
<th align="left">any</th>
<th align="left">unknown</th>
</tr>
</thead>
<tbody><tr>
<td align="left">타입 안전성</td>
<td align="left">낮음</td>
<td align="left">높음</td>
</tr>
<tr>
<td align="left">사용 가능 여부</td>
<td align="left">즉시 사용 가능</td>
<td align="left">타입 검사 후 사용 가능</td>
</tr>
<tr>
<td align="left">컴파일러 타입 검사</td>
<td align="left">없음</td>
<td align="left">타입 검사 필요</td>
</tr>
<tr>
<td align="left">사용 목적</td>
<td align="left">타입 안정성보다는 자유로운 사용이 필요할 때</td>
<td align="left">안전한 타입 검사가 필요한 경우</td>
</tr>
</tbody></table>
<p><strong>어떤 타입을 선택해야 할까?</strong></p>
<ul>
<li><p><code>any</code>는 매우 유연하지만, 타입 안정성을 희생</p>
</li>
<li><p>정말로 타입을 모르는 경우에만 사용하고, 가능하면 <code>unknown</code> 타입을 사용</p>
</li>
<li><p><code>unknown</code> 타입은 타입 검사를 강제하므로, 타입 안전성을 유지하면서 유연한 코드를 작성 가능</p>
</li>
</ul>
<p>결론적으로, 타입 안정성이 중요한 코드에서는 <code>unknown</code>을 사용하는 것이 더 좋은 선택이 될 수 있다. any는 꼭 필요한 상황에서만 사용하는 것이 좋다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트 엔드 예상 문제 ]]></title>
            <link>https://velog.io/@white0_0/%ED%94%84%EB%A1%A0%ED%8A%B8-%EC%97%94%EB%93%9C-%EC%98%88%EC%83%81-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@white0_0/%ED%94%84%EB%A1%A0%ED%8A%B8-%EC%97%94%EB%93%9C-%EC%98%88%EC%83%81-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Sat, 05 Oct 2024 04:55:08 GMT</pubDate>
            <description><![CDATA[<p><strong>브라우저 렌더링 과정</strong></p>
<p>** 1. HTML 파일 다운로드 및 파싱**</p>
<ul>
<li><p>브라우저는 서버에서 HTML 파일을 받아옵니다.</p>
</li>
<li><p>HTML 파일을 받아오면, 브라우저의 렌더링 엔진이 HTML 파싱을 시작합니다.</p>
</li>
<li><p>이때 HTML을 분석하며 DOM (Document Object Model) 트리를 만듭니다. DOM은 HTML 요소들을 노드 형태로 표현한 트리 구조입니다.</p>
</li>
<li><p>HTML에 포함된 <code>&lt;link&gt;, &lt;script&gt;, &lt;img&gt;</code> 등 외부 리소스는 별도로 다운로드 요청을 보내며, CSS와 JavaScript 파일이 이 과정에서 로드됩니다.</p>
</li>
</ul>
<p><strong>2. CSS 파일 다운로드 및 파싱</strong></p>
<ul>
<li><p>브라우저는 HTML 파싱 중 <link rel="stylesheet"> 태그를 만나면, CSS 파일을 서버에서 다운로드받아 파싱합니다.</p>
</li>
<li><p>CSS 파일은 브라우저의 렌더링 엔진에 의해 분석되어 CSSOM (CSS Object Model) 트리로 변환됩니다.</p>
</li>
<li><p>CSSOM은 각 HTML 요소에 적용할 스타일을 정의하며, DOM과 결합하여 최종 화면에 표시될 구조를 만듭니다.</p>
</li>
</ul>
<p><strong>3. DOM과 CSSOM 결합 (Render Tree 생성)</strong></p>
<ul>
<li><p>브라우저는 파싱된 DOM 트리와 CSSOM 트리를 결합하여 렌더 트리 (Render Tree) 를 만듭니다.</p>
</li>
<li><p>렌더 트리는 화면에 실제로 그려질 요소들만 포함하며, display: none 속성이 있는 요소나 <head> 태그 내부의 내용은 제외됩니다.</p>
</li>
<li><p>이 렌더 트리는 각 요소의 위치와 스타일 정보를 포함한 시각적 구조입니다.</p>
</li>
</ul>
<p><strong>4. 레이아웃 단계 (Layout)</strong></p>
<ul>
<li><p>레이아웃 단계에서는 렌더 트리를 기반으로 각 요소의 정확한 위치와 크기를 계산합니다. 이 과정을 <strong>&quot;reflow&quot;</strong>라고도 부릅니다.</p>
</li>
<li><p>브라우저는 페이지의 뷰포트 크기, 콘텐츠 크기, 그리고 CSS 스타일에 따라 각 요소가 어디에 배치될지를 계산합니다.</p>
</li>
</ul>
<p><strong>5. 페인팅 (Painting)</strong></p>
<ul>
<li>페인팅 단계에서는 각 요소의 스타일(색상, 배경 이미지, 텍스트 등)을 렌더 트리에 적용하여 화면에 그려질 픽셀 데이터를 생성합니다.</li>
</ul>
<ul>
<li>이 단계에서 요소들은 layers (레이어)로 나뉘어 처리되며, 각 레이어는 페인팅 작업을 통해 색상, 그림자, 텍스트, 경계선 등의 시각적 스타일을 적용받습니다.</li>
</ul>
<p><strong>6. 컴포지팅 (Compositing)</strong></p>
<ul>
<li>페인팅 작업이 끝난 후, 여러 레이어로 나뉘어진 요소들은 최종적으로 합쳐져 화면에 렌더링됩니다. 이 과정을 <strong>컴포지팅 (Compositing)</strong>이라고 합니다.<ul>
<li>브라우저는 각 레이어를 GPU를 통해 빠르게 렌더링하고, 화면에 그려질 최종 이미지를 사용자에게 표시합니다.</li>
</ul>
</li>
</ul>
<p><strong>7. JavaScript 실행 (자바스크립트의 영향)</strong></p>
<ul>
<li><p>HTML 파싱 중 <script> 태그를 만나면 JavaScript 파일이 다운로드되고, 자바스크립트 엔진에 의해 실행됩니다.</p>
</li>
<li><p>JavaScript는 DOM을 동적으로 변경할 수 있기 때문에, 브라우저는 DOM이나 CSSOM을 다시 파싱하거나 레이아웃, 페인팅, 컴포지팅을 다시 해야 하는 경우가 있습니다. 이를 reflow나 repaint라고 합니다.</p>
</li>
<li><p>예를 들어, DOM에 새로운 요소를 추가하거나 CSS 스타일을 변경하면 브라우저는 레이아웃을 다시 계산하고, 그에 따라 화면을 다시 그립니다.</p>
</li>
</ul>
<p><strong>8. 최종 렌더링</strong></p>
<ul>
<li><p>모든 과정이 끝나면, 브라우저는 화면에 최종적으로 렌더된 페이지를 사용자에게 보여줍니다.</p>
</li>
<li><p>이 모든 과정은 매우 짧은 시간 안에 이루어지며, 브라우저는 빠르게 사용자가 웹 페이지와 상호작용할 수 있도록 처리합니다.</p>
</li>
</ul>
<h4 id="csr-client-side-rendering-vs-ssr-server-side-rendering">CSR (Client-Side Rendering) vs SSR (Server-Side Rendering)</h4>
<p><strong>1. Client-Side Rendering (CSR)</strong></p>
<ul>
<li><p>CSR에서는 <strong>클라이언트(브라우저)</strong>가 HTML, CSS, JavaScript 파일을 받아서 클라이언트 측에서 렌더링을 수행합니다.</p>
</li>
<li><p>초기 페이지 로드 시 서버는 기본 HTML만 제공하고, JavaScript 파일을 다운로드한 후, 브라우저가 이 파일을 실행하여 필요한 데이터를 가져와 화면에 동적으로 그립니다.</p>
</li>
</ul>
<p><strong>장점</strong></p>
<ul>
<li><p>빠른 사용자 인터랙션: 페이지 내에서 빠른 네비게이션과 동적 콘텐츠 업데이트가 가능해 사용자 경험이 부드러워집니다.</p>
</li>
<li><p>더 적은 서버 부담: 서버는 기본 HTML과 JavaScript 파일만 제공하며, 나머지 처리는 클라이언트에서 이루어집니다. 서버 자원을 아낄 수 있습니다.</p>
</li>
<li><p>애플리케이션과 같은 사용자 경험: CSR은 SPA(Single Page Application) 방식으로 페이지 전환 없이 데이터를 업데이트해 앱처럼 사용할 수 있습니다.  </p>
</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li><p><strong>초기 로딩 속도 느림</strong>: 처음에 빈 HTML을 받고, JavaScript 파일을 로드하고 실행한 후에야 콘텐츠가 나타나므로 초기 로딩이 느릴 수 있습니다.</p>
</li>
<li><p><strong>SEO(Search Engine Optimization) 문제</strong>: 클라이언트 측에서만 렌더링이 이루어지기 때문에, 검색 엔진 크롤러가 제대로 콘텐츠를 인덱싱하지 못할 가능성이 있습니다. 일부 검색 엔진은 JavaScript 실행을 지원하지 않기 때문입니다.</p>
</li>
<li><p><strong>JavaScript 의존성</strong>: 브라우저에서 JavaScript가 비활성화된 경우에는 사이트가 제대로 작동하지 않습니다.</p>
</li>
</ul>
<p><strong>2. Server-Side Rendering (SSR)</strong></p>
<ul>
<li><p>SSR에서는 서버에서 HTML을 모두 렌더링한 후에 클라이언트로 전송합니다. 클라이언트는 렌더링된 HTML을 받아 바로 화면에 표시할 수 있습니다.</p>
</li>
<li><p>서버에서 미리 HTML을 준비하여 전달하므로, 클라이언트는 초기 로드에서 완전한 페이지를 받아 볼 수 있습니다.  </p>
</li>
</ul>
<p><strong>장점</strong></p>
<ul>
<li><p><strong>빠른 초기 로딩</strong>: 서버에서 완전한 HTML을 바로 제공하므로, 초기 화면이 빠르게 표시됩니다.</p>
</li>
<li><p><strong>SEO 친화적</strong>: 검색 엔진 크롤러가 서버에서 완전한 HTML을 받아 바로 인덱싱할 수 있어 SEO가 잘 동작합니다.</p>
</li>
<li><p><strong>첫 페이지 유저 경험 향상</strong>: 사용자는 즉시 페이지의 콘텐츠를 확인할 수 있어 더 나은 사용자 경험을 제공합니다.</p>
</li>
</ul>
<p>단점</p>
<ul>
<li><p><strong>서버 부하 증가</strong>: 매번 페이지를 요청할 때마다 서버가 HTML을 생성하고 렌더링해야 하므로, 트래픽이 많아질 경우 서버에 부담이 가중됩니다.</p>
</li>
<li><p><strong>느린 사용자 인터랙션</strong>: 클라이언트가 서버에서 새로운 페이지를 다시 받아야 하므로, SPA처럼 부드러운 사용자 경험을 제공하기 어려울 수 있습니다.</p>
</li>
<li><p><strong>복잡한 구현</strong>: 서버에서 렌더링과 상태 관리를 해야 하므로, CSR에 비해 구현이 복잡하고 더 많은 리소스가 필요합니다.</p>
</li>
</ul>
<h4 id="restful-api의-원칙">RESTful API의 원칙</h4>
<p><strong>1. 리소스 기반 설계</strong></p>
<ul>
<li><p>RESTful API에서는** 리소스<strong>(Resource)를 기반으로 설계됩니다. 각 리소스는 고유한 **URI</strong>(Uniform Resource Identifier)를 통해 식별됩니다.</p>
</li>
<li><p>리소스는 일반적으로 <strong>명사</strong>를 사용해 식별하며, URI는 해당 리소스를 표현하는 경로를 나타냅니다.  </p>
</li>
</ul>
<p><strong>2. HTTP 메서드 사용</strong></p>
<ul>
<li><p><strong>HTTP 메서드</strong>를 사용하여 리소스에 대한 작업을 정의합니다. 각 메서드는 CRUD(Create, Read, Update, Delete) 작업을 표현하는 데 사용됩니다.  </p>
</li>
<li><p><strong>GET</strong>: 리소스 조회 (Read)</p>
</li>
<li><p><strong>POST</strong>: 리소스 생성 (Create)</p>
</li>
<li><p><strong>PUT</strong>: 리소스 전체 수정 (Update)</p>
</li>
<li><p><strong>PATCH</strong>: 리소스 부분 수정 (Partial Update)</p>
</li>
<li><p><strong>DELETE</strong>: 리소스 삭제 (Delete)</p>
</li>
</ul>
<p><strong>3. 상태 없음 (Stateless)</strong></p>
<ul>
<li>RESTful API는 <strong>Stateless</strong>(상태 비저장성)를 가져야 합니다. 즉, 서버는 각 클라이언트의 요청을 독립적으로 처리하며, 요청 간의 상태를 서버가 저장하지 않습니다.</li>
<li>클라이언트의 모든 요청은 필요한 정보를 전부 포함해야 하고, 서버는 해당 요청을 통해 모든 처리를 수행할 수 있어야 합니다.</li>
</ul>
<p>예시: 클라이언트가 API를 호출할 때, 매 요청마다 인증 토큰 등을 포함하여 서버가 별도의 세션 상태를 기억하지 않도록 설계해야 합니다.</p>
<p><strong>4. 캐시 가능 (Cacheable)</strong></p>
<ul>
<li>RESTful API는 응답을 <strong>캐시</strong>할 수 있어야 합니다. 적절한 캐싱 전략을 통해 네트워크 트래픽을 줄이고, 성능을 개선할 수 있습니다.<ul>
<li>서버는 응답에 캐시 관련 헤더(<code>Cache-Control</code>, <code>Expires</code>, <code>ETag</code> 등)를 포함하여, 클라이언트나 프록시 서버가 결과를 캐싱할 수 있도록 합니다.</li>
</ul>
</li>
</ul>
<p>*<em>5. 계층화된 시스템 (Layered System)  *</em></p>
<ul>
<li><p>RESTful API는 계층화된 아키텍처를 가져야 합니다. 클라이언트는 중간 서버(프록시, 게이트웨이 등)를 통해 최종 서버에 접근할 수 있으며, 이러한 계층을 명확히 인식할 필요는 없습니다.  </p>
</li>
<li><p>이 원칙을 통해 시스템은 확장성 및 보안 측면에서 이점을 얻을 수 있습니다.</p>
</li>
</ul>
<p><strong>예시</strong>: 클라이언트는 직접적인 서버가 아닌 프록시 서버를 통해 API 요청을 처리할 수 있고, 클라이언트는 이러한 중간 단계를 알 필요가 없습니다.  </p>
<p>*<em>6. 일관된 인터페이스 (Uniform Interface) *</em> </p>
<ul>
<li><p><strong>RESTful API는 일관된 인터페이스를 제공해야 합니다. 클라이언트와 서버가 서로 독립적으로 발전할 수 있도록, 인터페이스 설계가 일관되고 자주 변경되지 않아야 합니다.</strong></p>
</li>
<li><p>RESTful API에서는 <strong>URI</strong>를 통해 리소스를 식별하고, HTTP 메서드를 통해 리소스에 대한 작업을 일관되게 처리합니다.</p>
</li>
</ul>
<h4 id="restful-api를-설계할-때의-이점">RESTful API를 설계할 때의 이점</h4>
<p> ** 1. 확장성** </p>
<ul>
<li>서버와 클라이언트가 독립적으로 동작할 수 있으므로, 시스템 확장성이 좋아집니다. API 자체가 리소스에 대한 명확한 인터페이스를 제공하기 때문에 서버 확장, 클라이언트 확장 모두 용이합니다.</li>
</ul>
<p><strong>2. 유지 보수성 (Maintainability)</strong></p>
<ul>
<li>일관된 인터페이스와 리소스 기반 설계를 따름으로써, API가 쉽게 이해되고 유지보수할 수 있습니다. 특히, 여러 팀이 협력할 때 유용합니다.</li>
</ul>
<ol start="3">
<li><p><strong>클라이언트-서버 분리 (Client-Server Separation)</strong></p>
<ul>
<li>클라이언트와 서버가 명확히 분리되므로, 클라이언트는 서버의 내부 구현을 몰라도 되고, 서버는 클라이언트의 상태를 유지할 필요가 없습니다. 이렇게 독립적인 개발이 가능해집니다.</li>
</ul>
</li>
<li><p><strong>재사용성 (Reusability)</strong></p>
<ul>
<li>리소스 중심 설계로, 동일한 API를 다양한 용도로 재사용할 수 있습니다. 예를 들어, 여러 서비스가 동일한 사용자 정보를 활용할 때 동일한 API를 사용하면 됩니다.</li>
</ul>
</li>
</ol>
<h4 id="cors-cross-origin-resource-sharing">CORS (Cross-Origin Resource Sharing)</h4>
<p><strong>CORS가 무엇인지, 그리고 브라우저에서 어떻게 동작하는지 설명</strong></p>
<p><strong>CORS</strong>(Cross-Origin Resource Sharing)는 웹 애플리케이션이 <strong>다른 출처</strong>의 리소스에 접근할 수 있도록 허용하는 <strong>보안 메커니즘</strong>입니다. CORS는 <strong>동일 출처 정책</strong>(Same-Origin Policy, SOP)을 완화하기 위해 사용됩니다.</p>
<p><strong>동일 출처 정책</strong>(Same-Origin Policy): 브라우저에서 보안상의 이유로, 스크립트가 자신이 로드된 <strong>출처</strong>(Origin)와 다른 출처의 리소스에 접근하는 것을 제한하는 보안 기능입니다.  </p>
<p><strong>CORS의 동작 방식</strong>
CORS는 클라이언트(주로 브라우저)와 서버 간의 요청 및 응답에 포함된 헤더를 통해 동작합니다. CORS 요청은 두 가지 형태로 나뉩니다: <strong>단순 요청(Simple Requests)</strong>과 <strong>사전 요청</strong>
(Preflight Requests).</p>
<p><strong>1. 단순 요청 (Simple Requests)</strong></p>
<p>단순 요청은 다음 조건을 모두 만족할 때 발생</p>
<ul>
<li><p>HTTP 메서드가 GET, POST, 또는 HEAD일 때.</p>
<ul>
<li>요청 헤더에 허용된 헤더만 포함될 때 (Accept, Accept-Language, Content-Language, Content-Type 등이 허용됨).</li>
</ul>
</li>
</ul>
<p>이 경우, 클라이언트는 요청을 보내고, 서버는 응답에 CORS 헤더를 포함시켜 클라이언트가 요청이 허용되었는지 판단할 수 있게 합니다.</p>
<p><strong>2. 사전 요청 (Preflight Requests) **
단순 요청이 아닌 경우(예: 메서드가 <code>PUT</code>, <code>DELETE</code>이거나 사용자 정의 헤더가 포함된 경우), 브라우저는 실제 요청을 보내기 전에 **사전 요청(Preflight Request)</strong>을 보냅니다. 이 요청은 서버에 특정 요청이 허용되는지 확인하기 위한 안전장치입니다.</p>
<p>사전 요청은 HTTP 메서드 OPTIONS를 사용하여 전송되며, 서버는 요청을 허용할지 여부를 헤더로 응답합니다.</p>
<ul>
<li><p><strong>Access-Control-Allow-Methods</strong>: 서버가 허용하는 HTTP 메서드를 지정합니다.</p>
</li>
<li><p><strong>Access-Control-Allow-Headers</strong>: 클라이언트가 사용할 수 있는 요청 헤더 목록을 지정합니다.</p>
</li>
<li><p><strong>Access-Control-Max-Age</strong>: 클라이언트가 사전 요청 결과를 얼마나 오래 캐시할 수 있는지 지정합니다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 완주하지 못한 선수]]></title>
            <link>https://velog.io/@white0_0/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%99%84%EC%A3%BC%ED%95%98%EC%A7%80-%EB%AA%BB%ED%95%9C-%EC%84%A0%EC%88%98</link>
            <guid>https://velog.io/@white0_0/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%99%84%EC%A3%BC%ED%95%98%EC%A7%80-%EB%AA%BB%ED%95%9C-%EC%84%A0%EC%88%98</guid>
            <pubDate>Sun, 29 Sep 2024 10:43:51 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-설명">문제 설명</h3>
<p>수많은 마라톤 선수들이 마라톤에 참여하였습니다. 단 한 명의 선수를 제외하고는 모든 선수가 마라톤을 완주하였습니다.</p>
<p>마라톤에 참여한 선수들의 이름이 담긴 배열 participant와 완주한 선수들의 이름이 담긴 배열 completion이 주어질 때, 완주하지 못한 선수의 이름을 return 하도록 solution 함수를 작성해주세요.</p>
<h3 id="제한사항">제한사항</h3>
<ul>
<li><p>마라톤 경기에 참여한 선수의 수는 1명 이상 100,000명 이하입니다.</p>
</li>
<li><p>completion의 길이는 participant의 길이보다 1 작습니다.</p>
</li>
<li><p>참가자의 이름은 1개 이상 20개 이하의 알파벳 소문자로 이루어져 있습니다.</p>
</li>
<li><p>참가자 중에는 동명이인이 있을 수 있습니다.</p>
</li>
</ul>
<h3 id="입출력-예">입출력 예</h3>
<p><img src="https://velog.velcdn.com/images/white0_0/post/9d618577-9d35-4824-801b-7a289b68b662/image.png" alt=""></p>
<h3 id="입출력-예-설명">입출력 예 설명</h3>
<p>예제 #1
&quot;leo&quot;는 참여자 명단에는 있지만, 완주자 명단에는 없기 때문에 완주하지 못했습니다.</p>
<p>예제 #2
&quot;vinko&quot;는 참여자 명단에는 있지만, 완주자 명단에는 없기 때문에 완주하지 못했습니다.</p>
<p>예제 #3
&quot;mislav&quot;는 참여자 명단에는 두 명이 있지만, 완주자 명단에는 한 명밖에 없기 때문에 한명은 완주하지 못했습니다.</p>
<h3 id="정답코드">정답코드</h3>
<pre><code>function solution(participant, completion) {

    const obj = {};
    for(const p of participant) {
        if(obj[p]){
            obj[p] += 1
        }else obj[p] = 1;
    }

    for(const c of completion) {
        if(obj[c]){
            obj[c] -= 1
        }
    }

    for(const key in obj) {
        if(obj[key] === 1){
            return key
        }
    }
}</code></pre><h3 id="코드설명">코드설명</h3>
<ol>
<li><p><strong>빈 객체 생성</strong>: 우선 빈 객체 <code>obj</code>를 생성하여, 참가자의 이름을 키로 사용해 이름의 등장 횟수를 저장할 수 있게 합니다.</p>
</li>
<li><p><strong>참가자 카운팅</strong>: 첫 번째 for문에서 <code>participant</code> 배열을 순회하며, 참가자의 이름을 객체 <code>obj</code>에 저장합니다.</p>
</li>
</ol>
<ul>
<li>만약 이름이 이미 <code>obj</code>에 있다면(즉, 동명이인이 있을 경우), 해당 이름에 1을 더해 <strong>동명이인의 수</strong>를 카운팅합니다.</li>
<li>이름이 처음 등장하면, 객체에 그 이름을 <strong>1</strong>로 설정해줍니다.</li>
</ul>
<ol start="3">
<li><strong>완주자 제외</strong>: 두 번째 for문에서는 <code>completion</code> 배열을 순회하며, obj에서 완주자의 이름을 찾아 값을 <strong>1씩 감소</strong>시킵니다.</li>
</ol>
<ul>
<li>이렇게 하면 <strong>완주자는 값</strong>이 0이 되고, <strong>완주하지 못한 참가자</strong>는 값이 1로 남아있게 됩니다.</li>
</ul>
<ol start="4">
<li><strong>완주하지 못한 사람 찾기</strong>: 마지막으로 <code>obj</code>의 키들을 순회하면서, 값이 1인 이름을 찾습니다. 이 <strong>이름이 완주하지 못한 참가자</strong>이므로, 해당 이름을 반환합니다.
<a href="https://school.programmers.co.kr/learn/courses/30/lessons/42576">문제 링크</a></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 카드 뭉치]]></title>
            <link>https://velog.io/@white0_0/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%B9%B4%EB%93%9C-%EB%AD%89%EC%B9%98</link>
            <guid>https://velog.io/@white0_0/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%B9%B4%EB%93%9C-%EB%AD%89%EC%B9%98</guid>
            <pubDate>Thu, 26 Sep 2024 15:47:35 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-설명">문제 설명</h3>
<p>코니는 영어 단어가 적힌 카드 뭉치 두 개를 선물로 받았습니다. 코니는 다음과 같은 규칙으로 카드에 적힌 단어들을 사용해 원하는 순서의 단어 배열을 만들 수 있는지 알고 싶습니다.</p>
<p>원하는 카드 뭉치에서 카드를 순서대로 한 장씩 사용합니다.
한 번 사용한 카드는 다시 사용할 수 없습니다.
카드를 사용하지 않고 다음 카드로 넘어갈 수 없습니다.
기존에 주어진 카드 뭉치의 단어 순서는 바꿀 수 없습니다.
예를 들어 첫 번째 카드 뭉치에 순서대로 [&quot;i&quot;, &quot;drink&quot;, &quot;water&quot;], 두 번째 카드 뭉치에 순서대로 [&quot;want&quot;, &quot;to&quot;]가 적혀있을 때 [&quot;i&quot;, &quot;want&quot;, &quot;to&quot;, &quot;drink&quot;, &quot;water&quot;] 순서의 단어 배열을 만들려고 한다면 첫 번째 카드 뭉치에서 &quot;i&quot;를 사용한 후 두 번째 카드 뭉치에서 &quot;want&quot;와 &quot;to&quot;를 사용하고 첫 번째 카드뭉치에 &quot;drink&quot;와 &quot;water&quot;를 차례대로 사용하면 원하는 순서의 단어 배열을 만들 수 있습니다.</p>
<p>문자열로 이루어진 배열 <code>cards1, cards2</code>와 <code>원하는 단어 배열 goal</code>이 매개변수로 주어질 때, cards1과 cards2에 적힌 단어들로 goal를 만들 있다면 &quot;Yes&quot;를, 만들 수 없다면 &quot;No&quot;를 return하는 solution 함수를 완성해주세요.</p>
<h3 id="제한사항">제한사항</h3>
<ul>
<li><p>1 ≤ cards1의 길이, cards2의 길이 ≤ 10</p>
<ul>
<li><p>1 ≤ cards1[i]의 길이, cards2[i]의 길이 ≤ 10</p>
</li>
<li><p>cards1과 cards2에는 서로 다른 단어만 존재합니다.</p>
</li>
</ul>
</li>
<li><p>2 ≤ goal의 길이 ≤ cards1의 길이 + cards2의 길이</p>
<ul>
<li>1 ≤ goal[i]의 길이 ≤ 10</li>
<li>goal의 원소는 cards1과 cards2의 원소들로만 이루어져 있습니다.
cards1, cards2, goal의 문자열들은 모두 알파벳 소문자로만 이루어져 있습니다.<h3 id="입출력-예">입출력 예</h3>
<img src="https://velog.velcdn.com/images/white0_0/post/e899b13b-90b6-4940-9161-9ac8563b1f22/image.png" alt=""></li>
</ul>
</li>
</ul>
<h3 id="입출력-예-설명">입출력 예 설명</h3>
<p>입출력 예 #1</p>
<p>본문과 같습니다.</p>
<p>입출력 예 #2</p>
<p>cards1에서 &quot;i&quot;를 사용하고 cards2에서 &quot;want&quot;와 &quot;to&quot;를 사용하여 &quot;i want to&quot;까지는 만들 수 있지만 &quot;water&quot;가 &quot;drink&quot;보다 먼저 사용되어야 하기 때문에 해당 문장을 완성시킬 수 없습니다. 따라서 &quot;No&quot;를 반환합니다.</p>
<h3 id="정답코드">정답코드</h3>
<pre><code>function solution(cards1, cards2, goal) {
    let idx1 = 0;
    let idx2 = 0;

    let count = 0;
    let n = goal.length;
    for (let i = 0; i &lt; goal.length; i++) {
        if (idx1 &lt; cards1.length &amp;&amp; goal[i] === cards1[idx1]) {
            count++;
            idx1++; // cards1에서 카드를 사용한 경우
        } else if (idx2 &lt; cards2.length &amp;&amp; goal[i] === cards2[idx2]) {
            count++;
            idx2++; // cards2에서 카드를 사용한 경우
        } 
    }

    return count === n ? &quot;Yes&quot;: &quot;No&quot;; // 모든 goal 요소를 성공적으로 찾은 경우
}</code></pre><h3 id="코드설명">코드설명</h3>
<p>원하는 단어 배열 goal의 길이만큼 반복문을 실행하면서 각 카드뭉치 시작점에 존재하면 count++ 한 뒤 마지막에 goal의 문장을 만들었다면 count가 goal 배열 길이만큼 증가했을 것이다. cards1과 cards2 배열의 길이가 다르므로 각각 인덱스 시작점인 0을 변수로 두고 count++ 할때 해당 idx 값도 증가시켰다. </p>
<h3 id="다른-사람의-풀이">다른 사람의 풀이</h3>
<pre><code>function solution(cards1, cards2, goal) {
    let j = 0;
    let k = 0;
    for(let i=0;i&lt;goal.length;i++){
        if(goal[i] == cards1[j]) j++;
        else if(goal[i] == cards2[k]) k++;
        else return &quot;No&quot;
    }
    return &quot;Yes&quot;;
}</code></pre><p>내 정답 코드와 비교했을 때 더 간단하고 가독성도 좋은 것 같다. 여기서는 각 카드의 index 시작점은 똑같이 설정해 주지만 인덱스의 범위를 확인하지 않는데 왜 그러냐면 만약 만들 수 있는 goal 문장이 매개변수로 주어지면 index 범위를 초과할 상황 없이 반복문에서 return &quot;No&quot; 하지 않고 정상적으로 종료될 것이다. 그리고 반복문에 else return &quot;No&quot;를 해줌으로써 일치하는 단어가 없다면 이후에 반복을 할 필요 없이 조기 종료도 가능하다.</p>
<p>다른 사람의 풀이를 보는 것만으로도 많은 도움이 되는 것 같다. 더 효율적인 방법을 알게되는 것은 물론 나중에 다른 문제를 풀 때도 응용도 가능하다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 기능개발]]></title>
            <link>https://velog.io/@white0_0/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EA%B8%B0%EB%8A%A5%EA%B0%9C%EB%B0%9C</link>
            <guid>https://velog.io/@white0_0/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EA%B8%B0%EB%8A%A5%EA%B0%9C%EB%B0%9C</guid>
            <pubDate>Thu, 26 Sep 2024 13:48:04 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-설명">문제 설명</h3>
<p>프로그래머스 팀에서는 기능 개선 작업을 수행 중입니다. 각 기능은 진도가 100%일 때 서비스에 반영할 수 있습니다.</p>
<p>또, 각 기능의 개발속도는 모두 다르기 때문에 뒤에 있는 기능이 앞에 있는 기능보다 먼저 개발될 수 있고, 이때 뒤에 있는 기능은 앞에 있는 기능이 배포될 때 함께 배포됩니다.</p>
<p>먼저 배포되어야 하는 순서대로 <code>작업의 진도가 적힌 정수 배열 progresses</code>와 각 <code>작업의 개발 속도가 적힌 정수 배열 speeds</code>가 주어질 때 각 배포마다 몇 개의 기능이 배포되는지를 return 하도록 solution 함수를 완성하세요.</p>
<h3 id="제한-사항">제한 사항</h3>
<ul>
<li>작업의 개수(progresses, speeds배열의 길이)는 100개 이하입니다.</li>
<li>작업 진도는 100 미만의 자연수입니다.</li>
<li>작업 속도는 100 이하의 자연수입니다.</li>
<li>배포는 하루에 한 번만 할 수 있으며, 하루의 끝에 이루어진다고 가정합니다. 예를 들어 진도율이 95%인 작업의 개발 속도가 하루에 4%라면 배포는 2일 뒤에 이루어집니다.<h3 id="입출력-예">입출력 예</h3>
<img src="https://velog.velcdn.com/images/white0_0/post/6db6ebd3-dbb0-415d-8147-cc5e0ae07805/image.png" alt=""></li>
</ul>
<h3 id="입출력-예-설명">입출력 예 설명</h3>
<h3 id="입출력-예-1">입출력 예 #1</h3>
<p>첫 번째 기능은 93% 완료되어 있고 하루에 1%씩 작업이 가능하므로 7일간 작업 후 배포가 가능합니다.
두 번째 기능은 30%가 완료되어 있고 하루에 30%씩 작업이 가능하므로 3일간 작업 후 배포가 가능합니다. 하지만 이전 첫 번째 기능이 아직 완성된 상태가 아니기 때문에 첫 번째 기능이 배포되는 7일째 배포됩니다.
세 번째 기능은 55%가 완료되어 있고 하루에 5%씩 작업이 가능하므로 9일간 작업 후 배포가 가능합니다.</p>
<p>따라서 7일째에 2개의 기능, 9일째에 1개의 기능이 배포됩니다.</p>
<h3 id="입출력-예-2">입출력 예 #2</h3>
<p>모든 기능이 하루에 1%씩 작업이 가능하므로, 작업이 끝나기까지 남은 일수는 각각 5일, 10일, 1일, 1일, 20일, 1일입니다. 어떤 기능이 먼저 완성되었더라도 앞에 있는 모든 기능이 완성되지 않으면 배포가 불가능합니다.</p>
<p>따라서 5일째에 1개의 기능, 10일째에 3개의 기능, 20일째에 2개의 기능이 배포됩니다.</p>
<h3 id="정답코드">정답코드</h3>
<pre><code>class Queue {
  items = [];
  front = 0;
  rear = 0;

  push(item) {
    this.items.push(item);
    this.rear++;
  }

  pop() {
    return this.items[this.front++];
  }

  isEmpty() {
    return this.front === this.rear;
  }
}

function solution(progresses, speeds) {
  const queue = new Queue();

  const answer = [];
  progresses.forEach((progress, i) =&gt; {
    let remainingDays = Math.ceil((100 - progress) / speeds[i]);
    queue.push(remainingDays);
  });

  while (!queue.isEmpty()) {
    let count = 1;
    let num = queue.pop();
    while (num &gt;= queue.items[queue.front]) {
      count++;
      queue.pop();
    }
    answer.push(count);
  }
  return answer;
}
</code></pre><h3 id="코드설명">코드설명</h3>
<p>일단 문제에서 뒤에 있는 기능이 앞에 있는 기능보다 먼저 개발될 수 있고, 이때 뒤에 있는 기능은 앞에 있는 기능이 배포될 때 함께 배포된다는 문구를 봐서 앞에부터 처리되니 큐로 해결해야겠다고 생각했다. 그렇기 때문에 완벽한 큐의 역할을 한다고는 할 수 없지만 class Queue를 만들어서 생성한다. </p>
<p>정답을 return할 배열을 선언하고 작업이 남은 날짜를 구하기 위해 <code>작업의 진도가 적힌 정수 배열 progresses</code>를 순회하면서 작업이 완료되면 100프로이므로 100에서 진행된 진도 값을 빼준 값을 작업 속도로 나눠서 구한다. 그다음 queue에 push 한다. 여기서 주의할 점은 Math.ceil() 올림 처리 함수를 사용해야 한다는 것이다. 왜냐하면 지금 작업의 진도가 30이고 작업의 개발 속도도 30일 경우 2.xx 일이 필요한데 우리는 3일로 처리를 해야 하기 때문이다. </p>
<p>그다음 while으로 큐에 남은 것이 없을 때까지 반복문을 실행한다. 최소 배포되는 것이 1개는 존재하기 때문에 count = 1을 할당했고 남은 일수를 pop 하여 num 변수에 담는다. 현재 작업 날짜보다 같거나 작으면 배포를 같이 할 수 있다는 의미이기 때문에 count++ 한 뒤 pop() 하여 제거한다. 현재 작업 날짜보다 큰 경우는 조건을 충족하지 못하기 때문에 count++하지 않고 1만 push 하면 된다. 이렇게 queue 요소가 남지 않을 때까지 반복하면 성공이다.</p>
<p>문제의 핵심은 큐를 이용하는 것과 남은 작업 일을 구한다는 아이디어와 Math.ceil로 올림 처리하는 부분이 중요한 것 같다!</p>
<h3 id="다른-정답코드">다른 정답코드</h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js] TanStack Query V5 무한 스크롤 구현]]></title>
            <link>https://velog.io/@white0_0/Next.js-TanStack-Query-V5-%EB%AC%B4%ED%95%9C-%EC%8A%A4%ED%81%AC%EB%A1%A4-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@white0_0/Next.js-TanStack-Query-V5-%EB%AC%B4%ED%95%9C-%EC%8A%A4%ED%81%AC%EB%A1%A4-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Tue, 24 Sep 2024 01:27:46 GMT</pubDate>
            <description><![CDATA[<h3 id="useinfinitequery-사용-이유">useInfiniteQuery 사용 이유</h3>
<p>팀 프로젝트 요구사항에 따르면 <strong>초기 페이지</strong>에서는 서버 사이드 렌더링(SSR)을 통해 <strong>목록 10개</strong>를 보여주고, 이후에는 클<strong>라이언트 사이드</strong>에서 <strong>무한 스크롤 기능</strong>을 통해 목록을 추가로 요청하여 받아와야 한다. 이 기능을 효율적으로 구현하기 위해 TanStack Query(v5)의 <code>useInfiniteQuery</code> 훅과 <strong>react-intersection-observer</strong> 라이브러리의 <code>useInView</code> 훅을 사용했다.</p>
<p><code>useInfiniteQuery</code>를 사용하면 <strong>페이지네이션 처리</strong>와 <strong>다음 페이지 요청</strong>을 간편하게 관리할 수 있고, 자동으로 <strong>캐싱 및 리페치</strong> 기능을 제공하여 성능 최적화를 할 수 있다. 특히, 페이지네이션과 무한 스크롤을 함께 구현할 때 API 호출을 효율적으로 제어할 수 있다는 장점이 있다.</p>
<p>이전에는 TanStack Query에서 <code>useQuery</code>와 <code>useMutation</code>을 간단하게 사용해 본 경험이 있지만, 무한 스크롤과 같은 기능은 이번 프로젝트에서 처음으로 시도해 보는 부분이다.</p>
<h4 id="초기-ssr을-실행하는-홈페이지-코드">초기 SSR을 실행하는 홈페이지 코드</h4>
<pre><code>
const Home = async () =&gt; 
  const datas = await response.json() 데이터를 받아 왓다고 가정
  return (
    &lt;div&gt;
        &lt;CardList datas={datas} /&gt;
      &lt;/div&gt;

  );
};

export default Home;
</code></pre><p>실제 코드에서 그냥 정말 이번 기능 구현하면서 볼 부분말고 다 지웠다. 서버에서 데이터를 10개 받아서 CardList에 전달했다고 가정하겠다.</p>
<p>코드를 설명하기전에 내가 사용한 <code>useInfiniteQuery</code>의 설정 옵션과 리턴 값으로 사용한 것들에 대해서 설명하겠다. 따로 이 코드만 추출해서 보여주겠다.</p>
<h3 id="useinfinitequery-구성-코드">useInfiniteQuery 구성 코드</h3>
<p><code>useInfiniteQuery</code>는 반환 값 속성이 몇 개 추가된 것을 제외하면 <code>useQuery</code>와 동일하다고 공식 문서에서 설명하고 있다.</p>
<pre><code>const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
    queryFn: async ({ pageParam = 0 }) =&gt; {
      const axios = getInstance();
      const response = await axios.get&lt;Gathering[]&gt;(&#39;gatherings&#39;, {
        params: { offset: pageParam, limit },
      });
      return response.data;
    },
    getNextPageParam: (lastPage, allPages) =&gt; {
      return lastPage.length === limit ? allPages.length * limit : undefined; // 다음 offset 계산
    },
    initialPageParam: 0,
    queryKey: [&#39;gatherings&#39;],
    initialData: {
      pages: [gatherings],
      pageParams: [0],
    },
  });</code></pre><p>여기서 사용한 <code>useInfiniteQuery</code> 옵션들에 대해서 먼저 적어보겠다.</p>
<h3 id="useinfinitequery-구성-옵션">useInfiniteQuery 구성 옵션</h3>
<p><strong>queryFn:</strong> 데이터를 패칭하는 비동기 함수</p>
<p>*<em>getNextPageParam: *</em>서버에서 데이터를 페이지 단위로 불러오는 경우 다음 페이지의 요청을 관리하기 위해 사용</p>
<p><strong>getNextPageParam 매개변수</strong></p>
<ul>
<li><strong>lastPage:</strong> 마지막으로 받아온 페이지의 데이터이다. 이를 통해 마지막 페이지가 있는지, 다음 페이지가 있는지 등의 정보를 확인할 수 있다.</li>
<li><strong>allPages:</strong> 지금까지 불러온 모든 페이지의 데이터 배열이다. 이 배열을 통해 현재까지 불러온 모든 데이터를 확인할 수 있다.</li>
</ul>
<p>*<em>initialPageParam: *</em>첫 번째 페이지를 가져올 때 사용할 기본 페이지 매개변수 (필수)</p>
<p>*<em>queryKey: *</em>서버에서 데이터를 가져오는 요청을 식별하는 데 사용되는 키</p>
<p>*<em>initialData: *</em>  쿼리가 처음 실행될 때 사용할 초기 데이터</p>
<ul>
<li><code>pages</code>는 각 페이지의 데이터를 포함하는 배열</li>
<li><code>pageParams</code>는 각 페이지에 대한 파라미터 배열이다.  여기서는 초기값으로 0을 설정하여 첫 번째 페이지의 오프셋을 나타낸다.</li>
</ul>
<h3 id="useinfinitequery-반환-속성">useInfiniteQuery 반환 속성</h3>
<p>*<em>data: *</em>data는 두 가지 속성을 갖는다.</p>
<ul>
<li><p><code>data.pages</code>: 모든 페이지를 포함하는 배열</p>
</li>
<li><p><code>data.pageParams</code>: 모든 페이지 매개변수를 포함하는 배열</p>
</li>
</ul>
<p><strong>fetchNextPage:</strong></p>
<ul>
<li><code>fetchNextPage</code> 함수는 무한 스크롤이나 페이지네이션을 사용할 때, 다음 페이지의 데이터를 불러오는 역할</li>
<li><code>fetchNextPage</code>는 queryFn을 호출하여 다음 페이지의 데이터를 비동기적으로 가져오는 함수</li>
</ul>
<p><strong>hasNextPage</strong>: 가져올 다음 페이지가 있는 경우 true, (getNextPageParam 옵션을 통해 알려짐)</p>
<h3 id="클라이언트-측-무한스크롤-구현-전체-코드">클라이언트 측 무한스크롤 구현 전체 코드</h3>
<p>서버 사이드에서 패칭해온 데이터를 전달받는데 따로 변수를 선언하여 저장해서 사용할 필요가 없다. <code>useInfiniteQuery</code>의 구성 옵션 중 하나인 <code>initialData</code> 속성을 사용하여 초기 데이터를 저장한다. 주의할 점은 바로 데이터를 저장하는 게 아니라 코드처럼 pages 속성에 배열로 감싸서 실제 전달 데이터를 추가하고 pageParams 값도 마찬가지로 줘야 한다.</p>
<p><code>queryFn</code>은 실제 데이터를 요청하는 로직을 포함하는 비동기 함수이다. 함수를 따른 파일에 작성하고 import 하여 사용해도 상관없지만 지금은 한눈에 알아볼 수 있게 작성하였다. params의 offset과 limit를 넘겨주고 있는데 이건 요청하는 api를 보고 어떻게 페이지 단위로 데이터를 요청할지 결정하는 것이기 때문에 절대적인 것이 아니다.</p>
<p><code>getNextPageParam</code> 서버에서 데이터를 페이지 단위로 불러오는 경우 다음 페이지의 요청을 관리하기 위해 사용한다고 했다.<br>매개변수로 전달되는 <code>lastPage</code>는  마지막으로 받아온 페이지의 데이터인데 내 코드에서는 지금 limit를 10으로 고정해두고 데이터를 받아오기 때문에 [데이터1, 데이터2, ....데이터 10] 이렇게 마지막으로 받아온 데이터를 의미한다. 그래서 마지막으로 받아온 데이터의 배열이 길이가 limit와 같다면, 10개를 가져왔다면 다음 페이지가 존재한다고 말할 수 있기 때문에 조건문을 사용한 것이고 있을 경우 <code>allPages.length 길이</code> * <code>limit</code>를 사용하여 offset 값을 전달한다. 이 전달 값은 queryFn의 매개변수로 전달되는 pageParam으로 전달되는 것이다.</p>
<p>allPages는 지금까지 불러온 모든 페이지의 데이터 배열인데 예를 들어, 10개의 데이터를 2번 불러왔다고 가정하면 <code>allPages = [ [데이터 1, 데이터 2, ... 데이터 10], [데이터 11, 데이터 12, ... 데이터 20]]</code> 이런 데이터를 가지고 있을 것이다. 2차원 배열이라는 것에 주의하자!</p>
<p>이제 <code>useInfiniteQuery</code>로 부터 반환된 속성인 <code>data, fetchNextPage, hasNextPage</code> 사용하여 데이터를 요청하고 페이지에 보여줘야 하는데 설명하기전에 무한 스크롤을 하려면 받아온 콘텐츠의 끝 지점에 도달했다는 것을 알아야 하는데 그러기 위해서 <code>react-intersection-observer</code> 라이브러리의 <code>useInView</code> 훅을  알아야 한다. </p>
<pre><code>&#39;use client&#39;;

import { Gathering } from &#39;@/lib/definition&#39;;
import React, { useEffect } from &#39;react&#39;;
import ProgressCard from &#39;./ProgressCard&#39;;
import { useInfiniteQuery } from &#39;@tanstack/react-query&#39;;
import { getInstance } from &#39;@/utils/axios&#39;;
import { useInView } from &#39;react-intersection-observer&#39;;

const limit = 10;

const ProgressCardList = ({ gatherings }: { gatherings: Gathering[] }) =&gt; {
  const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
    queryFn: async ({ pageParam = 0 }) =&gt; {
      const axios = getInstance();
      const response = await axios.get&lt;Gathering[]&gt;(&#39;gatherings&#39;, {
        params: { offset: pageParam, limit },
      });
      return response.data;
    },
    getNextPageParam: (lastPage, allPages) =&gt; {
      return lastPage.length === limit ? allPages.length * limit : undefined; // 다음 offset 계산
    },
    initialPageParam: 0,
    queryKey: [&#39;gatherings&#39;],
    initialData: {
      pages: [gatherings],
      pageParams: [0],
    },
  });

  const { ref, inView } = useInView({
    threshold: 0.5, // 50% 보일 때
  });

  // inView가 true일 때 다음 페이지를 fetch
  useEffect(() =&gt; {
    if (inView &amp;&amp; hasNextPage) {
      fetchNextPage();
    }
  }, [inView, hasNextPage, fetchNextPage]);

  return (
    &lt;div className=&quot;flex flex-col items-center gap-6 mt-6&quot;&gt;
      {data?.pages
        .flat() // 2차원 배열을 1차원으로 평탄화
        .map((gathering, index) =&gt; &lt;ProgressCard gathering={gathering} key={index} /&gt;)}
      &lt;div ref={ref} /&gt; {/* 스크롤 감지를 위한 ref 추가 */}
    &lt;/div&gt;
  );
};

export default ProgressCardList;
</code></pre><h3 id="react-intersection-observer란">react-intersection-observer란?</h3>
<p><code>react-intersection-observer</code>는  React에서 <code>Intersection Observer API</code>를 간편하게 사용할 수 있도록 제공하는 라이브러리다. 이 API는 특정 요소가 뷰포트(사용자의 화면)에 보이는지 여부를 감지하는 데 사용된다. 주로 무<strong>한 스크롤(Infinite Scroll), 지연 로딩(Lazy Loading), 애니메이션 트리거</strong> 등에서 활용된다.</p>
<p><strong>실제 내 코드 중 사용 부분</strong></p>
<pre><code> const { ref, inView } = useInView({
    threshold: 0.5, // 50% 보일 때
  });</code></pre><ul>
<li><code>useInView</code>: <code>react-intersection-observer</code>에서 제공하는 훅으로, 해당 요소가 뷰포트에 들어오는지 감지</li>
<li><code>ref</code>: 감지할 요소에 할당되는 참조값이다. 이 요소가 화면에 보이는지 감지한다.</li>
<li><code>inView</code>: 요소가 뷰포트에 보이면 true, 그렇지 않으면 false</li>
<li><code>threshold</code>: 요소가 얼마나 보여야 <code>inView</code>가 <code>true</code>가 되는지를 결정하는 옵션입니다. <code>0.5</code>는 요소의 50%가 보일 때 <code>true</code>가 된다.</li>
</ul>
<p><a href="https://www.npmjs.com/package/react-intersection-observer">react-intersection-observer 링크</a></p>
<p>아래 코드를 보면 <code>data?.pages.flat().map()</code> 부분에서는 <code>data.pages</code>가 2차원 배열로 되어 있기 때문에, 이를 평탄화하여 1차원 배열로 만든 후 .map()을 통해 각 요소에 접근한다. </p>
<p>그 후, 목록의 마지막에 <code>&lt;div&gt;</code> 요소를 추가하고, 이 <code>div</code>에 <code>useInView</code> 훅에서 반환된 ref를 연결한다. ref는 스크롤 감지 대상 요소를 지정하는데 사용된다. 이 div가 화면에 나타나는지(뷰포트에 들어오는지) 감지하여, 감지되었을 때 추가 데이터를 불러오는 트리거 역할을 한다.</p>
<p><code>useEffect</code> 훅은 <code>inView</code>가 <code>true</code>일 때, 즉 <code>div</code> <strong>요소가 뷰포트에 50% 이상 보이게 되면</strong>(<code>threshold: 0.5</code>) <code>fetchNextPage</code> 함수를 호출하여 다음 페이지의 데이터를 요청한다. 이때 <code>hasNextPage</code>는 <strong>다음 페이지가 존재하는지 여부</strong>를 나타내는 값으로, true일 때만 추가 데이터를 불러온다.</p>
<p><strong>무한스크롤 구현 전체 코드중 일부분</strong></p>
<pre><code>  const { ref, inView } = useInView({
    threshold: 0.5, // 50% 보일 때
  });

  // inView가 true일 때 다음 페이지를 fetch
  useEffect(() =&gt; {
    if (inView &amp;&amp; hasNextPage) {
      fetchNextPage();
    }
  }, [inView, hasNextPage, fetchNextPage]);

  return (
    &lt;div className=&quot;flex flex-col items-center gap-6 mt-6&quot;&gt;
      {data?.pages
        .flat() // 2차원 배열을 1차원으로 평탄화
        .map((gathering, index) =&gt; &lt;ProgressCard gathering={gathering} key={index} /&gt;)}
      &lt;div ref={ref} /&gt; {/* 스크롤 감지를 위한 ref 추가 */}
    &lt;/div&gt;
  );</code></pre><p>위와 같이 <code>useInfiniteQuery</code>와 <code>react-intersection-observer</code>의 <code>useInView</code> 훅을 사용하여 서버에서 데이터를 받아 무한 스크롤 기능을 구현할 수 있었다. <strong>SSR</strong>로 초기 데이터를 렌더링하고, 이후 <strong>클라이언트 측에서 추가 데이터를 자동으로 요청</strong>하는 방식으로 요구사항을 충족시켰다. </p>
<p>이번 프로젝트에서 처음으로 <strong>무한 스크롤 기능</strong>을 구현해보았는데, TanStack Query와 Intersection Observer API 덕분에 효율적으로 간단하게 구현할 수 있었다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 크레인 인형뽑기 게임]]></title>
            <link>https://velog.io/@white0_0/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%ED%81%AC%EB%A0%88%EC%9D%B8-%EC%9D%B8%ED%98%95%EB%BD%91%EA%B8%B0-%EA%B2%8C%EC%9E%84</link>
            <guid>https://velog.io/@white0_0/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%ED%81%AC%EB%A0%88%EC%9D%B8-%EC%9D%B8%ED%98%95%EB%BD%91%EA%B8%B0-%EA%B2%8C%EC%9E%84</guid>
            <pubDate>Fri, 20 Sep 2024 15:20:45 GMT</pubDate>
            <description><![CDATA[<p>이번 문제는 설명과 특히 제공되는 그림이 많아서 문제에 대한 설명대신 문제 링크를 걸고 정답 코드와 코드설명만 적기로 결정했다.
<a href="https://school.programmers.co.kr/learn/courses/30/lessons/64061">문제링크</a></p>
<h3 id="정답코드">정답코드</h3>
<pre><code>function solution(board, moves) {
  const basket = [];
  let score = 0;

  moves.forEach((move) =&gt; {
    let doll = 0;

    for (let row = 0; row &lt; board.length; row++) {
      if (board[row][move - 1] !== 0) {
        doll = board[row][move - 1];
        board[row][move - 1] = 0;
        break;
      }
    }


    if (doll === 0) return;

    if (basket.length === 0) {
      basket.push(doll);
    } else {
      if (basket[basket.length - 1] === doll) {
        basket.pop();
        score += 2;
      } else {
        basket.push(doll);
      }
    }
  });

  return score;
}
</code></pre><h3 id="코드설명">코드설명</h3>
<p>이번 문제에 핵심은 스택을 활용하는 것이다. 
인형을 담을 basket 변수 스택으로 활용할 것이다.
score는 전체 사라진 인형 숫자 값이다.</p>
<p>매개변수로 전달되는 moves는 크레인을 작동시킨 위치가 담긴 배열인데 결국 moves 배열 길이만큼 동작을 수행해야 한다. 따라서 forEach로 moves 배열 길이만큼 순회하면서 내부에서 뽑은 인형의 모양을 저장할 변수 doll을 선언 후 0으로 초기화해준다. </p>
<p>그다음 각각의 forEach 순회 중에 인형 위치가 담긴 배열 board를 board의 행 길이만큼 탐색하는데 세로로 탐색하므로 행은 고정이기 때문에 board[row]이고 그다음 move 배열은 위치를 1번 2번으로 나타내지만 인덱스는 0번부터이므로 - 1을 해준 값을 열의 위치로 사용해
준다. </p>
<p>0은 빈칸을 나타내므로 빈칸이 아닐 경우에만 뽑은 인형의 번호를 doll 변수에 저장 후 뽑은 위치에 0을 할당하고 반목문을 빠져나온다. 아래에서 현재 인형을 담은 basket에 인형이 없으면 무조건 push를 하고 인형이 존재한다면 최상위에 인형 모양을 나타내는 값과 뽑았던 인형 모양 값 doll과 비교하여 같으면 push 하지 않고 사라지기 때문에 기존 basket에 있던 인형을 pop 하고 사라진 인형 개수를 나타내는 score 변수에 2를 더해준다. </p>
<p>중간에 if (doll === 0) return;문의 역할은 예를 들어 1번 라인을 탐색하였는데 인형이 없으면 doll 값이 0일 것이다. 그런데 basket에는 0을 push 하면 안 되기 때문에 추가해 준 조건이다.</p>
<p>마지막에 총 사라진 개수를 return 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바스크립트] 표현식과 문]]></title>
            <link>https://velog.io/@white0_0/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%91%9C%ED%98%84%EC%8B%9D%EA%B3%BC-%EB%AC%B8</link>
            <guid>https://velog.io/@white0_0/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%91%9C%ED%98%84%EC%8B%9D%EA%B3%BC-%EB%AC%B8</guid>
            <pubDate>Fri, 20 Sep 2024 13:51:40 GMT</pubDate>
            <description><![CDATA[<h3 id="값이란">값이란?</h3>
<p><strong>값은 식(표현식 expression)이 평가되어 생성된 결과</strong>를 말한다. 평가란 식을 해석해서 값을 생성하거나 참조하는 것을 의미한다. </p>
<p>아래와 같은 식을 이렇게 말할 수 있다. 100 + 100은 평가되어 숫자 값 200을 생성한다.
<code>100 + 100; // 200</code></p>
<p>변수는 <strong>하나의 값</strong>을 저장하기 위해 확보한 메모리 공간 자체 또는 그 메모리 공간을 식별하기 위해 붙인 이름이다. 변수에 할당되는 것도 값이다.</p>
<p><code>let sum = 3 + 3</code></p>
<p>sum 변수에 할당되는 것은 3 + 3이 아니라 3 + 3이 평가된 결과인 값 6이다. 3 + 3은 할당 이전에 평가되어 값을 생성한다. 값은 다양한 방법으로 생성이 가능하다. 기본적인 방법은 리터럴이다.</p>
<h3 id="리터럴">리터럴</h3>
<p>리터럴은 사람이 이해할 수 있는 문자 또는 약속된 기호를 사용해 <strong>값을 생성하는 표기법</strong>이다. </p>
<p>아래의 5는 단순히 숫자 5가 아니라 숫자 리터럴이다. 사람이 이해할 수 있는 아라비아 숫자를 사용해 숫자 리터럴 5를 코드에 기술하면 자바스크립트 엔진은 평가하여 숫자 값 5를 생성한다.
<code>5</code></p>
<p>자바스크립트 엔진은 코드가 실행되는 시점인 런타임에 리터럴을 평가해 값을 생성한다. </p>
<h3 id="표현식">표현식</h3>
<p>표현식은 값으로 평가될 수 있는 문이다. 즉, 표현식이 평가되면 새로운 값을 생성하거나 기존 값을 참조한다. </p>
<p>20 + 20은 리터럴과 연산자로 이뤄져 있다. 하지만 20 + 20도 평가되어 숫자 값 40을 생성하므로 표현식이다.
<code>let goal = 20 + 20</code></p>
<p>변수 식별자를 참조하면 변수 값으로 평가된다. 식별자 참조는 값을 생성하지는 않지만 값으로 평가되므로 표현식이다. 
<code>goal; // -&gt; 40</code> </p>
<p>표현식은 리터럴, 식별자(변수, 함수 등의 이름), 연산자, 함수 호출 등의 조합으로 이뤄질 수 있다. </p>
<p><strong>표현식과 표현식이 평가된 값은 동치</strong>이다. 예를 들어 1 + 2 = 3에서 1 + 2는 3과 동치다. 자바스크립트의 코드로 예시를 들어보겠다.</p>
<p><code>let x = 1 + 2</code> 이런 코드가 있고 x + 5를 한다고 하자. 우리는 그냥 변수에 저장된 값에 5를 더해야겠다고 사용하지만 + 연산자는 좌황과 우항의 값을 산술 연산하는 연산자이므로 좌항과 우항에는 값이 위치해야 한다.</p>
<p>x는 식별자 표현식이기 때문에 3으로 평가가 된다. 따라서 숫자 값이 위치해야 할 자리에 표현식 x를 사용할 수 있는 것이다. </p>
<p>문법적으로 값이 위치할 수 있는 자리에도 표현식도 위치할 수 있다.</p>
<h3 id="문">문</h3>
<p><strong>문(statement)은 프로그램을 구성하는 기본 단위이자 최소 실행 단위</strong>이다. 문의 집합으로 이뤄진 것이 프로그램이며, 문을 작성하고 순서에 맞게 나열하는 것이 프로그래밍이다. </p>
<p>문은 여러 토큰으로 구성된다. <strong>토큰이란 문법적인 의미를 가지며, 문법적으로 더 이상 나눌 수 없는 코드의 기본 요소</strong>를 의미한다. 예를 들어, 키워드, 식별자, 연산자, 리터럴, 세미콜론 등의 기호는 문법적인 의미를 가지며, 문법적으로 더 이상 나눌 수 없는 코드의 기본 요소인 토큰이다.</p>
<h3 id="표현식인-문과-아닌-문">표현식인 문과 아닌 문</h3>
<p>변수 선언문은 값으로 평가될 수 없으므로 표현식이 아니다.
<code>let x;</code></p>
<p><code>x = 1 +2</code>는 표현식이면서 완전한 문이기도 하다.</p>
<p>표현 식인 문과 표현식이 아닌 문을 구별하는 가장 간단하고 명료한 방법은 변수에 할당하는 것이다. 표현 식인 문은 값으로 평가되므로 변수에 할당할 수 있고 표현식이 아닌 문은 값으로 평가할 수 없으므로 변수에 할당 시 에러가 발생한다. </p>
<p><code>let x = var y;</code> // 예상치 못한 토큰 var 에러</p>
<p><code>x = 100</code> 할당문은 표현식이지만 완전한 문이기도하다.</p>
<h3 id="완료-값">완료 값</h3>
<p>크롬 개발자 도구에서 표현식이 아닌 문을 실행하면 언제나 undefined를 출력하는 데 이를 완료 값이라고 한다. 완료 값은 표현식의 평가 결과가 아니다. 따라서 다른 값처럼 변수에 할당하거나 참조할 수 없다. </p>
<p><img src="https://velog.velcdn.com/images/white0_0/post/d7587356-e5d5-433e-ac49-f8e811a62b15/image.png" alt=""></p>
<p>위에 사진과 다르게 표현식이 문을 실행하면 언제나 평가된 값을 반환한다. 
<img src="https://velog.velcdn.com/images/white0_0/post/3ba7482f-0064-4d14-8d69-9b3d858aee5e/image.png" alt=""></p>
<p><strong>참조한 책 이미지</strong>: 저자 이웅모
<img src="https://velog.velcdn.com/images/white0_0/post/d3578bb0-e7f6-407d-9df8-3f98c37146a4/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 주식가격]]></title>
            <link>https://velog.io/@white0_0/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%A3%BC%EC%8B%9D%EA%B0%80%EA%B2%A9</link>
            <guid>https://velog.io/@white0_0/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%A3%BC%EC%8B%9D%EA%B0%80%EA%B2%A9</guid>
            <pubDate>Wed, 18 Sep 2024 08:12:48 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-설명">문제 설명</h3>
<p>초 단위로 기록된 주식가격이 담긴 배열 prices가 매개변수로 주어질 때, 가격이 떨어지지 않은 기간은 몇 초인지를 return 하도록 solution 함수를 완성하세요.</p>
<h3 id="제한사항">제한사항</h3>
<ul>
<li><p>prices의 각 가격은 1 이상 10,000 이하인 자연수입니다.</p>
</li>
<li><p>prices의 길이는 2 이상 100,000 이하입니다.</p>
<h3 id="입출력-예">입출력 예</h3>
<p><img src="https://velog.velcdn.com/images/white0_0/post/d6059c90-485c-4603-806a-f8e15655c57f/image.png" alt=""></p>
</li>
</ul>
<h3 id="입출력-예-설명">입출력 예 설명</h3>
<ul>
<li><p>1초 시점의 ₩1은 끝까지 가격이 떨어지지 않았습니다.</p>
</li>
<li><p>2초 시점의 ₩2은 끝까지 가격이 떨어지지 않았습니다.</p>
</li>
<li><p>3초 시점의 ₩3은 1초뒤에 가격이 떨어집니다. 따라서 1초간 가격이 떨어지지 않은 것으로 봅니다.</p>
</li>
<li><p>4초 시점의 ₩2은 1초간 가격이 떨어지지 않았습니다.</p>
</li>
<li><p>5초 시점의 ₩3은 0초간 가격이 떨어지지 않았습니다.</p>
</li>
</ul>
<h3 id="정답코드">정답코드</h3>
<pre><code>function solution(prices) {
    const n = prices.length;
    const answer=new Array(n).fill(0);

    const stack = [0];

    for(let i=1; i&lt;n; i++) {
        while(stack.length &gt; 0 &amp;&amp; prices[i] &lt; prices[stack[stack.length -1]]) {

            const  j = stack.pop();
            answer[j] = i - j;
        }
        stack.push(i);
    }

    while(stack.length &gt; 0) {
        const j = stack.pop();
        answer[j] = n - 1 - j;
    }

    return answer;
}</code></pre><h3 id="코드설명">코드설명</h3>
<p>문제를 푸는 방식은 다음과 같다.
N^2인 복잡도를 이용하는 방식이 아닌 O(N)을 활용하기 때문에 배열의 모든 요소를 순차적으로 비교하는 것이 아니라 주식 가격이 처음으로 떨어지는 지점을 이용하여 다른 지점의 길이를 효율적으로 계산해야 한다.</p>
<p>이게 무슨 말이냐면 배열 <code>[1, 5, 6, 3, 7]</code>이 있다고 가정하겠다</p>
<p>여기서는 가격이 계속 오르다가 6 -&gt; 3으로 주식 가격이 처음 떨어지는데 이때 6의 길이는 1로 확정된다. 1초 동안 유지했다는 의미이고  answer 배열에 <strong>요소 6의 인덱스 번호</strong>인 answer[2]에 1을 저장하고 추가적으로 pop도 해줘야 하는데 왜냐면 길이가 확정됐기 때문에 필요가 없어진다. </p>
<p>그다음 5 &gt; 3이므로 떨어졌다는 의미이고 3<strong>(요소 3의 인덱스)</strong> - 1<strong>(요소 5의 인덱스)</strong> 하여 길이는 2로 확정되고 answer 배열에 <strong>요소 5의 인덱스 번호</strong>인 answer[1]에 2를 저장하고 마찬가지로 pop을 실행한다. 2초 동안 유지했다는 의미이다.</p>
<p>이어서 1 &lt; 3 이번에는 1초 주가의 가격은 떨어진 게 아니므로 pop을 하지 않고 3요소의 인덱스 3을 push 해준다. 마지막으로 7과 3을 비교하는데 가격이 떨어지지 않았으므로 7의 <strong>인덱스</strong> 4를 push 한다.</p>
<p>이렇게 되면 스택에 남는 요소들은 길이가 확정되지 않은, 가격이 끝까지 떨어지지 않은 주식이다.
이 요소들도 마지막에 pop 하면서 길이를 구해주면 된다. </p>
<p>이 부분은 코드의 마지막 while 문에 해당하는데 지금 stack = [0, 3, 4]이므로 pop 하면 4가 나올 것이고 j = 4 , answer[4] = 5 - 1- 4 =&gt; answer[4] = 0이 들어간다. 마지막 요소이므로 길이가 0인 게 맞다. 나머지도 대입해 보면 길이가 나올 것이다.</p>
<p>코드만 보면 길이가 짧기 때문에 엄청 간단해 보이지만 각 주가마다의 길이를 어떻게 확정하는지와 길이를 표현하기 위해 stack에는 index 값을 넣어주는 것, 가격이 떨어졌다는 것을 확인하는 조건문 마지막에 남은 stack 요소들의 길이를 구하는 것처럼 고려해야 할 부분들이 많았던 것 같다. </p>
<p> <a href="https://school.programmers.co.kr/learn/courses/30/lessons/42584">문제 링크</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 짝지어 제거하기]]></title>
            <link>https://velog.io/@white0_0/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%A7%9D%EC%A7%80%EC%96%B4-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B0%EA%B8%B0-Lv2</link>
            <guid>https://velog.io/@white0_0/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%A7%9D%EC%A7%80%EC%96%B4-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B0%EA%B8%B0-Lv2</guid>
            <pubDate>Sun, 15 Sep 2024 07:05:01 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-설명">문제 설명</h3>
<p>짝지어 제거하기는, 알파벳 소문자로 이루어진 문자열을 가지고 시작합니다. 먼저 문자열에서 같은 알파벳이 2개 붙어 있는 짝을 찾습니다. 그다음, 그 둘을 제거한 뒤, 앞뒤로 문자열을 이어 붙입니다. 이 과정을 반복해서 문자열을 모두 제거한다면 짝지어 제거하기가 종료됩니다. 문자열 S가 주어졌을 때, 짝지어 제거하기를 성공적으로 수행할 수 있는지 반환하는 함수를 완성해 주세요. 성공적으로 수행할 수 있으면 1을, 아닐 경우 0을 리턴해주면 됩니다.</p>
<p>예를 들어, 문자열 S = baabaa 라면</p>
<p>b aa baa → bb aa → aa →</p>
<p>의 순서로 문자열을 모두 제거할 수 있으므로 1을 반환합니다.</p>
<h3 id="제한사항">제한사항</h3>
<ul>
<li>문자열의 길이 : 1,000,000이하의 자연수</li>
<li>문자열은 모두 소문자로 이루어져 있습니다.<h3 id="입출력-예">입출력 예</h3>
<img src="https://velog.velcdn.com/images/white0_0/post/6d0369b7-bf09-45a3-8d3c-8a02d1a9fe32/image.png" alt=""></li>
</ul>
<h3 id="입출력-예-설명">입출력 예 설명</h3>
<p><strong>입출력 예 #1</strong></p>
<p>위의 예시와 같습니다.</p>
<p><strong>입출력 예 #2</strong></p>
<p>문자열이 남아있지만 짝지어 제거할 수 있는 문자열이 더 이상 존재하지 않기 때문에 0을 반환합니다.</p>
<h3 id="정답코드">정답코드</h3>
<pre><code>function solution(s) {
  const stack = [];

  for (let c of s) {
    if (stack.length === 0) {
      stack.push(c);
    } else {
      const top = stack[stack.length - 1];
      if (top === c) stack.pop();
      else stack.push(c);
    }
  }

  return stack.length === 0 ? 1 : 0;
}
</code></pre><h3 id="코드설명">코드설명</h3>
<p>제한사항에서 문자열의 길이가 1,000,000 이하의 자연수이기 때문에 복잡도가(N^2)인 알고리즘으로 접근하면 무조건 시간 초과가 발생한다. 스택을 활용하면 간단하게 구현할 수 있다.</p>
<p>우선 처음에 배열의 데이터가 없다면 데이터를 push 해서 삽입한다. 데이터가 없으면 다음 데이터와 쌍인지 아닌지 비교도 할 수 없다. </p>
<p>그런 다음 데이터와 직전에 넣은 데이터 즉, 제일 최상단의 데이터와 비교한다. 일치한다면 pop() 실행하고 아니라면 push()한다. 이렇게 입력으로 들어온 문자열을 순회하면서 최종적으로 stack 데이터가 남아 있는지 여부를 확인하여 남아 있지 않다면 1 아니라면 0을 반환한다.</p>
<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/12973">프로그래머스 문제 링크</a></p>
]]></description>
        </item>
    </channel>
</rss>