<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>one1_programmer.log</title>
        <link>https://velog.io/</link>
        <description>프론트 개발과 클라우드 환경에 관심이 많습니다:)</description>
        <lastBuildDate>Thu, 12 Mar 2026 04:39:34 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>one1_programmer.log</title>
            <url>https://velog.velcdn.com/images/one1_programmer/profile/23dd4ae5-dc71-4862-8955-ea1b342795a6/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. one1_programmer.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/one1_programmer" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[알고리즘] 노란불 신호등 (파이썬 풀이)]]></title>
            <link>https://velog.io/@one1_programmer/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%85%B8%EB%9E%80%EB%B6%88-%EC%8B%A0%ED%98%B8%EB%93%B1-%ED%8C%8C%EC%9D%B4%EC%8D%AC-%ED%92%80%EC%9D%B4</link>
            <guid>https://velog.io/@one1_programmer/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%85%B8%EB%9E%80%EB%B6%88-%EC%8B%A0%ED%98%B8%EB%93%B1-%ED%8C%8C%EC%9D%B4%EC%8D%AC-%ED%92%80%EC%9D%B4</guid>
            <pubDate>Thu, 12 Mar 2026 04:39:34 GMT</pubDate>
            <description><![CDATA[<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/468371">프로그래머스 &gt; 2025 카카오 하반기 1차 &gt; 노란불 신호등</a></p>
<p>문제 조건에는 신호의 지속 시간이 정해져있지 않으므로 시간은 무한으로 흐를 수 있다. 즉 각 신호등은 시간의 흐름에 따라 특정 주기(초록불, 노란불, 빨간불의 지속 시간의 합)로 신호를 반복한다. 이때, 유한한 시간을 범위로 놓고 판단할 필요가 있는데, 이 범위는 세 신호등의 최소 공통 주기가 될 것이다. 즉, 모든 신호등이 노란불이 되는 경우가 존재한다면, 그 경우는 세 신호등 주기의 최소 공배수를 내에 있을 것이다.</p>
<blockquote>
<p>알고리즘 분류는 &#39;수학을 곁들인 완전탐색&#39; 정도로 볼 수 있을 것 같다.</p>
</blockquote>
<h1 id="🤔개념">🤔개념</h1>
<p>우선 파이썬에서 최소공배수를 구하는 방법을 알아보자.</p>
<h2 id="파이썬에서-최소공배수-구하기">파이썬에서 최소공배수 구하기</h2>
<h3 id="mathlcm-함수-사용-python-39-버전-이상"><code>math.lcm()</code> 함수 사용 (Python 3.9 버전 이상)</h3>
<pre><code class="language-python">  import math

  # 12와 18의 최소공배수 구하기
  result = math.lcm(12, 18)
  print(result)  # 출력: 36</code></pre>
<h3 id="mathgcd-함수-활용"><code>math.gcd()</code> 함수 활용</h3>
<p>gcd 함수로 최대공약수를 먼저 구한 뒤, 두 수의 곱을 최대공약수로 나눌 수 있다.</p>
<pre><code class="language-python">import math

a = 12
b = 18
# math.gcd로 최대공약수를 구해 활용
lcm = (a * b) // math.gcd(a, b)
print(lcm)  # 출력: 36</code></pre>
<h3 id="여러-수의-최소공배수-구하기">여러 수의 최소공배수 구하기</h3>
<p><code>math.lcm</code> 은 여러 인자를 한 번에 계산할 수 있다 (Python 3.9 버전 이상).</p>
<pre><code class="language-python">import math

# 12, 18, 24의 최소공배수
result = math.lcm(12, 18, 24)
print(result)  # 출력: 72</code></pre>
<h3 id="39-미만-버전-또는-함수-구현">3.9 미만 버전 또는 함수 구현</h3>
<p><code>math.gcd</code> 만 사용 가능한 경우, <code>functools.reduce</code> 를 사용하여 리스트 전체의 최소공배수를 구할 수 있다.</p>
<pre><code class="language-python">import math
from functools import reduce

def lcm(a, b):
    return abs(a * b) // math.gcd(a, b)

def lcm_list(numbers):
    return reduce(lcm, numbers)

print(lcm_list([12, 18, 24]))  # 출력: 72</code></pre>
<blockquote>
<p>프로그래머스의 파이썬 버전은 3.8(26.03 기준)이다.
<img src="https://velog.velcdn.com/images/one1_programmer/post/8347fa7f-d48b-48b3-96ca-525ab7eab1ce/image.png" alt="">
카카오 코테 당시의 버전은 3.9 이상이었던걸로 기억하긴 한다.</p>
</blockquote>
<h1 id="☺️풀이">☺️풀이</h1>
<h2 id="나의-풀이">나의 풀이</h2>
<p>각 신호등에 대한 노란불이 켜지는 시간을 배열로 따로 저장했다.</p>
<pre><code class="language-py">import math
from functools import reduce

def lcm(a, b):
    return abs(a*b)//math.gcd(a,b)

def lcm_list(numbers):
    return reduce(lcm, numbers)

def solution(signals):
    answer = 0
    주기s=[]
    for 초,노,빨 in signals:
        주기=초+노+빨
        주기s.append(주기)
    최소공통주기=lcm_list(주기s)

    노란불켜지는시간=[[0]*(최소공통주기+1) for _ in range(len(signals))]

    for i,sig in enumerate(signals):
        초,노,빨=sig
        주기=주기s[i]
        k=0
        while True:
            s=초+1+주기*k
            e=초+노+주기*k
            if 최소공통주기&lt;s:
                break
            # 노란불이 켜진 시간 범위만큼 해당 시각들은 0-&gt;1 변환
            for j in range(s,e+1):
                노란불켜지는시간[i][j]=1
            k+=1

    for 시각 in range(1,최소공통주기+1):
        cnt=0
        신호등갯수=len(signals)
        for sig in range(신호등갯수):
            if 노란불켜지는시간[sig][시각]==1:
                cnt+=1
        if cnt==신호등갯수:
            answer=시각
            break
    else:
        answer=-1

    return answer</code></pre>
<h3 id="채점-결과">채점 결과</h3>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/d13283bd-cbb6-48a7-86db-4ebdf6575352/image.png" alt=""></p>
<h2 id="다른-사람의-풀이">다른 사람의 풀이</h2>
<p>나머지를 활용한 다른 사람의 풀이를 참고하여 풀이를 보완해보았다.</p>
<pre><code class="language-py"># 다른 사람 풀이 참고: 나머지 활용
import math
from functools import reduce

def lcm(a,b):
    return abs(a*b)//math.gcd(a,b)

def lcm_list(numbers):
    return reduce(lcm,numbers)

def solution(signals):
    answer = 0
    주기s=[초+노+빨 for 초,노,빨 in signals]
    최소공통주기=lcm_list(주기s)

    for 시각 in range(1, 최소공통주기+1):
        모두노란불=True

        for 초,노,빨 in signals:
            주기=초+노+빨
            mod=시각%주기 # 나머지로 판단

            # 신호등 중 하나라도 해당 시각에 노란불이 아니면 나가리
            if not (초&lt;=mod&lt;=초+노-1):
                모두노란불=False
                break

        if 모두노란불:
            return 시각+1

    return -1</code></pre>
<h3 id="채점결과">채점결과</h3>
<p>나머지를 활용하면 시간/공간복잡도를 훨씬 줄일 수 있음을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/one1_programmer/post/43145472-2811-4cde-a40a-7ff839381101/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[테스트] MSW로 쿠키 모킹하기]]></title>
            <link>https://velog.io/@one1_programmer/%ED%85%8C%EC%8A%A4%ED%8A%B8-MSW%EB%A1%9C-%EC%BF%A0%ED%82%A4-%EB%AA%A8%ED%82%B9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@one1_programmer/%ED%85%8C%EC%8A%A4%ED%8A%B8-MSW%EB%A1%9C-%EC%BF%A0%ED%82%A4-%EB%AA%A8%ED%82%B9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 17 Feb 2026 10:49:47 GMT</pubDate>
            <description><![CDATA[<p>프로젝트에서 쿠키+세션 방식으로 비회원 로그인을 구현하였고, 이를 MSW로 어떻게 모킹했는지와 관련하여 포스팅했다.</p>
<p>Fetch API 응답 인스턴스를 사용하여 응답 쿠키를 모킹하는 것은 <code>Cookie</code> 헤더가 설정할 수 없는 &#39; Forbidden Header(금지된 헤더)&#39; 중 하나이기 때문에 문제가 된다. MSW는 응답의 무결성과 보안을 손상시키지 않으면서 응답 쿠키를 모킹할 수 있도록 <code>HttpResponse</code> 클래스에서 이러한 제한을 우회하는 방법을 제공한다.</p>
<h2 id="응답-모킹하기">응답 모킹하기</h2>
<p>기본적으로 쿠키의 정보는 <code>키=값</code> 을 <code>;</code> 로 구분하여 담아주며, 실제 담을 정보는 앞 쪽에 <code>HttpOnly</code>와 같은 부가 정보는 뒤쪽에 작성한다.</p>
<p>공식문서의 예제는 다음과 같다.</p>
<pre><code class="language-ts">http.post(&#39;/login&#39;, () =&gt; {
  return new HttpResponse(null, {
    headers: {
      // Setting the &quot;Set-Cookie&quot; header on the mocked response
      // will set the cookies on the `document` as if they were
      // received from the server.
      &#39;set-cookie&#39;: &#39;authToken=abc&#39;,
    },
  })
})</code></pre>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/39403f85-660a-4ee5-8aa1-4b2c11a1b23b/image.gif" alt=""></p>
<pre><code class="language-ts">export const userHandlers = [
  // 비회원 로그인 API
  http.post(&quot;/api/v1/users/guest-login&quot;, () =&gt; {
    const sessionId = Math.random().toString(36).substring(2, 15).toUpperCase();

    return HttpResponse.json(
      {
        timeStamp: new Date().toISOString(),
        code: &quot;SUCCESS&quot;,
        message: &quot;요청이 성공적으로 처리되었습니다.&quot;,
        result: {
          userId: generateRandomUserId(),
          nickname: generateRandomNickname(),
        },
      },
      {
        status: 200,
        headers: {
          &quot;set-cookie&quot;: `JSESSIONID=${sessionId}; path=/; httpOnly; sameSite=Lax`,
        },
      },
    );
  }),
];
</code></pre>
<blockquote>
<p>실제로 위와 같이 하나하나 옵션을 지정하기 불편하다. 따라서 쿠키 읽기/쓰기를 편리하게 할 수 있게 하는 유명한 라이브러리인 <a href="https://www.npmjs.com/package/cookie">npm cookie</a>를 사용하는 것을 권장한다. </p>
</blockquote>
<p>브라우저 Applications 탭의 Cookies 확인해보자.
<img src="https://velog.velcdn.com/images/one1_programmer/post/8289b3b8-3eff-453a-8a9d-1aeff9c9b5ae/image.png" alt=""></p>
<h2 id="요청-가로채기">요청 가로채기</h2>
<p>쿠키에 특정 데이터가 있는 지 확인하기 위해 요청을 가로챌 수 있다. 응답 resolver의 파라미터에서 제공된 <code>cookie</code> 를 통해 요청의 쿠키를 읽을 수 있다. <code>request.headers.get(&#39;cookie&#39;)</code>를 직접 읽을 수도 있지만 편의를 위해 제공된다.</p>
<pre><code class="language-ts">http.get(&#39;/api/user&#39;, ({ cookies }) =&gt; {
  if (!cookies.authToken) {
    return new HttpResponse(null, { status: 403 })
  }

  return HttpResponse.json({ name: &#39;John&#39; })
})</code></pre>
<p>사실, 작업 중에 MSW의 쿠키가 해당 탭에서 잘 안나타거나 예전 값이 반환되는 경우가 많았다. 아직 이유는 모르겠지만 개발 서버를 다시 실행해주면 어느정도 해결 가능했다.</p>
<blockquote>
<p>참고자료
<a href="https://www.npmjs.com/package/cookie">MSW 공식문서 - Mocking HTTP</a>
<a href="https://mswjs.io/docs/http/intercepting-requests/cookies">MSW 공식문서 - Intercepting requests
</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[웹] REST API 기본 개념]]></title>
            <link>https://velog.io/@one1_programmer/%EC%9B%B9-REST-API-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90</link>
            <guid>https://velog.io/@one1_programmer/%EC%9B%B9-REST-API-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90</guid>
            <pubDate>Wed, 04 Feb 2026 09:06:21 GMT</pubDate>
            <description><![CDATA[<p><strong>REST 원칙</strong>이란 REST(REpresentational State Transfer) API 설계의 중심에는 자원(Resource)이 있고, 자원에 대한 행위(HTTP Method)를 통해 자원을 처리하도록 설계하는 것을 말한다. </p>
<p>이때, 자원(resource)은 이름을 지닐 수 있는 모든 정보 및 개념적인 대상을 의미한다. </p>
<p>ex) 문서, 이미지, 자원들의 집합, 실존하는 대상 등</p>
<ul>
<li><p>구성요소</p>
<table>
<thead>
<tr>
<th>구성 요소</th>
<th>내용</th>
<th>표현 방법</th>
</tr>
</thead>
<tbody><tr>
<td>Resource</td>
<td>자원</td>
<td>HTTP URI</td>
</tr>
<tr>
<td>Verb</td>
<td>자원에 대한 행위</td>
<td>HTTP Method</td>
</tr>
<tr>
<td>Representations</td>
<td>자원에 대한 행위의 내용</td>
<td>HTTP Message Payload</td>
</tr>
</tbody></table>
</li>
</ul>
<p>REST API란 REST 원칙을 적용하여 서비스 API를 설계하는 것이고, RESTful API는 REST 원칙을 잘 지켜서 설계한 API를 뜻한다. </p>
<blockquote>
<p><strong>API</strong>
프로그램끼리 통신할 수 있도록 하는 중재자</p>
</blockquote>
<ul>
<li><p>장점</p>
<ul>
<li><p>Open API를 제공하기 쉽다.</p>
</li>
<li><p>멀티 플랫폼 지원 및 연동이 용이하다.</p>
</li>
<li><p>원하는 타입으로 데이터를 주고 받을 수 있다.</p>
</li>
<li><p>기존 웹 인프라(HTTP)를 그대로 사용할 수 있다.</p>
<blockquote>
<p>HTTP는 Restful API의 필수요소는 아니지만, RESTful API의 조건을 구현하기 용이하기 때문에 현업에서 주로 사용된다.</p>
</blockquote>
</li>
</ul>
</li>
<li><p>단점</p>
<ul>
<li>메서드로 수행할 수 있는 동작이 4가지(CRUD)로 한정된다.</li>
<li>분산 환경에는 부적합하다.</li>
</ul>
</li>
</ul>
<h1 id="🤔개념">🤔개념</h1>
<h2 id="uriuniform-resource-identifier"><strong>URI(Uniform Resource Identifier)</strong></h2>
<p>웹 상에서의 자원을 식별하기 위한 정보이다. 자원(Resource)을 식별(Identify)하는 통일(Uniform)된 방식으로 URI로 자원을 식별할 때는 ‘이름’ 또는 ‘위치’를 기반으로 식별한다.</p>
<ul>
<li><strong>URN(Uniform Resource Name):</strong> 이름으로 자원을 식별하는 방식</li>
<li><strong>URL(Uniform Resourc Locator):</strong> 위치로 자원을 식별하는 방식으로, 오늘날 인터넷 환경에서 웹 주소로써 자원 식별에 많이 사용된다.</li>
</ul>
<h3 id="url의-구조"><strong>URL의 구조</strong></h3>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/6308e60f-785b-47e3-96af-7b05af97b06d/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>구성</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>scheme</strong></td>
<td>자원에 접근하는 방법 (일반적으로 프로토콜)</td>
</tr>
<tr>
<td><strong>authority</strong></td>
<td>호스트를 특정할 수 있는 IP 주소나 도메인 네임</td>
</tr>
<tr>
<td><strong>path</strong></td>
<td>자원이 위치하고 있는 경로</td>
</tr>
<tr>
<td><strong>query</strong></td>
<td>URL에 대한 매개변수 역할을 하는 문자열로, 쿼리 문자열, 쿼리 파라미터 등으로 불린다. 각 쿼리는 <code>이름=값</code> 으로 명시되고, 쿼리들은 <code>&amp;</code> 로 구분된다.</td>
</tr>
<tr>
<td><strong>fragment</strong></td>
<td>자원의 일부분을 가리키기 위한 정보</td>
</tr>
</tbody></table>
<h2 id="rest와-uri">REST와 URI</h2>
<p>REST에서 URI는 특정 요청이 ‘어떤 자원’에 관한 것인지 표현해야 하고 또 가능한 한 ‘그것만’ 표현해야 한다. 또한 자원으로 무엇을 ‘하는가’를 나타내는 동사는 URI에 가능한 한 포함하지 않으며, 자원이 가리키는 것은 명사로 표현되어야 한다.</p>
<pre><code class="language-jsx">GET https://api.example.com/v1/todos/1</code></pre>
<p>요청의 메서드와 URI 자체가 그것의 목적을 명확히 나타낸다면, 개발자가 다른 팀과의 협업할 때나 공공 API를 처음으로 사용해야 할 때 큰 어려움 없이 새 API의 사용법을 익힐 수 있다.</p>
<h3 id="네이밍-컨벤션"><strong>네이밍 컨벤션</strong></h3>
<ul>
<li>리소스명은 동사가 아닌 명사를 사용한다. 즉, <code>get</code> 같은 행위에 대한 표현은 URI에 포함하지 않는다.</li>
<li>자원에 대한 행위는 오로지 HTTP 메서드로 표현하며, 분명한 목적으로 사용해야 한다.</li>
</ul>
<pre><code>| 메서드 | 행위 | 목적 | 페이로드 |
| --- | --- | --- | --- |
| GET | index/retrieve | 모든/특정 리소스를 조회 | x |
| POST | create | 리소스를 생성 | ○ |
| PUT | replace | 리소스의 전체를 교체 | ○ |
| PATCH | modify | 리소스의 일부를 수정 | ○ |
| DELETE | delete | 모든/특정 리소스를 삭제 | x |</code></pre><ul>
<li>소문자를 사용해야 한다.</li>
<li>언더바(<code>_</code>) 대신 대시(<code>-</code>)를 사용한다.</li>
<li>파일 확장자는 포함하지 않는다.</li>
<li>마지막에 슬래시(<code>/</code>)를 포함하지 않는다.</li>
<li>(선택사항) API 버전을 관리한다.<ul>
<li>환경은 항상 변하기 때문에 API의 signature가 변경될 수도 있음에 유의한다.</li>
<li>특정 API를 변경할 때는 반드시 하위 호환성을 보장해야 한다.</li>
</ul>
</li>
</ul>
<h2 id="제약-조건">제약 조건</h2>
<p>네이밍 컨벤션을 잘 지키는 것만으로는 RESTful하다고 말하지 못할 수 있다. 진정한 RESTful API가 되기 위해서 아래 6가지 스타일을 반드시 지켜야 한다.</p>
<h3 id="client-server-클라이언트-서버-구조"><strong>Client-Server (클라이언트-서버 구조)</strong></h3>
<p>REST 서버는 API를 제공하고 클라이언트는 사용자 인증이나 컨텍스트(세션, 로그인 정보) 등을 직접 관리하는 구조로 각각의 역할이 확실히 구분된다. 때문에 클라이언트와 서버에서 개발해야 할 내용이 명확해지고 서로 간 의존성이 줄어든다.</p>
<h3 id="stateless-무상태성"><strong>Stateless (무상태성)</strong></h3>
<p>HTTP는 stateless(무상태성)한 프로토콜이므로 HTTP 위에서 주로 동작하는 REST 또한 무상태성 성격을 갖는다. 다시 말해 작업을 위한 상태 정보를 따로 저장하고 관리하지 않는다. 세션 정보나 쿠키 정보를 별도로 저장하고 관리하지 않기 때문에 API 서버는 들어오는 요청만을 단순히 처리하면 된다. 덕분에 서비스의 자유도가 높아지고 서버에서 불필요한 정보를 관리하지 않음으로써 구현이 단순하다는 장점이 있다.</p>
<p>무상태성은 HTTP의 멱등성(idempotent)와도 관련이 있다. 이는 클라이언트가 같은 요청을 몇 번을 보내든 언제나 같은 응답이 돌아와야 한다는 특성이다. 정확하게는 실제 데이터가 바뀌었을 때를 제외하고, 첫 번째 작업을 수행한 뒤 여러 차례 적용해도 결과를 변경하지 않는 특성이라 할 수 있다. 멱등성을 지닌 HTTP 메서드로는 <code>GET</code>, <code>PUT</code>, <code>DELETE</code> 가 있다.</p>
<h3 id="caching-캐시-가능"><strong>Caching (캐시 가능)</strong></h3>
<p>클라이언트와 서버는 서로에 대해서는 기억하지 않아야 하지만, 자신이 어떤 응답을 받았는지와 어떤 응답을 보냈는지는 기억해두는 것이 권장된다.  </p>
<p>REST는 웹 표준인 HTTP를 그대로 사용하기 때문에 웹에서 사용하는 기존 인프라를 그대로 활용할 수 있다. 이를테면, HTTP 프로토콜 표준에서 사용하는 <code>Last-Modified</code> 태그나 <code>E-Tag</code> 등을 이용하면 캐싱 구현이 가능하다.</p>
<h3 id="layeredhierarchical-system-계층화"><strong>Layered(Hierarchical) system (계층화)</strong></h3>
<p>REST 서버는 다중 계층으로 구성될 수 있으며, 보안, 로드 밸런싱, 암호화 계층을 추가해 구조상의 유연성을 둘 수 있고 PROXY, 게이트웨이 같은 네트워크 기반의 중간매체를 사용할 수 있게 한다.</p>
<h3 id="uniform-interface"><strong>Uniform Interface</strong></h3>
<p>URI로 지정한 리소스에 대한 조작을 통일되고 한정적이고 일관된 인터페이스로 수행하는 아키텍처 스타일을 말한다.</p>
<ul>
<li><p><strong>Identification of Resources:</strong> 자원을 객체로 보며, 이때 객체는 시간이 지남에 따라 없었다가 생성되고 상태가 변화하고 파괴되어 없어진다는 특성을 지닌다. 따라서 특정한 객체를 식별하기 위해서는 해당 객체의 현재 상태를 보는 것만으로는 부족하고, 개별 객체에 대해 언제나 변하지 않는 불변값(고유한 식별자)를 부여해야 한다. 결국 서버의 개별 자원에 대한 고유한 식별자로 고유한 URI를 사용해야 한다는 의미이다.</p>
</li>
<li><p><strong>Manipulation of Resources through Representations:</strong> 표현(representations)은 ‘특정한 상태의 자원에 대한 표현’을 의미한다.</p>
</li>
<li><p><strong>Self-descriptive messages(자체 표현 메시지):</strong> 클라이언트와 서버 사이에 존재하는 중개자들에게 메시지의 내용을 어떻게 처리해야 하는 지에 대해 제대로 설명해야 한다. ex) host 헤더</p>
</li>
<li><p><strong>HATEOAS(Hypermedia as the Engine of Application State):</strong> 각 요청의 응답에 가용한 다른 요청들의 정보를 포함시키는 것이다. 해당 정보를 통해서 개발자는 API 문서들을 모두 뒤져보지 않아도 다음에 어떤 요청을 보낼 수 있는지 살펴볼 수 있다. 또한 API의 세부 사항이 변경되더라도 클라이언트에서 이 정보를 참조하게 만들면 클라이언트의 코드를 수정할 필요가 없어진다.</p>
<pre><code class="language-json">  // 출처: 유튜브 얄팍한 코딩사전
  {
    &quot;id&quot;: 1,
    &quot;title&quot;: &quot;Clean Code&quot;,
    &quot;author&quot;: &quot;Robert C. Martin&quot;,
    &quot;published_date&quot;: &quot;2008-08-01&quot;,
    &quot;isbn&quot;: &quot;9780132350884&quot;,
    &quot;status&quot;: &quot;available&quot;,
    &quot;links&quot;: [
      { &quot;rel&quot;: &quot;self&quot;, &quot;href&quot;: &quot;https://api.yalcobooks.com/v1/books/1&quot; },
      { &quot;rel&quot;: &quot;update&quot;, &quot;href&quot;: &quot;https://api.yalcobooks.com/v1/books/1&quot; },
      { &quot;rel&quot;: &quot;delete&quot;, &quot;href&quot;: &quot;https://api.yalcobooks.com/v1/books/1&quot; }
    ]
  }</code></pre>
<blockquote>
<p>파스타를 주문했을 때, 파스타를 갖다주면서 커피와 디저트 메뉴판을 같이 주는 것을 떠올려보자.
[출처: 유튜브 얄팍한 코딩사전]</p>
</blockquote>
</li>
</ul>
<h3 id="code-on-demand">Code-on-demand</h3>
<p>REST API 메시지만 보고도 이를 쉽게 이해 할 수 있는 자체 표현 구조로 되어 있다.</p>
<ul>
<li><strong>구성요소</strong><ul>
<li><strong>자원:</strong> URI를 이용하여 표현</li>
<li><strong>행위:</strong> HTTP 메서드를 이용하여 표현</li>
</ul>
</li>
</ul>
<blockquote>
<p>참고자료
<a href="https://www.youtube.com/watch?v=fB3MB8TXNXM">REST API - 이거 하나로 끝남</a>
<a href="https://www.youtube.com/watch?v=Nxi8Ur89Akw">[10분 테코톡] 정의 REST API</a>
<a href="https://www.youtube.com/watch?v=RP_f5dMoHFc">Day1, 2-2.  그런 REST API로 괜찮은가</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[테스트] MSW 기본개념]]></title>
            <link>https://velog.io/@one1_programmer/%ED%85%8C%EC%8A%A4%ED%8A%B8-MSW-%EA%B8%B0%EB%B3%B8%EA%B0%9C%EB%85%90</link>
            <guid>https://velog.io/@one1_programmer/%ED%85%8C%EC%8A%A4%ED%8A%B8-MSW-%EA%B8%B0%EB%B3%B8%EA%B0%9C%EB%85%90</guid>
            <pubDate>Fri, 30 Jan 2026 09:58:46 GMT</pubDate>
            <description><![CDATA[<p>개발을 하다 보면 백엔드 API가 완성되지 않았거나, 테스트 환경에서 네트워크 의존성을 제거해야 하는 순간이 온다.   나의 경우 모킹을 구현한다고 하면 예전에는 <code>json-server</code> , <code>axios-mock-adapter</code> 를 순차적으로 사용했던 것 같다. 하지만 해당 라이브러리들의 한계는 뚜렷했다.</p>
<ul>
<li><strong>json-server의 한계</strong><ul>
<li><strong>별도 프로세스 관리:</strong> 애플리케이션 외에 별도의 로컬 서버를 띄우고 관리해야 합니다. 팀원들과 환경을 맞추려면 포트 설정부터 DB 파일(<code>db.json</code>) 공유까지 신경 쓸 게 늘어난다.</li>
<li><strong>엔드포인트 불일치:</strong> 실제 API 주소(예: <code>api.service.com</code>)와 로컬 서버 주소(<code>localhost:3000</code>)가 다르기 때문에, 코드 내에 환경별로 URL을 교체하는 로직을 넣어야 한다.</li>
<li><strong>복잡한 비즈니스 로직 구현의 어려움:</strong> 단순 CRUD는 쉽지만, 특정 헤더에 따른 응답 변경, 정교한 에러 시뮬레이션, 동적인 상태 변화를 구현하려면 결국 별도의 백엔드 코딩을 하는 것과 다름없다.</li>
</ul>
</li>
</ul>
<p>하지만 &quot;네트워크 단에서 직접 요청을 가로채는&quot; 더욱 효율적인 라이브러리인 MSW를 사용하면 이러한 한계들을 쉽게 극복할 수 있다.</p>
<table>
<thead>
<tr>
<th><strong>비교 항목</strong></th>
<th><strong>json-server</strong></th>
<th>*<em>axios-mock-adapter *</em></th>
<th><strong>MSW</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>작동 위치</strong></td>
<td>별도 로컬 서버</td>
<td>애플리케이션 내부 (라이브러리 단)</td>
<td>브라우저 네트워크 레벨 (Service Worker)</td>
</tr>
<tr>
<td><strong>코드 오염</strong></td>
<td>낮음 (서버 분리)</td>
<td>높음</td>
<td>매우 낮음 (완전 분리)</td>
</tr>
<tr>
<td><strong>디버깅</strong></td>
<td>네트워크 탭 확인 가능</td>
<td>확인 불가</td>
<td>실제 요청처럼 확인 가능</td>
</tr>
<tr>
<td><strong>환경 제약</strong></td>
<td>포트/서버 관리 필요</td>
<td>Axios 라이브러리에 의존</td>
<td>환경/라이브러리 무관</td>
</tr>
</tbody></table>
<p><strong>MSW(Mock Service Worker)</strong>는 브라우저와 Node.js 환경 모두를 지원하는 API 모킹 라이브러리로, 단순한 가짜 데이터를 넘어서는 &#39;독립적인 네트워크 계층&#39;을 구축하게 해준다. 이름에서 알 수 있듯 서비스 워커를 통해 실제 API 요청을 가로채고 미리 정의된 응답을 반환한다. 이를 통해 빠르고 안정적인 테스트 환경을 구축할 수 있다. 브라우저의 경우 서비스 워커를 사용하고, Node.js 환경에서는 내부적으로 <code>XHR</code>, <code>fetch</code> 등의 요청을 가로채는 인터셉터를 사용한다.</p>
<p>공식문서에 나와있는 MSW의 주요 특징은 다음과 같다.</p>
<ul>
<li>주요 특징<ul>
<li><strong>완벽한 독립성 (Agnostic)</strong><ul>
<li>프레임워크, 도구, 환경에 얽매이지 않는다. <code>React</code>, <code>Vue</code>, <code>Angular</code>에 더해 순수 자바스크립트 환경에서도 동일하게 작동한다.</li>
<li><code>window.fetch</code> 같은 네이티브 API뿐만 아니라 <code>Axios</code>, <code>React Query</code>, <code>Apollo</code> 등 어떤 클라이언트를 사용하더라도 추가 설정 없이 즉시 통합된다.</li>
</ul>
</li>
<li><strong>끊김 없는 매끄러움 (Seamless)</strong><ul>
<li><strong>브라우저:</strong> 실제 서비스 워커(Service Worker) API를 활용해 네트워크 수준에서 요청을 가로챈다. 애플리케이션 코드를 수정하거나 <code>fetch</code>를 오염시키지 않는 &#39;플랫폼 기반&#39;의 접근 방식을 지향한다.</li>
<li><strong>Node.js:</strong> 표준 인터셉트 수단이 없는 노드 환경에서도 모듈 패칭이 아닌 클래스 확장(Class Extension) 방식을 사용한다. 덕분에 테스트 환경을 실제 운영 환경과 최대한 유사하게 유지할 수 있다.</li>
</ul>
</li>
<li><strong>높은 재사용성 (Reusable)</strong><ul>
<li>MSW는 API 모킹을 하나의 독립된 레이어로 취급한다. 한 번 작성한 모킹 로직은 개발 단계, 통합 테스트, E2E 테스트, 그리고 Storybook이나 라이브 데모에서도 그대로 재사용할 수 있다.</li>
<li>환경마다 모킹 전략을 새로 짤 필요 없이, &#39;단일 진실 공급원(Single Source of Truth)&#39;으로서 네트워크 동작을 관리할 수 있다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h1 id="🤔개념">🤔개념</h1>
<h2 id="브라우저-환경에서-모킹">브라우저 환경에서 모킹</h2>
<p>React와 같이 브라우저 환경에서 모킹이 필요한 경우, 서비스 워커 등록이 필요하다. msw에서는 서비스 워커 관련 스크립트(<code>mockServiceWorker.js</code>)를 제공한다. 이는 애플리케이션 바깥으로 나가는 네트워크 트래픽을 가로채는 역할을 수행한다.</p>
<h3 id="환경-구성">환경 구성</h3>
<ol>
<li><p><strong><code>mockServiceWorker.js</code> 등록</strong></p>
<pre><code class="language-bash"> npx msw init &lt;프로젝트의 public 디렉터리의 위치&gt; --save</code></pre>
</li>
<li><p><strong>브라우저용 워커 세팅</strong></p>
<pre><code class="language-jsx"> // _mocks/browser.ts
 import { setupWorker } from &quot;msw/browser&quot;;
 import { roomHandlers } from &quot;./handlers/room&quot;;

 export const worker = setupWorker(...roomHandlers);</code></pre>
</li>
<li><p><strong>핸들러 정의:</strong> 프로젝트에서 사용할 핸들러를 정의한다. http에 대한 응답 등이 될 수 있다.</p>
<pre><code class="language-jsx"> // 예시: _mocks/handlers/room.ts
 import { http, HttpResponse } from &quot;msw&quot;;

 export const roomHandlers = [
   http.post(&quot;/api/v1/rooms&quot;, () =&gt; {
     return HttpResponse.json(
       {
         timeStamp: &quot;2026-01-29T14:23:45.123+09:00&quot;,
         // ...
       },
       { status: 201 },
     );
   }),
 ];</code></pre>
<blockquote>
<p>모킹 로직을 각 테스트 케이스나 컴포넌트 내부에 흩어놓는 것이 아니라, 실제 API 라우터 구조와 유사하게 <code>/mocks/handlers</code> 폴더 내에 중앙화하여 관리하였다. 이렇게 하면, 추후 API 명세가 변경되었을 때 다른 코드의 수정 없이 중앙의 핸들러 파일만 수정하면 모든 테스트와 개발 환경에 변경 사항이 일괄적으로 반영된다.</p>
</blockquote>
</li>
<li><p><strong>워커 시작 비동기 함수 정의</strong></p>
<pre><code class="language-jsx"> export async function enableMocking() {
   if (import.meta.env.MODE !== &quot;development&quot;) {
     return;
   }

   const { worker } = await import(&quot;./browser&quot;); // browser.ts가 위치한 상대 경로

   // 서비스 워커가 준비되어 요청을 가로칠 준비가 완료되면,
   // worker.start()는 resolve된 Promise를 반환함.
   return worker.start();
 }</code></pre>
</li>
<li><p><strong>앱 진입점에 적용</strong></p>
<pre><code class="language-jsx"> import { createRoot } from &quot;react-dom/client&quot;;
 import Router from &quot;./routes&quot;;
 import { enableMocking } from &quot;./_mocks&quot;;

 enableMocking().then(() =&gt; {
   createRoot(document.getElementById(&quot;root&quot;)!).render(
      &lt;Router /&gt;
   );
 });</code></pre>
</li>
</ol>
<h3 id="결과">결과</h3>
<p>앱 구동 시 개발자 도구 콘솔 탭을 확인하면, 아래와 같이 모킹이 성공적으로 세팅됨을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/f5520b54-70b6-4208-928f-0e56b531966f/image.png" alt=""></p>
<p>모킹한 api로 요청을 보내고 네트워크 탭을 확인하면, 서비스 워커가 성공적으로 요청을 가로챈 것을 확인할 수 있다. 콘솔 탭에서도 응답을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/301a2637-f8f1-4691-bd60-aecc0cca753d/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/c838894c-6170-4f0d-a653-98ffa97a9dd8/image.png" alt=""></p>
<h2 id="http-응답-모킹하기">HTTP 응답 모킹하기</h2>
<h3 id="네트워크-에러">네트워크 에러</h3>
<p>테스트 환경에서 꼭 정상적인 응답이 오란 법은 없다. 예를 들어, 일시적인 네트워크 에러 등 예측할 수 없는 에러에 대해서도 올바른 UI가 표시되는지 확인해야 하는 경우도 생긴다. MSW는 <code>Response.error()</code> 를 제공하여 이런 상황을 모킹할 수 있게 지원한다.</p>
<pre><code class="language-jsx">http.get(&#39;/resource&#39;, () =&gt; {
  return HttpResponse.error()
})</code></pre>
<ul>
<li>적용 예시<ul>
<li>DNS 에러</li>
<li>커넥션 타임아웃</li>
<li>오프라인 상태의 클라이언트</li>
</ul>
</li>
</ul>
<h2 id="websocket-모킹하기">WebSocket 모킹하기</h2>
<pre><code class="language-ts">
import { ws } from &quot;msw&quot;;

const WS_URL = &quot;ws://localhost:15674/ws-test&quot;;

const chat = ws.link(WS_URL);

export const wsHandlers = [
  chat.addEventListener(&quot;connection&quot;, ({ client }) =&gt; {
    // 연결 실패 테스트 위한 플래그
    const SHOULD_FAIL = false;

    if (SHOULD_FAIL) {
      throw new Error(&quot;Connection failed&quot;);
    }

    client.addEventListener(&quot;message&quot;, event =&gt; {
      console.log(&quot;Intercepted message from the client&quot;, event);
      const message = event.data;
    });

    client.send(&quot;hello from server!&quot;); // 텍스트
    client.send(new Blob([&quot;hello world&quot;], { type: &quot;text/plain&quot; })); // Blob
    client.send(new TextEncoder().encode(&quot;hello world&quot;)); // ArrayBuffer

    client.addEventListener(&quot;close&quot;, event =&gt; {
      console.log(&quot;Client is closing the connection&quot;, client);
    });
  }),
];</code></pre>
<h2 id="테스트-환경과의-통합">테스트 환경과의 통합</h2>
<h3 id="setupserver"><code>setupServer()</code></h3>
<p>통합 테스트 시, setup과 teardown을 사용해 테스트 실행 전과 후에 API를 모킹하고 해제하는 작업을 수행한다.</p>
<pre><code class="language-js">// setupTests.js
import { setupServer } from &#39;msw/node&#39;;

import { handlers } from &#39;@/__mocks__/handlers&#39;;

/* msw */
export const server = setupServer(...handlers);

beforeAll(() =&gt; {
  server.listen();
});

afterEach(() =&gt; {
  server.resetHandlers(); // 런타임에 변경한 MSW의 모킹을 초기화
});

afterAll(() =&gt; {
  server.close();
});</code></pre>
<h3 id="resethandlers"><strong><code>resetHandlers()</code></strong></h3>
<p>한편 프로젝트 내에서 공유하는 단일 인스턴스를 변경했으니, 이를 원래의 핸들러로 복구해줘야 한다. 이를 위해서는 <code>server.resetHandlers()</code> 를 Teardown에서 호출해줘야 한다. 즉, 초기화 작업을 통해 일관된 모킹 환경으로 안정성 있는 테스트 작성이 가능하게 된다.</p>
<pre><code class="language-tsx">import { setupServer } from &#39;msw/node&#39;;

import { handlers } from &#39;@/__mocks__/handlers&#39;;

export const server = setupServer(...handlers);

afterEach(() =&gt; {
  server.resetHandlers();
  vi.clearAllMocks();
});</code></pre>
<blockquote>
<p>아래와 같이 새로운 핸들러를 제공하는 것도 가능하다.</p>
<pre><code class="language-tsx">const worker = setupWorker(http.get(&#39;/resource&#39;, resolver))
worker.use(http.post(&#39;/user&#39;, resolver))

worker.resetHandlers(http.patch(&#39;/book/:bookId&#39;, resolver))
// 런타임 &quot;POST /user&quot;와 초기 &quot;GET /resource&quot; 모두 제거되고 
// &quot;PATCH /book/:bookId&quot; 요청 핸들러만 유효함.</code></pre>
</blockquote>
<h3 id="use"><code>use()</code></h3>
<p>특정 API에 대해 상황에 따라 다른 모킹을 적용하려면 어떻게 해야 할까? 기존 핸들러를 바꾸지 못하는 상황에서는 특정 테스트 환경 내에서만 이를 동적으로 변경할 수 있으면 좋을 것 같다.</p>
<p>다행히도 msw에서는 <code>use</code> 라는 함수를 통해서 이런 기능을 제공한다. 이때, 초기에 구동을 위해 설정한 msw 서버 인스턴스와 동일한 인스턴스를 사용해야 기존에 모킹된 API의 응답을 변경할 수 있다.</p>
<pre><code class="language-tsx">import { http } from &#39;msw&#39;
import { setupServer } from &#39;msw/node&#39;

const server = setupServer()

// 요청 핸들러를 현재 서버 인스턴스에 추가 
server.use(http.get(&#39;/resource&#39;, resolver), http.post(&#39;/resource&#39;, resolver))</code></pre>
<pre><code class="language-jsx">// handlers.js
// 사용자 정보가 없는 비로그인 상태
rest.get(`/user`, (req, res, ctx) =&gt; {
  return res(ctx.status(200), ctx.json(null));
}),</code></pre>
<pre><code class="language-jsx">// 사용자 정보가 있는 로그인 상태를 테스트 해야 하는 상황
beforeEach(() =&gt; {
  // 기존 handlers 응답을 use 함수 내의 응답을 기준으로 테스트를 실행
  server.use(
    rest.get(&#39;/user&#39;, (_, res, ctx) =&gt; {
      return res(
        ctx.status(200),
        ctx.json({
          id: 1,
          email: &#39;maria@mail.com&#39;,
          name: &#39;Maria&#39;,
          password: &#39;12345&#39;,
        }),
      );
    }),
  );
});</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[생성형 AI] 클로드 코드(Claude Code)를 맞이할 준비]]></title>
            <link>https://velog.io/@one1_programmer/%EC%83%9D%EC%84%B1%ED%98%95-AI-%ED%81%B4%EB%A1%9C%EB%93%9C-%EC%BD%94%EB%93%9CClaude-Code%EB%A5%BC-%EB%A7%9E%EC%9D%B4%ED%95%A0-%EC%A4%80%EB%B9%84-%EC%A0%95%EB%93%A4%EC%97%88%EB%8D%98-%EC%BB%A4%EC%84%9C%EB%A5%BC-%EB%96%A0%EB%82%98%EB%B3%B4%EB%82%B4%EB%A9%B0</link>
            <guid>https://velog.io/@one1_programmer/%EC%83%9D%EC%84%B1%ED%98%95-AI-%ED%81%B4%EB%A1%9C%EB%93%9C-%EC%BD%94%EB%93%9CClaude-Code%EB%A5%BC-%EB%A7%9E%EC%9D%B4%ED%95%A0-%EC%A4%80%EB%B9%84-%EC%A0%95%EB%93%A4%EC%97%88%EB%8D%98-%EC%BB%A4%EC%84%9C%EB%A5%BC-%EB%96%A0%EB%82%98%EB%B3%B4%EB%82%B4%EB%A9%B0</guid>
            <pubDate>Sat, 24 Jan 2026 08:16:34 GMT</pubDate>
            <description><![CDATA[<p>그동안 커서(Cursor) 에디터에 익숙했던터라 크게 클로드 코드(Claude Code)로 갈아탈 필요성을 느끼지 못했다.</p>
<p>새로 맞이할 에이전트를 &#39;클로드 코드&#39;로 선택한 결정적인 이유는 다음과 같다.</p>
<ul>
<li>개발직군에서 가장 많이 사용하는 에이전트로 인지하고 있다. 그만큼 신뢰성은 보장이 되었다는 뜻이라고 받아들였다.</li>
<li>CLI를 제공한다. 이전에는 &#39;에이, 커서는 에디터에서 다 할 수 있는데 불편하게 CLI로 굳이?&#39;라는 생각을 했었다. 하지만 최근들어 커서를 사용하며 폴더, 코드, 채팅 영역을 한정된 크기에서 프로젝트를 다루며 피로감을 많이 느꼈다. CLI가 이런 피로감을 덜어줄 수 있는 방법이 될 수 있지 않을까 생각했다. (물론, 클로드 코드도 익스텐션을 통해 GUI를 제공하고 있긴 하다)</li>
</ul>
<p>인프런 라이브 세션 &#39;AI 시대 개발자로 살아남는 방법(?)&#39;에서 어느 개발자가 앤트로픽에서 <a href="https://anthropic.skilljar.com/claude-code-in-action">Claude Code 공식 강의</a>(분량 자체는 1시간 이내로 매우 적다)를 추천했다. 이전에 친구가 추천해주어 북마크는 해두었지만, 해당 라이브 세션을 통해 다시 열어보게 되었다. </p>
<h1 id="🤔개념">🤔개념</h1>
<h2 id="what-is-coding-assistant">What is Coding assistant?</h2>
<h3 id="코딩-어시스턴트의-작동-방식-how-coding-assistants-work"><strong>코딩 어시스턴트의 작동 방식 (How Coding Assistants Work)</strong></h3>
<p>코딩 어시스턴트는 인간 개발자가 문제를 해결하는 방식과 유사한 3단계 과정을 거친다.</p>
<ol>
<li><strong>맥락 파악 (Gather context):</strong> 오류가 무엇인지, 어떤 코드 부분이 영향을 받는지, 관련된 파일은 무엇인지 이해한다.</li>
<li><strong>계획 수립 (Formulate a plan):</strong> 코드를 수정하고 테스트를 실행하여 해결책을 검증하는 등 문제를 어떻게 풀지 결정한다.</li>
<li><strong>실행 (Take action):</strong> 실제로 파일을 업데이트하거나 명령어를 실행하여 해결책을 구현한다.</li>
</ol>
<p>이 과정에서 어시스턴트는 파일 읽기, 문서 검색, 명령어 실행 등 <strong>외부 세계와의 상호작용</strong>을 수행하며, 필요에 따라 이 과정을 반복(Iterate)한다.</p>
<h3 id="도구-사용tool-use의-챌린지와-해결책"><strong>도구 사용(Tool Use)의 챌린지와 해결책</strong></h3>
<p>언어 모델 자체는 텍스트를 입력받아 텍스트를 출력할 뿐, 실제로 파일을 읽거나 명령어를 실행할 능력은 없다. 이를 해결하기 위해 ‘도구 사용(Tool Use)&#39;이라는 똑똑한 시스템을 사용한다.</p>
<ul>
<li><strong>작동 메커니즘:</strong> 사용자가 요청을 보내면, 코딩 어시스턴트는 언어 모델에게 &quot;파일을 읽고 싶다면 &#39;ReadFile: 파일명&#39;이라고 답하라&quot;는 식의 지침을 몰래 추가한다.</li>
<li><strong>처리 흐름:</strong><ol>
<li>사용자의 질문한다(&quot;main.go 파일에 무슨 코드가 있어?&quot;).</li>
<li>모델이 도구 사용 요청을 텍스트로 응답한다(&quot;ReadFile: main.go&quot;).</li>
<li>코딩 어시스턴트가 실제로 파일을 읽어 그 내용을 모델에게 전달한다.</li>
<li>모델이 파일 내용을 바탕으로 최종 답변을 생성한다.</li>
</ol>
</li>
</ul>
<p>즉, 모델은 텍스트로 명령을 내리고, 실제 작업은 어시스턴트 시스템이 수행하는 구조이다.</p>
<h3 id="클로드claude의-도구-사용이-중요한-이유"><strong>클로드(Claude)의 도구 사용이 중요한 이유</strong></h3>
<p>Claude 시리즈(Opus, Sonnet, Haiku)는 도구의 기능을 이해하고 이를 활용해 작업을 완수하는 데 특히 강력하다. 이러한 강력한 도구 사용 능력은 Claude Code에 다음과 같은 이점을 제공한다</p>
<ul>
<li><strong>더 어려운 작업 해결:</strong> 여러 도구를 조합하여 복잡한 작업을 처리하고, 처음 보는 도구도 활용할 수 있다.</li>
<li><strong>확장 가능한 플랫폼:</strong> 새로운 도구를 쉽게 추가할 수 있으며, 워크플로우 변화에 맞춰 클로드가 적응한다.</li>
<li><strong>보안 강화:</strong> 전체 코드베이스를 외부 서버로 보내 인덱싱(색인)할 필요 없이 코드를 탐색할 수 있어 보안성이 높다.</li>
</ul>
<h2 id="기본-기능">기본 기능</h2>
<h3 id="코드-베이스-스캔">코드 베이스 스캔</h3>
<p>클로드는 이전 세션에 대한 기억 없이 매 세션을 시작한다. 따라서 개발자의 코드 스타일 선호도나 팀 내에서 정한 특별한 규칙이 있다는 것도 알지 못한다. 이는 CLAUDE.md 를 해결할 수 있으며, 클로드는 이 파일을 통해 자동으로 환경설정을 읽어오므로 세션 간에 환경설정이 유지될 수 있다.   </p>
<ul>
<li><p><strong>위치에 따른 CLAUDE.md</strong></p>
<ul>
<li><strong><code>CLAUDE.md</code>:</strong> /init 명령어 수행 결과로 생성되며, 일반적으로 Git에 커밋한다.</li>
<li><strong><code>CLAUDE.local.md</code>:</strong> 개인적인 명령어와 클로드를 위한 커스텀을 포함하는 <a href="http://CLAUDE.md">CLAUDE.md</a> 파일이다. Git에 커밋되지 않아 다른 엔지니어들과 공유되지 않는다.</li>
<li><strong><code>~/.claude/CLAUDE.md</code>:</strong> 머신 내의 모든 프로젝트에서 전역적으로 공유된다.</li>
</ul>
</li>
<li><p>특징</p>
<ul>
<li><strong><code>/init</code> 으로 <code>CLAUDE.md</code> 자동 작성:</strong> <code>/init</code> 수행 → 코드베이스 스캔 → 요약 생성 → <code>CLAUDE.md</code> 작성 → 매 요청마다 <code>CLAUDE.md</code> 참조</li>
<li>프로젝트 특정 하위 디렉터리의 파일을 읽을 때, 해당 하위 디렉터리에 있는 <code>CLAUDE.md</code> 파일을 자동으로 가져온다. 이 파일들은 실행 시에는 로드되지 않으며, 클로드가 코드베이스의 해당 부분에서 활발하게 작업할 때만 포함된다.</li>
</ul>
</li>
</ul>
<blockquote>
<p><strong>모범사례</strong></p>
</blockquote>
<ul>
<li>프로젝트가 무엇인지 한 줄로 설명하는 것으로 시작하세요.</li>
<li>코드 스타일 선호 사항을 구체적이고 실행 가능하게 만드세요.</li>
<li>주요 명령어(테스트, 빌드, 린트, 배포)를 포함하세요.</li>
<li>실수를 방지할 수 있도록 주의 사항을 충분히 자세히 알려주세요.</li>
<li>300줄 이하로 유지하거나, 모든 줄이 그 자리에 있을 만한 가치가 있도록 하세요.</li>
<li>자세한 안내를 @import한 파일로 옮기세요.</li>
<li>오래되었거나 최신 지침과 충돌하는 내용은 모두 제거하십시오.</li>
<li>정말 중요한 규칙만 강조 표시하세요.</li>
<li>작업 시작 전에만 지침을 추가하지 말고, 작업하면서 동시에 지침을 추가하세요.</li>
<li>PR 리뷰에서 규칙(컨벤션)이 발견되면 업데이트하세요.</li>
<li>오래되었거나 상충되는 규칙이 있는지 정기적으로 검토하십시오.</li>
</ul>
<h3 id="rules"><code>rules</code></h3>
<pre><code class="language-bash">your-project/
├── .claude/
│   ├── CLAUDE.md           # 주요 프로젝트 지침
│   └── rules/
│       ├── code-style.md   # 코드 스타일 가이드라인
│       ├── testing.md      # 테스트 컨벤션
│       └── security.md     # 보안 요구사항</code></pre>
<p>규모가 큰 프로젝트의 경우, <code>.claude/rules/</code> 를 활용하여 모든 내용을 담은 하나의 큰 파일 대신, 지침을 특정 목적에 맞는 규칙 파일로 분할할 수 있다.</p>
<ul>
<li>특징<ul>
<li>이 디렉터리 내의 모든 마크다운 파일은 메인 <code>CLAUDE.md</code> 파일과 동일한 우선순위로 자동으로 로드된다.</li>
<li>팀 구성원들이 각기 다른 규칙 세트를 담당할 때 효과적이다.</li>
</ul>
</li>
</ul>
<h3 id="기억-추가하기">기억 추가하기</h3>
<p>명령어에 <code>#</code> 접두어를 붙이면, CLAUDE.md에 작성한 내용을 바탕으로 새로운 기억(규칙)을 추가한다.</p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/07aa320d-4cba-4c08-8772-f59580da5aab/image.png" width="600"/></p>

<blockquote>
<p>커서(cursor) 에디터에서는 <code>.cursor/rules/</code> 하위에 커서룰을 직접 생성하여 사용했는데, 클로드 코드에서는 간단히 프롬프트 앞에 <code>#</code> 을 붙여주면 편리하게 룰을 생성할 수 있다.</p>
</blockquote>
<h3 id="특정-경로를-컨텍스트로-추가">특정 경로를 컨텍스트로 추가</h3>
<p><code>@</code> 접두사를 붙이면 특정 경로의 파일을 컨텍스트로 추가할 수 있다. 자동완성이 지원된다.</p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/f82d9555-da10-46a3-b79d-9c26064d5d65/image.png" width="600"/></p>

<h2 id="지능-향상-방법">지능 향상 방법</h2>
<p>계획 모드와 사고 모드를 사용하여 클로드의 지능을 더욱 높여보자.</p>
<blockquote>
<p>두 모드를 결합하여 넓은 범위의 이해와 깊은 논리 분석을 동시에 수행할 수 있지만, 토큰 소모량이 늘어나 비용이 발생할 수 있다는 점은 유의해야 한다.</p>
</blockquote>
<h3 id="계획-모드">계획 모드</h3>
<p><code>Shift + Tab</code> 을 두 번 누른다.</p>
<ul>
<li>특징<ul>
<li>코드베이스에 대한 광범위한 이해가 필요하고 다양한 영역을 살펴보아야 하는 작업에 유용하다.</li>
<li>여러 단계를 거쳐 완료해야 하는 작업에 유용하다.</li>
</ul>
</li>
</ul>
<h3 id="사고-모드">사고 모드</h3>
<p>클로드의 확장 사고 기능을 활성화하여 특정 작업에 대해 더 많이 추론할 수 있도록 한다.</p>
<p>Think → Think more → Think a lot → Think longer → Ultrathink</p>
<ul>
<li>특징<ul>
<li>특정 까다로운 로직에 집중해야할 때 유용하다.</li>
<li>어려운 버그를 해결할 때 유용하다.</li>
</ul>
</li>
</ul>
<table>
<thead>
<tr>
<th><strong>구분</strong></th>
<th><strong>계획 모드 (Planning Mode)</strong></th>
<th><strong>사고 모드 (Thinking Mode)</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>최적화된 작업</strong></td>
<td>코드베이스에 대한 넓은 이해가 필요한 작업</td>
<td>복잡한 로직 및 알고리즘 문제 해결</td>
</tr>
<tr>
<td><strong>특징</strong></td>
<td>여러 파일이나 구성 요소에 영향을 주는 변경</td>
<td>까다로운 버그 디버깅 및 심층 추론</td>
</tr>
<tr>
<td><strong>워크플로우</strong></td>
<td>다단계 구현 및 전체적인 구조 설계</td>
<td>깊이 있는 논리 분석</td>
</tr>
</tbody></table>
<h2 id="대화-흐름-제어-기술"><strong>대화 흐름 제어 기술</strong></h2>
<h3 id="escape-키를-이용한-중단-interrupting-claude"><strong>Escape 키를 이용한 중단 (Interrupting Claude)</strong></h3>
<p>클로드가 잘못된 방향으로 응답하거나 한꺼번에 너무 많은 일을 처리하려 할 때 사용한다.</p>
<ul>
<li><code>Escape</code> 키를 누르면 응답이 즉시 중단되며, 사용자는 대화의 방향을 다시 설정할 수 있다.</li>
<li>예를 들어, 여러 함수의 테스트 코드를 작성해달라고 했을 때 클로드가 너무 방대한 계획을 세우기 시작하면, 이를 중단하고 한 번에 하나의 함수에만 집중하도록 지시할 수 있다.</li>
<li><strong>Escape와 기억(Memories)의 결합:</strong> 클로드가 반복적인 실수를 할 때 이를 교정하는 아주 좋은 방법이다.<ul>
<li>먼저 <code>Escape</code> 키로 응답을 멈춘 뒤, <code>#</code> 단축키를 사용하여 올바른 접근 방식에 대한 &#39;기억&#39;을 추가한다.</li>
<li>이후 수정된 정보를 바탕으로 대화를 계속하면, 향후 프로젝트 진행 시 동일한 오류가 발생하는 것을 방지할 수 있다.</li>
</ul>
</li>
</ul>
<h3 id="대화-되감기-rewinding-conversations"><strong>대화 되감기 (Rewinding Conversations)</strong></h3>
<p><code>Escape</code> 키를 두 번 누르면 지금까지 보낸 모든 메시지가 표시되며, 이전 지점으로 돌아가 대화를 다시 시작할 수 있다.</p>
<ul>
<li>이는 오류를 디버깅하느라 길어진 불필요한 대화 기록을 제거하고, 클로드가 현재 작업에만 집중할 수 있도록 돕는다.</li>
<li>코드베이스에 대한 이해와 같은 유익한 맥락은 유지하면서 산만한 기록만 지울 수 있다는 장점이 있다.</li>
</ul>
<blockquote>
<p>내가 알지 못하는 것일 수도 있지만, 커서(Cursor) 에디터에는 되감기 기능은 없는 걸로 알고 있다. 커서를 사용할 때 컨텍스트로 인해 에이전트가 편향된 대답을 하는 것이 답답했던 적이 종종 있었다. 그런 불편했던 경험을 떠올려 보면, 클로드 코드의 되감기(rewind) 기능은 되게 유용하게 쓸 수 있을 것 같다.</p>
</blockquote>
<h3 id="맥락-관리-명령어-context-management-commands"><strong>맥락 관리 명령어 (Context Management Commands)</strong></h3>
<ul>
<li><strong><code>/compact</code></strong>: 전체 대화 기록을 요약하면서 핵심 정보는 보존한다. 대화가 길어졌지만 클로드가 학습한 중요한 프로젝트 지식을 유지하고 싶을 때 유용하다.</li>
<li><strong><code>/clear</code></strong>: 대화 기록을 완전히 삭제하고 새로 시작한다. 완전히 다른 작업으로 전환하거나, 기존 맥락이 새로운 작업을 방해할 우려가 있을 때 사용한다.</li>
</ul>
<h2 id="사용자-정의-명령어-creating-custom-commands">사용자 정의 명령어 (Creating Custom Commands)</h2>
<p>기본적으로 <code>/</code> 을 입력하면 커서가 제공하는 명령어를 사용할 수 있다. 이때, 사용자 지정 명령어를 등록하여 사용할 수도 있다. <code>.claude/commands/{지정 명령어}.md</code> 파일을 생성한 후 파일에 해당 명령어가 수행할 동작을 정의한다. 파일의 첫 줄에 해당 명령어가 수행할 동작의 목적을 정의할 수 있으며, 이는 실제 명령어 수행 시 미리보기로 확인할 수 있다. 아래의 예시들에서는 이해를 돕기 위해 맨 첫 라인에 파일 경로 주석을 달았지만, 실제로는 미리보기로 주석 내용이 표시되므로 첫 라인에 주석을 달면 안된다.</p>
<ul>
<li><p>특징</p>
<ul>
<li><strong>자동화:</strong> 반복적인 워크플로우를 단일 명령어로 변환한다.</li>
<li><strong>일관성:</strong> 매번 동일한 단계와 규칙(테스트 컨벤션 등)이 준수되도록 보장한다.</li>
<li><strong>맥락 제공:</strong> 프로젝트 전용 지침이나 팀의 관례를 클로드에게 명확히 전달한다.</li>
</ul>
</li>
<li><p>과정</p>
<ol>
<li>프로젝트 디렉토리 내의 <code>.claude</code> 폴더를 생성한다.</li>
<li>그 안에 <code>commands</code>라는 새 디렉토리를 생성한다.</li>
<li>원하는 명령어 이름으로 마크다운(.md) 파일을 만든다 (예: <code>audit.md</code>).</li>
<li>파일명이 곧 명령어가 된다. 즉, <code>audit.md</code>는 <code>/audit</code> 명령어가 됩니다.</li>
</ol>
</li>
<li><p>예시</p>
<pre><code class="language-markdown">  # .claude\commands\audit.md
  Your goal is to update any vulnerable dependencies.

  Do the following:

  1. Run `npm audit` to find vulnerable installed packages in this project
  2. Run `npm audit fix` to apply updates
  3. Run tests and verify the updates didn&#39;t break anything</code></pre>
  <p><img src="https://velog.velcdn.com/images/one1_programmer/post/b402d532-2ab9-4f48-b219-a3920905dd4f/image.png" width="600"/></p>


</li>
</ul>
<h3 id="인자arguments-활용하기">인자(Arguments) 활용하기</h3>
<p><code>$ARGUMENTS</code> 자리표시자(Placeholder)를 사용하면 훨씬 유연한 명령어를 만들 수 있다. 예시처럼 &quot;$ARGUMENTS에 대한 종합적인 테스트를 작성해줘&quot;라고 설정하면, 실행 시 <code>/write_tests [파일명]</code>과 같이 특정 대상을 지정할 수 있다. 이때, 인자는 파일 경로뿐만 아니라 클로드에게 전달하고 싶은 어떤 문자열이든 가능하다.</p>
<ul>
<li><p>예시</p>
<pre><code class="language-markdown">  # .claude\commands\write_tests.md
  Write comprehensive tests for: $ARGUMENTS

  Testing conventions:

  - Use Vitests with React Testing Library
  - Place test files in a **test** directory in the same folder as the source file
  - Name test files as [filename].test.ts(x)
  - Use @/ prefix for imports

  Coverage:

  - Test happy paths
  - Test edge cases
  - Test error states
  - Focus on testing behavior and public API&#39;s rather than implementation details.</code></pre>
  <p><img src="https://velog.velcdn.com/images/one1_programmer/post/b445311e-2c47-4b82-aa14-6e453e4716ad/image.png" width="600"/></p>



</li>
</ul>
<blockquote>
<p>진작에 클로드 코드를 썼어야 했나.. CLI를 지원하는 클로드 코드가 가지고 있는 강력한 무기 중 하나가 아닐까 한다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js] Error Handling, 예측 가능한 것과 그렇지 않은 것]]></title>
            <link>https://velog.io/@one1_programmer/Next.js-%EC%97%90%EB%9F%AC-%EC%B2%98%EB%A6%AC-%EC%98%88%EC%B8%A1-%EA%B0%80%EB%8A%A5%ED%95%9C-%EA%B2%83%EA%B3%BC-%EA%B7%B8%EB%A0%87%EC%A7%80-%EC%95%8A%EC%9D%80-%EA%B2%83</link>
            <guid>https://velog.io/@one1_programmer/Next.js-%EC%97%90%EB%9F%AC-%EC%B2%98%EB%A6%AC-%EC%98%88%EC%B8%A1-%EA%B0%80%EB%8A%A5%ED%95%9C-%EA%B2%83%EA%B3%BC-%EA%B7%B8%EB%A0%87%EC%A7%80-%EC%95%8A%EC%9D%80-%EA%B2%83</guid>
            <pubDate>Sat, 03 Jan 2026 05:09:53 GMT</pubDate>
            <description><![CDATA[<p>에러는 Expected errors와 Uncaught exceptions의 두 가지 분류로 나눌 수 있다. 이번 포스팅에서는 공식문서와 유튜브 Boaz 님의 영상을 참고하여 Next.js가 에러를 다루는 방법을 정리하고자 한다.</p>
<h1 id="🤔개념">🤔개념</h1>
<h2 id="예측-가능한-에러-다루기">예측 가능한 에러 다루기</h2>
<p>‘예측 가능한 에러(Expected error)’란 애플리케이션에서 발생할 수 있는 일반적인 동작의 에러이다. 예를 들어, 서버사이드에서 일어나는 폼 유효성 검증이나 실패한 요청이 있다. 이러한 에러는 명시적으로 처리하고 클라이언트에 반환해야 한다.</p>
<h3 id="server-functionsactions"><strong>Server Functions(Actions)</strong></h3>
<p>서버 함수 내에서는 <code>try...catch</code> 문이 아닌 단순히 에러를 던짐으로써 반환한다. <code>useActionState</code> 를 사용하여 서버 함수 내의 반환된 예측 가능한 에러를 처리할 수 있다. 그러면 <code>try...catch</code> 는 어디에서 이루어지는가? <code>useActionState</code> 내에서 이러한 동작이 추상화되어 있으며 개발자는 단순히 이 훅에서 반환되는 값을 활용하여 폼 상태를 관리할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/85bddebb-64c3-4594-8d45-6fc6c746b23c/image.png" alt=""></p>
<pre><code class="language-tsx">&#39;use server&#39;

export async function createPost(prevState: any, formData: FormData) {
  const title = formData.get(&#39;title&#39;)
  const content = formData.get(&#39;content&#39;)

  const res = await fetch(&#39;https://api.vercel.app/posts&#39;, {
    method: &#39;POST&#39;,
    body: { title, content },
  })
  const json = await res.json()

  // try...catch 문으로 에러를 처리하지 않고 바로 반환
  if (!res.ok) {
    return { message: &#39;Failed to create post&#39; }
  }
}</code></pre>
<pre><code class="language-tsx">&#39;use client&#39;

import { useActionState } from &#39;react&#39;
import { createPost } from &#39;@/app/actions&#39;

const initialState = {
  message: &#39;&#39;,
}

export function Form() {
  const [state, formAction, pending] = useActionState(createPost, initialState)

  return (
    &lt;form action={formAction}&gt;
      &lt;label htmlFor=&quot;title&quot;&gt;Title&lt;/label&gt;
      &lt;input type=&quot;text&quot; id=&quot;title&quot; name=&quot;title&quot; required /&gt;
      &lt;label htmlFor=&quot;content&quot;&gt;Content&lt;/label&gt;
      &lt;textarea id=&quot;content&quot; name=&quot;content&quot; required /&gt;
      // createPost 수행 후 반환된 상태에 따라 UI를 표시
      {state?.message &amp;&amp; &lt;p aria-live=&quot;polite&quot;&gt;{state.message}&lt;/p&gt;}
      &lt;button disabled={pending}&gt;Create Post&lt;/button&gt;
    &lt;/form&gt;
  )
}</code></pre>
<h3 id="server-components">Server Components</h3>
<p>서버 컴포넌트 내에서 데이터 페칭 수행 시, 조건부 에러 메시지를 렌더링하거나 리다이렉트 시킬 수 있다.</p>
<pre><code class="language-tsx">export default async function Page() {
  const res = await fetch(`https://...`)
  const data = await res.json()

  if (!res.ok) {
    return &#39;There was an error.&#39;
  }

  return &#39;...&#39;
}</code></pre>
<h3 id="not-found">Not found</h3>
<p>라우트 세그먼트 내에서 <code>notFound</code> 함수를 실행함으로써 미리 정의된 <code>not-found.js</code> 파일에서 404 UI를 띄울 수 있다.</p>
<pre><code class="language-tsx">// app/blog/[slug]/page.tsx
import { getPostBySlug } from &#39;@/lib/posts&#39;

export default async function Page({ params }: { params: { slug: string } }) {
  const { slug } = await params
  const post = getPostBySlug(slug)

  if (!post) {
    notFound()
  }

  return &lt;div&gt;{post.title}&lt;/div&gt;
}</code></pre>
<pre><code class="language-tsx">// app/blog/[slug]/not-found.tsx
export default function NotFound() {
  return &lt;div&gt;404 - Page Not Found&lt;/div&gt;
}</code></pre>
<h2 id="예측-불가능한-에러-다루기"><strong>예측 불가능한 에러 다루기</strong></h2>
<p>예측 불가능한 에러는 애플리케이션의 일반적인 흐름 중에 발생하지 않는 버그나 이슈를 말한다. 이는 결국 잡히지 않는 에러를 처리하는 것인데 자바스크립트에서는 보통 에러를 외부로 다시 던짐으로써 이를 외부에서 처리하는 방식을 취했다.</p>
<h3 id="에러-바운더리로-감싸기">에러 바운더리로 감싸기</h3>
<p>React에서는 자식 컴포넌트에서 잡히지 않은 에러를 상위에서 <code>Errorboundary</code> 클래스를 별도로 정의하여 사용했다면, Next.js는 동일한 동작을 <code>error.js</code> 로 추상화해놓았다.</p>
<ul>
<li><code>error.js</code> : 특정 라우트 세그먼트에서 발생하는 예외를 독립적으로 처리하여 사용자에게 더욱 안정적인 경험을 제공한다. 이는 애플리케이션의 견고성을 높이는 데 기여한다.</li>
</ul>
<pre><code class="language-tsx">&#39;use client&#39; // 에러 바운더리는 클라이언트 컴포넌트여야 함

import { useEffect } from &#39;react&#39;

export default function Error({
  error,
  reset,
}: {
  error: Error &amp; { digest?: string }
  reset: () =&gt; void
}) {
  useEffect(() =&gt; {
    // Log the error to an error reporting service
    console.error(error)
  }, [error])

  return (
    &lt;div&gt;
      &lt;h2&gt;Something went wrong!&lt;/h2&gt;
      &lt;button
        onClick={
          // Attempt to recover by trying to re-render the segment
          () =&gt; reset()
        }
      &gt;
        Try again
      &lt;/button&gt;
    &lt;/div&gt;
  )
}</code></pre>
<p>에러 바운더리는 렌더링 중 발생한 에러를 잡고 fallback UI를 제공함으로써 흰 화면이 보이며 애플리케이션 전체가 죽는 것을 방지한다. </p>
<p>여기서 유의해야 할 점이, 에러 바운더리는 이벤트 핸들러나 비동기 코드의 에러는 처리할 수 없고, 렌더링 중에 일어나는 동기적인 에러만 처리할 수 있다는 것이다. 비동기적인 에러의 경우 에러를 직접 잡아주거나 상태로써 관리해야 한다.</p>
<pre><code class="language-tsx">&#39;use client&#39;

import { useState } from &#39;react&#39;

export function Button() {
  const [error, setError] = useState(null)

  const handleClick = () =&gt; {
    try {
      // do some work that might fail
      throw new Error(&#39;Exception&#39;)
    } catch (reason) {
      setError(reason)
    }
  }

  if (error) {
    /* render fallback UI */
  }

  return (
    &lt;button type=&quot;button&quot; onClick={handleClick}&gt;
      Click me
    &lt;/button&gt;
  )
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js] Server Actions, 서버에서 실행되는 비동기 함수 (feat. 캐시 재검증)]]></title>
            <link>https://velog.io/@one1_programmer/Next.js-Server-Actions-%EC%84%9C%EB%B2%84%EC%97%90%EC%84%9C-%EC%8B%A4%ED%96%89%EB%90%98%EB%8A%94-%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%95%A8%EC%88%98-feat.-%EC%BA%90%EC%8B%9C-%EC%9E%AC%EA%B2%80%EC%A6%9D</link>
            <guid>https://velog.io/@one1_programmer/Next.js-Server-Actions-%EC%84%9C%EB%B2%84%EC%97%90%EC%84%9C-%EC%8B%A4%ED%96%89%EB%90%98%EB%8A%94-%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%95%A8%EC%88%98-feat.-%EC%BA%90%EC%8B%9C-%EC%9E%AC%EA%B2%80%EC%A6%9D</guid>
            <pubDate>Sat, 03 Jan 2026 05:07:49 GMT</pubDate>
            <description><![CDATA[<p><strong>Server Action(서버 액션)</strong>이란 브라우저에서 호출할 수 있는 서버에서 실행되는 비동기 함수로서, 폼 제출 및 데이터 변조를 처리하기 위해 서버에서 실행된다. html <code>&lt;form&gt;</code> 태그의 action 속성에 함수를 지정하고, 해당 함수에 <code>&#39;use server&#39;</code> 지시자가 있으면 함수는 Next 서버에서만 실행된다. 이를 통해, 오직 자바스크립트 함수를 통해서 브라우저와 서버 간의 통신을 가능하게 한다.</p>
<p>서버 액션을 만들면, <code>&#39;use server&#39;</code> 하위 코드에 대한 api가 자동으로 생성되며 브라우저에서 <code>&lt;form&gt;</code> 태그를 제출하면 자동으로 호출된다.</p>
<ul>
<li>특징<ul>
<li>서버 컴포넌트에서 서버 액션을 호출하려면 인라인 함수 레벨 또는 모듈 레벨의 <code>&#39;use server&#39;</code> 지시어를 추가한다.</li>
<li>클라이언트 컴포넌트에서 서버 액션을 호출하려면 파일 상단에 <code>&#39;use server&#39;</code> 지시어를 추가한다.</li>
<li>API 관련 동작을 간결하게 작성하게 해준다.</li>
<li>서버측에서만 실행되므로 보안상 유리하다.</li>
</ul>
</li>
</ul>
<pre><code class="language-tsx">function ReviewEditor() {
  async function createReviewAction(formData: FormData) {
    &quot;use server&quot;;

    // File 타입은 받지 않으므로 null이 아니면 문자열로 변환
    const content = formData.get(&quot;content&quot;)?.toString();
    const author = formData.get(&quot;author&quot;)?.toString();
  }

  return (
    &lt;section&gt;
      &lt;form action={createReviewAction}&gt;
        &lt;input name=&quot;content&quot; placeholder=&quot;리뷰 내용&quot; /&gt;
        &lt;input name=&quot;author&quot; placeholder=&quot;작성자&quot; /&gt;
        &lt;button type=&quot;submit&quot;&gt;작성하기&lt;/button&gt;
      &lt;/form&gt;
    &lt;/section&gt;
  );
}</code></pre>
<h1 id="🤔개념">🤔개념</h1>
<h2 id="추가-인수-전달">추가 인수 전달</h2>
<h3 id="bind-메서드-사용"><code>bind</code> 메서드 사용</h3>
<p>JavaScript <code>bind</code> 메서드를 사용하여 Server Action에 추가 인수를 전달할 수 있다.</p>
<pre><code class="language-tsx">&#39;use client&#39;

import { updateUser } from &#39;./actions&#39;

export function UserProfile({ userId }: { userId: string }) {
  const updateUserWithId = updateUser.bind(null, userId)

  return (
    &lt;form action={updateUserWithId}&gt;
      &lt;input type=&quot;text&quot; name=&quot;name&quot; /&gt;
      &lt;button type=&quot;submit&quot;&gt;Update User Name&lt;/button&gt;
    &lt;/form&gt;
  )
}</code></pre>
<h3 id="hidden-타입의-입력-필드-전달"><code>hidden</code> 타입의 입력 필드 전달</h3>
<p><code>&lt;input type=&quot;hidden&quot; name=&quot;userId&quot; value={userId} /&gt;</code> 와 같이 폼에 숨겨진 입력 필드로 인수를 전달할 수 있다.</p>
<pre><code class="language-tsx">function ReviewEditor({ movieId }: { movieId: string }) {
  return (
    &lt;section&gt;
      &lt;form action={createReviewAction}&gt;
        &lt;input name=&quot;movieId&quot; value={movieId} hidden readOnly /&gt;
        &lt;input type=&quot;text&quot; name=&quot;content&quot; placeholder=&quot;리뷰를 입력하세요...&quot; /&gt;
        &lt;input type=&quot;tex&quot; name=&quot;author&quot; placeholder=&quot;작성자&quot; /&gt;
        &lt;button type=&quot;submit&quot;&gt;리뷰 작성&lt;/button&gt;
      &lt;/form&gt;
    &lt;/section&gt;
  );
}</code></pre>
<pre><code class="language-tsx">&quot;use server&quot;;

export default async function createReviewAction(formData: FormData) {
  const movieId = formData.get(&quot;movieId&quot;)?.toString();
  const content = formData.get(&quot;content&quot;)?.toString();
  const author = formData.get(&quot;author&quot;)?.toString();

  if (!content || !author || !movieId) {
    return;
  }
}</code></pre>
<p>HTML <code>&lt;form&gt;</code> 요소의 데이터는 자동으로 <code>formData</code> 객체로 묶여 단일 매개변수로 전달된다.</p>
<h2 id="캐시-재검증">캐시 재검증</h2>
<p>Server Action으로 데이터를 성공적으로 변경(추가, 삭제 등)한 후, 변경사항을 반영하여 UI를 업데이트하고 관련 캐시를 무효화해야 한다. 이를 통해 사용자는 항상 최신 정보를 볼 수 있다.</p>
<h3 id="revalidatepath"><code>revalidatePath</code></h3>
<p>특정 경로에 해당하는 캐시된 데이터를 없앤다. 즉, 데이터를 다시 불러온다. </p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/2498063a-b4e9-4bf4-a481-5a4d904af08a/image.png" alt=""></p>
<ul>
<li><p><strong>특징</strong></p>
<ul>
<li>서버 액션 또는 서버 컴포넌트에서만 호출할 수 있다.</li>
<li>경로 하위에서 일어나는 모든 데이터 페칭이 캐시 설정 여부와 관계없이 재검증된다.</li>
<li>풀 라우트 캐시는 삭제되며, 이후 동일한 요청에서야 Dynamic Page로써 불러온 후 해당 데이터를 풀 라우트 캐시로 저장한다.</li>
</ul>
</li>
<li><p><strong>파라미터</strong></p>
<ul>
<li><strong><code>path</code>:</strong> 특정 경로나 파일명이 올 수 있다.</li>
<li><strong><code>type</code>:</strong> page이거나 layout이 올 수 있다.</li>
</ul>
</li>
<li><p><strong>방법</strong></p>
<ul>
<li><p>특정 주소에 해당하는 페이지만 재검증</p>
<pre><code class="language-tsx">  revalidatePath(`/book/${bookId}`);</code></pre>
</li>
<li><p>특정 경로의 모든 동적 페이지를 재검증</p>
<pre><code class="language-tsx">  revalidatePath(&quot;/book/[id]&quot;, &quot;page&quot;);</code></pre>
</li>
<li><p>특정 레이아웃을 갖는 모든 페이지를 재검증</p>
<pre><code class="language-tsx">  // &quot;레이아웃이 위치한 경로&quot;, &quot;layout&quot;
  revalidatePath(&quot;/(with-searchbar)&quot;, &quot;layout&quot;);

  // 응용) 모든 데이터 재검증
  revalidatePath(&quot;/&quot;, &quot;layout&quot;)</code></pre>
</li>
</ul>
</li>
</ul>
<pre><code>### `revalidateTag`

태그 기준으로 데이터 캐시 재검증한다. “특정 주소에 해당하는 페이지만 재검증” 시 해당 페이지 내에서 서로 다른 데이터 페칭이 2개 이상 존재하는 경우, 불필요하게 페이지 내 모든 캐시를 재검증하는 경우가 생길 수 있다. 따라서 페칭 자체에 태그를 달아 이러한 문제를 해결할 수 있다.

```tsx
const response = await fetch(
    `${process.env.NEXT_PUBLIC_API_SERVER_URL}/review/book/${bookId}`,
    { next: { tags: [`review-${bookId}`] } } // revalidateTag을 사용하기 위해 추가
);

// ---

revalidateTag(`review-${bookId}`); // 서버 액션 측
```</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js] Full Route Cache, 페이지 자체를 캐싱하는 메커니즘]]></title>
            <link>https://velog.io/@one1_programmer/Next.js-Full-Route-Cache-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%9E%90%EC%B2%B4%EB%A5%BC-%EC%BA%90%EC%8B%B1%ED%95%98%EB%8A%94-%EB%A9%94%EC%BB%A4%EB%8B%88%EC%A6%98</link>
            <guid>https://velog.io/@one1_programmer/Next.js-Full-Route-Cache-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%9E%90%EC%B2%B4%EB%A5%BC-%EC%BA%90%EC%8B%B1%ED%95%98%EB%8A%94-%EB%A9%94%EC%BB%A4%EB%8B%88%EC%A6%98</guid>
            <pubDate>Tue, 30 Dec 2025 01:03:02 GMT</pubDate>
            <description><![CDATA[<p><strong>Full Route Cache</strong>는 한 마디로 ‘페이지 자체를 캐싱하는 것’이라 할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/540b8f63-2e4a-420c-86a4-a78e91b92116/image.png" width="700">

<p>Next 서버 측에서 빌드 타임에 특정 페이지의 렌더링 결과를 캐싱하는 기능이다. 빌드 시에 Request Memoization와 Data Cache를 거쳐 반환된 특정 경로에 대한 페이지를 Full Route Cache로 저장하는 것이다. 이후 같은 경로의 페이지에 접속 요청이 생기면, Full Route Cache가 HIT 되어 캐시된 페이지를 브라우저에 반환한다(Pages Router의 SSG 방식의 렌더링과 유사하게 동작한다고 할 수 있다). 이를 통해, 요청이 오면 캐시된 결과를 즉시 반환하여 응답 속도를 크게 높일 수 있다.</p>
<h1 id="🤔개념">🤔개념</h1>
<h2 id="페이지-분류-서버-컴포넌트에만-해당"><strong>페이지 분류 (서버 컴포넌트에만 해당)</strong></h2>
<p>Full Route Cache는 Static Page에만 적용된다. 이때, Static Page는 기본적으로 Dynamic Page가 아닌 경우의 페이지에 해당한다. </p>
<h3 id="dynamic-page"><strong>Dynamic Page</strong></h3>
<ul>
<li><p>특정 페이지가 접속 요청을 받을 때마다 매번 변화가 생기거나 데이터가 달라지는 경우</p>
<ul>
<li><p>캐시되지 않는 Data Fetching을 사용하는 경우</p>
<pre><code class="language-tsx">  async function ServerComponent() {
      const response = await fetch(&quot;...&quot;); // 또는 no-store 옵션 있는 경우
      return &lt;div&gt;...&lt;/div&gt;;
  }</code></pre>
</li>
</ul>
</li>
<li><p>동적 함수(쿠키, 헤더, 쿼리 스트링)을 사용하는 컴포넌트가 존재하는 경우</p>
<ul>
<li><p>쿠키를 사용하는 경우</p>
<pre><code class="language-tsx">  import { cookies } from &quot;next/headers&quot;;

  async function ServerComponent() {
      const cookieStore = cookies();
      const theme = cookieStore.get(&quot;theme&quot;);

      return &lt;div&gt;...&lt;/div&gt;;
  }</code></pre>
</li>
<li><p>쿼리스트링을 사용하는 경우</p>
<pre><code class="language-tsx">  async function Page({ searchParams }: { searchParams: { q: string }}) {
      const q = searchParams.q;

      return &lt;div&gt;...&lt;/div&gt;;
  }</code></pre>
</li>
</ul>
</li>
</ul>
<table>
<thead>
<tr>
<th>동적 함수 (쿠키,헤더,쿼리스트링)</th>
<th>데이터 캐시</th>
<th>페이지 분류</th>
</tr>
</thead>
<tbody><tr>
<td>YES</td>
<td>NO</td>
<td>Dynamic Page</td>
</tr>
<tr>
<td>YES</td>
<td>YES</td>
<td>Dynamic Page</td>
</tr>
<tr>
<td>NO</td>
<td>NO</td>
<td>Dynamic Page</td>
</tr>
<tr>
<td>NO</td>
<td>YES</td>
<td>Static Page</td>
</tr>
</tbody></table>
<p>결론적으로, Full Route Cache를 적용할 수 있는 Static Page로 페이지를 구성하는 것이 권장된다. 물론, Dynamic Page를 사용해야 하는 경우에도 Request Memoization, Data Cache는 유효하므로 각각 경우에 맞게 최적화할 수 있다. </p>
<p>아래의 예시의 경우, 쿼리 스트링을 가져와서 사용자와 동적으로 상호작용해야 하므로, 해당 컴포넌트에서는 Static Page로 만들 수 없으며 Full Route Cache를 적용할 수 없다. 따라서 Data Cache를 적용하는 것이 최선이다.</p>
<ul>
<li><p><strong>Static Page로 만들 수 없는 경우 예시</strong></p>
<pre><code class="language-tsx">  export default async function Page({
    searchParams,
  }: {
    searchParams: Promise&lt;{ q?: string }&gt;;
  }) {
    const resolvedSearchParams = await searchParams;
    const response = await fetch(
      `${process.env.NEXT_PUBLIC_API_SERVER_URL}/book/search?q=${resolvedSearchParams.q}`,
      { cache: &quot;force-cache&quot; }
    );

    if (!response.ok) {
      return &lt;div&gt;에러가 발생했습니다.&lt;/div&gt;;
    }
    const books: BookData[] = await response.json();

    return (
      &lt;div&gt;
        {books.map((book) =&gt; (
          &lt;BookItem key={book.id} {...book} /&gt;
        ))}
      &lt;/div&gt;
    );
  }</code></pre>
</li>
</ul>
<h2 id="invalidation">Invalidation</h2>
<p>Full Route Cache를 무효화하는 방법은 두 가지가 있다.</p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/0235a661-5990-402f-a042-a7c8761ba903/image.png" width="700">

<ul>
<li><strong>revalidation:</strong> Data Cache를 재검증하면, 오래된(stale한) 데이터 대신 fresh한 데이터를 불러온다. 따라서 서버에서 컴포넌트를 새로운 데이터를 기반으로 다시 렌더링하고 새 렌더링 출력을 캐시하여 Router Cache를 무효화한다(page router의 ISR 방식의 렌더링과 유사하게 동작한다고 할 수 있다).</li>
<li><strong>재배포:</strong> 배포 간에 지속되는 Data Cache와 달리 Full Route Cache는 새 배포 시 지워진다.</li>
</ul>
<h2 id="generatestaticparams"><code>generateStaticParams</code></h2>
<p>쿼리 파라미터를 갖는 상세 페이지의 경우, 동적으로 경로를 가져와야 하지만 Next가 제공하는 <code>generateStaticParams</code> 함수를 통해 지정한 쿼리 파라미터의 값을 가지는 페이지를 빌드 타임에 미리 생성할 수 있다. 따라서 이 함수는 Pages Router의 <code>getStaticPaths</code> 와 유사한 기능을 하는 함수라 할 수 있다.</p>
<pre><code class="language-tsx">export function generateStaticParams() {
  return [{ id: &quot;1&quot; }, { id: &quot;2&quot; }, { id: &quot;3&quot; }]; // 값은 문자열로 지정해야 함
}

export default async function Page({
  params,
}: {
  params: Promise&lt;{ id?: string }&gt;;
}) {
  const resolvedParams = await params;
  const response = await fetch(
    `${process.env.NEXT_PUBLIC_API_SERVER_URL}/book/${resolvedParams.id}`
  );
  if (!response.ok) {
    return &lt;div&gt;에러가 발생했습니다.&lt;/div&gt;;
  }

  return (&lt;&gt;&lt;/&gt;);
}</code></pre>
<p>Next.js는 <code>generateStaticParams</code> 에 지정하지 않은 페이지에 대해서도 실시간으로 캐싱을 진행한다. 특정 상세 페이지 재접속 시 네트워크 응답 속도가 108ms-&gt;21ms 로 감소한 것을 확인할 수 있다.</p>
<ul>
<li>초기 접속<p><img src="https://velog.velcdn.com/images/one1_programmer/post/87b82b6e-4d85-401a-8350-708b1766713a/image.png" width="700">


</li>
</ul>
<ul>
<li>재접속<p><img src="https://velog.velcdn.com/images/one1_programmer/post/ebf5cd42-953d-420e-9809-d2d2e1467a34/image.png" width="700">


</li>
</ul>
<h3 id="dynamicparams"><code>dynamicParams</code></h3>
<p>만약, <code>generateStaticParams</code> 에 명시하지 않은 페이지의 경우 모두 NotFound 페이지로 보내고 싶을 수도 있다. 이러한 경우에는 <code>dynamicParams</code> 의 값을 <code>false</code> 로 지정하면 된다.</p>
<pre><code class="language-tsx">export const dynamicParams = false;

export function generateStaticParams() {
  return [{ id: &quot;1&quot; }, { id: &quot;2&quot; }, { id: &quot;3&quot; }];
}</code></pre>
<h2 id="route-segment-config">Route Segment Config</h2>
<h3 id="dynamic">dynamic</h3>
<p>페이지의 유형을 강제할 수 있다. 페이지 상단에 <code>export const dynamic = &quot;옵션명&quot;</code> 형식으로 사용 가능하다.</p>
<table>
<thead>
<tr>
<th>옵션명</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>auto</td>
<td>최초 요청에 대해 서버에서 새로 생성, 이후 요청에 대해 캐시된 페이지를 사용</td>
</tr>
<tr>
<td>force-dynamic</td>
<td>페이지를 강제로 Dynamic Page로 설정</td>
</tr>
<tr>
<td>force-static</td>
<td>페이지를 강제로 Static Page로 설정</td>
</tr>
<tr>
<td>error</td>
<td>페이지를 강제로 Static Page로 설정 (설정하면 안되는 이유를 빌드 오류로 표시) 에러 발생 시 캐시된 페이지를 사용</td>
</tr>
</tbody></table>
<h2 id="클라이언트-라우터-캐시">클라이언트 라우터 캐시</h2>
<p>브라우저에 저장되는 캐시로, 페이지 이동을 효율적으로 진행하기 위해 페이지의 일부 데이터를 보관한다. 페이지 이동 시 공통적으로 사용되는 레이아웃 컴포넌트 등의 RSC Payload 데이터를 캐시한다. 이를 통해 페이지 전환 시 불필요한 데이터 요청 없이 캐시된 레이아웃을 재사용하여 빠르게 화면을 구성할 수 있다.</p>
<blockquote>
<p><strong>참고자료</strong>
한 입 크기로 잘라먹는 Next.js (이정환)</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js] App Router에서의 데이터 페칭, Data Cache와 Request Memoization을 중심으로 살펴보기]]></title>
            <link>https://velog.io/@one1_programmer/Next.js-App-Router%EC%97%90%EC%84%9C%EC%9D%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8E%98%EC%B9%AD-Data-Cache%EC%99%80-Request-Memoization%EC%9D%84-%EC%A4%91%EC%8B%AC%EC%9C%BC%EB%A1%9C-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@one1_programmer/Next.js-App-Router%EC%97%90%EC%84%9C%EC%9D%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8E%98%EC%B9%AD-Data-Cache%EC%99%80-Request-Memoization%EC%9D%84-%EC%A4%91%EC%8B%AC%EC%9C%BC%EB%A1%9C-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Fri, 26 Dec 2025 05:33:37 GMT</pubDate>
            <description><![CDATA[<p>기존 Pages Router에서는 클라이언트 컴포넌트로만 이루어져 있었다. 이때, 클라이언트 컴포넌트에 <code>async</code> 키워드를 사용하면 브라우저의 동작에 문제를 일으킬 수 있기 때문에 해당 키워드를 사용하는 것은 권장되지 않는다. 따라서 컴포넌트 내에서 비동기적으로 데이터를 불러오는 것은 불가능했으며, 해당 동작을 수행하려면 서버에서만 실행되는 <code>getServerSideProps</code> 등의 특수한 함수를 사용해야 했다. 하지만 이런 동작은 자식 컴포넌트에서 데이터를 사용하기 위해 부모에서 데이터를 페칭하고 계속해서 props를 내려주거나 Context API를 사용해야 했다.</p>
<p>반면, App Router에서는 서버 컴포넌트를 활용함으로써 <code>async</code> 키워드와 함께 컴포넌트에서 데이터 페칭을 수행할 수 있게 되었다. 따라서 데이터가 필요한 컴포넌트에서 개별적으로 데이터 페칭을 수행하면 된다.</p>
<h1 id="🤔개념">🤔개념</h1>
<h2 id="data-cache">Data Cache</h2>
<p><code>fetch</code> 메서드를 활용해 불러온 데이터를 Next 서버에서 보관하는 기능으로, 데이터를 영구적으로 보관하거나, 특정 시간을 주기로 갱신하는 것도 가능하다. 이를 통해 불필요한 데이터 요청 수를 줄여 웹 서비스의 성능을 크게 개선할 수 있다.</p>
<blockquote>
<p>이 기능은 fetch를 확장한 것으로, axios 등의 외부 라이브러리에는 적용할 수 없다.</p>
</blockquote>
<blockquote>
<p>Next.js에서는 데이터 페칭 시 서버에 로그를 찍어주는 기능을 제공한다.</p>
<p><code>next.config.mjs</code></p>
<pre><code class="language-tsx"> const nextConfig: NextConfig = {
  /* config options here */
  logging: {
    fetches: {
      fullUrl: true,
    },
  },
}</code></pre>
</blockquote>
<h3 id="no-store"><code>no-store</code></h3>
<p>데이터 페칭의 결과를 저장하지 않으며, 캐싱을 전혀 수행하지 않는다. fetch에 아무런 캐싱 옵션을 지정하지 않는 경우와 동일한 동작을 한다.</p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/39709865-bdef-49ac-9ca9-97465d7f51a7/image.png" width="700">

<pre><code class="language-tsx">async function AllMovies() {
  const response = await fetch(
    `${process.env.NEXT_PUBLIC_API_SERVER_URL}/movie`,
    { cache: &quot;no-store&quot; }
  );
}</code></pre>
<pre><code class="language-tsx">async function RecoMovies() {
  const response = await fetch(
    `${process.env.NEXT_PUBLIC_API_SERVER_URL}/movie/random`
  );
}</code></pre>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/90b90bb4-5055-42a4-a92f-372d13d66f46/image.png" width="600">

<h3 id="force-cache"><code>force-cache</code></h3>
<p>요청의 결과를 무조건 캐싱한다. 한 번 호출된 이후에는 다시는 호출되지 않는다.</p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/33c21bb4-823e-463f-ba86-4b365ea47f49/image.png" width="700">

<pre><code class="language-tsx">async function RecommendMovies() {
  const response = await fetch(
    `${process.env.NEXT_PUBLIC_API_URL}/movie/random`,
    {
      cache: &quot;force-cache&quot;,
    }
  );
}</code></pre>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/94d58f92-f282-4f37-bd1c-a479d0409036/image.png" width="500">

<h3 id="next--revalidate-x-"><code>{next: { revalidate: x }}</code></h3>
<p>특정 시간을 주기로 캐시를 업데이트한다. Pages Router의 ISR 방식과 유사하다.</p>
<pre><code class="language-tsx">const response = await fetch(
  `${process.env.NEXT_PUBLIC_API_URL}/movie/random`,
  {
    next: { revalidate: 3 },
  }
);</code></pre>
<h3 id="-next--tags-a--"><code>{ next: { tags: [&#39;a&#39;] } }</code></h3>
<p>요청이 들어왔을 때 데이터를 최신화하며, Pages Router의 on-demand revalidate 방식과 유사하다.</p>
<table>
<thead>
<tr>
<th>옵션명</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>&quot;no-store&quot; (default, Next v15 이상)</td>
<td>데이터 페칭의 결과를 저장하지 않으며, 캐싱을 전혀 수행하지 않는다.</td>
</tr>
<tr>
<td>&quot;force-cache&quot;</td>
<td>요청의 결과를 무조건 캐싱하며, 한번 호출된 이후에는 다시 호출되지 않는다.</td>
</tr>
<tr>
<td>{ next: { revalidate: x(초) } }</td>
<td>특정 시간을 주기로 캐시를 업데이트하며, page router의 ISR 방식과 유사하다.</td>
</tr>
<tr>
<td>{ next: { tags: [&#39;a&#39;] } }</td>
<td>요청이 들어왔을 때 데이터를 최신화하며, page router의 on-demand revalidate 방식과 유사하다.</td>
</tr>
</tbody></table>
<h2 id="request-memoization">Request Memoization</h2>
<p>하나의 페이지를 렌더링하는 동안에 중복된 API 요청을 캐싱하기 위해 존재하는 개념이다. App Router에서는 서버 컴포넌트의 도입으로 서로 다른 컴포넌트가 서로 같은 데이터를 필요로 하는 상황에서 중복된 데이터 페칭이 필연적으로 수행된다.</p>
<p>리퀘스트 메모이제이션은 같은 페이지를 렌더링하는 과정에서 여러 컴포넌트가 동일한 <code>fetch</code> 요청을 할 때, 실제 요청은 한 번만 수행하고 결과를 공유하게 해준다. 이를 통해 동일한 API 요청이 한 번의 서버 렌더링 주기 내에서 중복되는 것을 방지한다.</p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/2e7c8fcb-1fa5-4f98-a4f2-9d6d418a99cb/image.png" width="700">

<table>
<thead>
<tr>
<th></th>
<th>Request Memoization</th>
<th>Data Cache</th>
</tr>
</thead>
<tbody><tr>
<td>보존기간</td>
<td>렌더링 종료 시 소멸</td>
<td>서버 가동중에는 영구 보관</td>
</tr>
<tr>
<td>목적</td>
<td>하나의 페이지를 렌더링하는 동안 중복된 API 요청 보관</td>
<td>백엔드 서버로부터 불러온 데이터 보관</td>
</tr>
</tbody></table>
<p>데이터 캐시는 서버 다운 전까지 지속되지만, 리퀘스트 메모이제이션은 한 번의 서버 렌더링 주기 후에 초기화된다.</p>
  <p><img src="https://velog.velcdn.com/images/one1_programmer/post/78930b84-40be-4d15-bd78-7053a8e6d44d/image.png" width="700">

<blockquote>
<p><strong>참고자료</strong>
한 입 크기로 잘라먹는 Next.js (이정환)</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[회고] JSCODE 모의면접 스터디 (컴퓨터 네트워크) 10기 참여 후기]]></title>
            <link>https://velog.io/@one1_programmer/%ED%9A%8C%EA%B3%A0-JSCODE-%EB%AA%A8%EC%9D%98%EB%A9%B4%EC%A0%91-%EC%8A%A4%ED%84%B0%EB%94%94-%EC%BB%B4%ED%93%A8%ED%84%B0-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-10%EA%B8%B0-%EC%B0%B8%EC%97%AC-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@one1_programmer/%ED%9A%8C%EA%B3%A0-JSCODE-%EB%AA%A8%EC%9D%98%EB%A9%B4%EC%A0%91-%EC%8A%A4%ED%84%B0%EB%94%94-%EC%BB%B4%ED%93%A8%ED%84%B0-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-10%EA%B8%B0-%EC%B0%B8%EC%97%AC-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Thu, 25 Dec 2025 15:36:10 GMT</pubDate>
            <description><![CDATA[<p>벌써 5주간의 JSCODE 모의면접 스터디 (컴퓨터 네트워크) 10기가 마무리되었다. 스터디를 참여한 계기부터 배운 점까지 정리해보려고 한다. 이 글이 앞으로의 기수에 참여하게 될 누군가에게 도움이 되었으면 한다.</p>
<h3 id="🤔참여하게된-계기">🤔참여하게된 계기</h3>
<p>계기로는 크게 JSCODE를 선택한 이유와 &#39;컴퓨터 네트워크&#39;를 고른 이유로 나눠 설명하겠다.</p>
<h4 id="왜-jscode">왜 JSCODE?</h4>
<p>우선, 현재 취준생으로서 스터디의 필요성을 느끼고 있었다. 인프런과 같은 곳에서 관련 스터디에 참여할 수도 있었겠지만, JSCODE를 선택한 이유는 다음과 같다.</p>
<ul>
<li>강제성 부여: 일정 금액을 내고 스터디에 참여할 수 있으며, 출결/과제 에 따라 일정 부분 환급을 받을 수 있다. 아무래도 내 돈을 내고 하다보니 더 열심히 참여해야 겠다는 생각이 들 수 밖에 없었다..😅</li>
<li>멘토님 존재: 현직자 분이 스터디의 시작에 이력서, 면접 등에 대한 팁을 공유하고 짧은 시간 Q&amp;A를 진행해주신다. 이번 멘토님은 백엔드 직무셔서 그렇게 많은 질문을 하지는 못했지만 이 시간을 잘 활용하면 좋았을 것 같다.</li>
</ul>
<h4 id="프론트엔드에-필요한-cs-지식-데이터베이스-운영체제">프론트엔드에 필요한 CS 지식, 데이터베이스, 운영체제..?</h4>
<p>프론트엔드 개발자로서 3대 전공 CS인 데이터베이스, 운영체제, 컴퓨터 네트워크 중 최우선순위 단연 컴퓨터 네트워크라고 생각했다. 왜냐하면 웹의 기반이 되는 CS 중 가장 비중이 크다고 느꼈기 때문이다. 실제로도 네이버와 같은 대기업에서 HTTP와 관련한 깊이 있는 질문을 받았다고 들었고, 실제 기술 면접에서도 CORS, REST API는 받아본 적이 있었다(물론 프론트엔드 개발자에게 전공 CS보다 중요한 건 웹 또는 브라우저 라고 생각한다).</p>
<h3 id="📝진행방식">📝진행방식</h3>
<p>컴퓨터 네트워크의 경우, 총 5주 동안 주 1회(금요일) 진행했다. 다음 주차에 진행할 질문들이 직전 주차에 공개된다. </p>
<blockquote>
<p>2주차부터는 인성 질문도 포함되어 있다.</p>
</blockquote>
<p>공개된 질문을 포함하여 관련 내용을 개인 기술 블로그나 공개 링크에 작성하여 과제로 제출한다.</p>
<h4 id="면접-진행-과정">면접 진행 과정</h4>
<ol>
<li>면접관, 면접자, 관찰자, 타임키퍼 로 역할 분담한다.</li>
<li>모의 면접 시작: 15분 간 면접을 진행한다. </li>
<li>상호 간 피드백: 면접이 종료되면 구글 폼을 통해 면접자에 대한 피드백을 작성한다. </li>
</ol>
<blockquote>
<p>멘토님의 질문을 받을 기회가 올 수 있다. 나의 경우 5주 진행하면서 1회 받을 수 있었다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/2fc5a4ed-3e10-45d4-89d1-93c524bdd87b/image.png" width="600">
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/176a9668-862a-4aec-bff1-421ec5c7455c/image.png" width="600">


<h3 id="😎느낀-점">😎느낀 점</h3>
<p>모의 면접 과정에서 받은 피드백은 다음과 같았는데, 이미 어느정도 인지하고 있던 부분도 있었고 새로 깨닫게 된 부분도 있었다.</p>
<ul>
<li>*<em>답변이 모호하다: *</em>확실히 알지 못하는 경우에 답변을 모호하게 하는 경우가 있다.</li>
<li><strong>포인트를 먼저 말해라:</strong> 이것 또한 잘 알지 못하는 경우에 종종 두괄식이 안 지켜진 것 같다. </li>
<li><strong>예시에 대해 2가지 이상을 준비하면 좋다:</strong> 잘 알지 못한다는 인상을 줄 수도 있을 것 같다는 생각이 들었다.</li>
</ul>
<p>모의 면접을 통해, 들어본 것과 아는 것은 다르다는 것을 새삼 느꼈다. 전공 수업이나 개인 학습을 통해 Notion에 열심히 정리해놓은 지식을 공개된 블로그에 옮기는 과정과 그 내용을 바탕으로 직접 발화하는 과정을 통해서 말이다.</p>
<blockquote>
<p>스터디원 중에는 현직자 분들도 꽤나 있었는데, 운좋게도 프론트엔드 분들도 만나뵙게 되어 궁금한 점을 물어볼 수 있는 기회가 있었다. 스터디가 네트워킹의 기회로 이어질 수 있다는 점을 알게 되었다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프론트엔드] 이미지 최적화 시리즈(2): OffscreenCanvas과 함께 알아보는 메인 스레드 블로킹 해결기 (feat. Web Worker)]]></title>
            <link>https://velog.io/@one1_programmer/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B5%9C%EC%A0%81%ED%99%94-%EC%8B%9C%EB%A6%AC%EC%A6%88-OffscreenCanvas%EA%B3%BC-%ED%95%A8%EA%BB%98-%EC%95%8C%EC%95%84%EB%B3%B4%EB%8A%94-%EB%A9%94%EC%9D%B8-%EC%8A%A4%EB%A0%88%EB%93%9C-%EB%B8%94%EB%A1%9C%ED%82%B9-%ED%95%B4%EA%B2%B0%EA%B8%B0-feat.-Web-Worker</link>
            <guid>https://velog.io/@one1_programmer/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B5%9C%EC%A0%81%ED%99%94-%EC%8B%9C%EB%A6%AC%EC%A6%88-OffscreenCanvas%EA%B3%BC-%ED%95%A8%EA%BB%98-%EC%95%8C%EC%95%84%EB%B3%B4%EB%8A%94-%EB%A9%94%EC%9D%B8-%EC%8A%A4%EB%A0%88%EB%93%9C-%EB%B8%94%EB%A1%9C%ED%82%B9-%ED%95%B4%EA%B2%B0%EA%B8%B0-feat.-Web-Worker</guid>
            <pubDate>Thu, 25 Dec 2025 14:19:26 GMT</pubDate>
            <description><![CDATA[<p>이 글에서는 이미지 최적화 과정을 메인 스레드에서 분리하여 사용자 경험(UX)을 개선한 과정을 공유하고자 한다.</p>
<h1 id="🤯문제-상황">🤯문제 상황</h1>
<p>동아리 홈페이지 프로젝트에는 다중 이미지 업로드와 이미지 압축 및 리사이징을 지원하고 있다. 이때, 고해상도 이미지 여러 장을 업로드하는 경우 아래와 키보드 입력 처리가 멈추는 현상(프리징 현상)이 나타났다.</p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/ffd2004d-a315-454a-b953-0e8638f92cee/image.gif" alt=""></p>
<p>이미지 업로드 과정에 대한 퍼포먼스 측정 결과, 아래와 같이 메인 스레드의 이미지 리사이징 과정에서 Long Task들이 존재하는 것을 확인할 수 있었다.
<img src="https://velog.velcdn.com/images/one1_programmer/post/5b75406d-466d-45a0-8d9f-4ab729178d16/image.png" alt=""></p>
<p>기존 코드는 <code>react-image-file-resizer</code> 라이브러리를 통해 메인 스레드에서 이미지를 리사이징을 수행하고 있었다.</p>
<pre><code class="language-ts">// src/_utils/resizeImageFile.ts
import Resizer from &#39;react-image-file-resizer&#39;;

interface IResizeFileProps {
    file: File;
    targetWidth: number;
    targetHeight: number;
    compressFormat: &#39;JPEG&#39; | &#39;PNG&#39; | &#39;WEBP&#39;;
    quality?: number; // 압축 품질 (0~100)
}

export default function resizeImageFile({
    file,
    targetWidth,
    targetHeight,
    compressFormat,
    quality,
}: ResizeFileProps): Promise&lt;File&gt; {
    return new Promise(resolve =&gt; {
        Resizer.imageFileResizer(
            file,
            targetWidth,
            targetHeight,
            compressFormat,
            quality,
            0,
            uri =&gt; {
                console.log(&#39;🚀 ~ newPromise ~ uri:&#39;, uri);
                resolve(uri as File);
            },
            &#39;file&#39;,
        );
    });
}</code></pre>
<p>해당 라이브러리는 Image 객체 로드, 캔버스 그리기(drawImage), Blob 변환(toBlob)이 모두 메인 스레드 점유하는 문제가 있었다. 따라서 리사이징 과정을 백그라운드에서 수행하면 좋겠다고 판단했으며, 이전에 유튜브를 통해 학습했던 Web Worker가 떠올랐다. </p>
<p>이제 Web Worker 개념을 활용하여 문제를 해결해가는 과정에서 학습한 개념부터 상세 내용을 살펴보자.</p>
<h1 id="🤔개념">🤔개념</h1>
<h2 id="메인-스레드main-thread와-블로킹blocking">메인 스레드(Main Thread)와 블로킹(Blocking)</h2>
<p>브라우저는 기본적으로 단일 메인 스레드에서 JavaScript 실행하며, 이곳에서 DOM 렌더링, 사용자 이벤트 처리를 모두 담당한다. 따라서, 만약 무거운 연산이 메인 스레드에서 실행되면, 브라우저는 해당 작업이 끝날 때까지 화면을 그리지 못하고 클릭과 같은 사용자 입력에도 반응할 수 없게 된다. 이를 블로킹(Blocking)이라고 한다.</p>
<h2 id="web-worker">Web Worker</h2>
<p>
      <img src="https://velog.velcdn.com/images/one1_programmer/post/9086f821-1cd6-4f88-a9b1-2014340a8834/image.png" width="300">
    <img src="https://velog.velcdn.com/images/one1_programmer/post/1244d9fb-54d7-4e13-88e3-db33812d5e37/image.png" width="300">
</p>

<p>Web Worker는 메인 스레드와 별개로 백그라운드에서 스크립트를 실행할 수 있는 브라우저 API이다. 무거운 작업을 워커 스레드로 위임하면 메인 스레드는 UI 렌더링에만 집중할 수 있어 쾌적한 사용자 경험을 제공할 수 있다.</p>
<h2 id="offscreencanvas-api">OffscreenCanvas API</h2>
<p>OffscreenCanvas는 DOM과 분리되어 메모리 상에서만 존재하는 캔버스이다. 일반적인 <code>&lt;canvas&gt;</code> 요소와 달리 DOM에 직접 접근할 필요가 없기 때문에 Web Worker(백그라운드 스레드)에서도 사용할 수 있다는 강력한 장점이 지닌다. 다시 말해, 이 API는 반드시 Web Worker에서 사용될 필요는 없지만 함께 사용된다면 더욱 그 진가를 발휘할 수 있다고 할 수 있다.</p>
<h1 id="😎해결-과정">😎해결 과정</h1>
<h2 id="web-worker-구현">Web Worker 구현</h2>
<p>canvas의 <code>convertToBlob</code> 메서드에는 확장자, 이미지 품질을 지정할 수 있다. 따라서 기본적으로 webp 확장자로 변환하고, 품질을 20% 낮추었다.</p>
<pre><code class="language-ts">// src/workers/imageResizer.worker.ts
import { FIXED_RESIZED_IMAGE_WIDTH } from &#39;../_constants/constants&#39;;

interface WorkerMessage {
    id: string;
    file: File;
    compressFormat?: &#39;WEBP&#39; | &#39;JPEG&#39; | &#39;PNG&#39;;
    quality?: number;
}

self.onmessage = async (e: MessageEvent&lt;WorkerMessage&gt;) =&gt; {
    const { id, file, compressFormat = &#39;WEBP&#39;, quality = 0.8 } = e.data;

    try {
        // 1. 이미지를 비트맵으로 로드
        const bitmap = await createImageBitmap(file);
        const { width, height } = bitmap;

        // 2. 리사이징 목표 크기 계산 (이번 최적화 이전에 수행한 이미지 비율 유지하는 로직)
        const targetWidth = FIXED_RESIZED_IMAGE_WIDTH;
        const targetHeight = Math.round((height / width) * FIXED_RESIZED_IMAGE_WIDTH);

        // 3. OffscreenCanvas 생성
        const canvas = new OffscreenCanvas(targetWidth, targetHeight);
        const ctx = canvas.getContext(&#39;2d&#39;);

        if (!ctx) {
            throw new Error(&#39;OffscreenCanvas context creation failed&#39;);
        }

        // 4. 리사이징된 이미지 그리기
        ctx.drawImage(bitmap, 0, 0, targetWidth, targetHeight);

        // 5. Blob으로 변환 (압축)
        const blob = await canvas.convertToBlob({
            type: compressFormat === &#39;WEBP&#39; ? &#39;image/webp&#39; : &#39;image/jpeg&#39;,
            quality,
        });

        // 6. File 객체 재생성
        // File 생성자는 최신 브라우저 및 Web Worker 환경에서 지원됨
        const resizedFile = new File([blob], file.name, {
            type: compressFormat === &#39;WEBP&#39; ? &#39;image/webp&#39; : &#39;image/jpeg&#39;,
            lastModified: Date.now(),
        });

        bitmap.close();

        self.postMessage({ id, success: true, file: resizedFile });
    } catch (error) {
        console.error(&#39;Worker image resize error:&#39;, error);
        self.postMessage({ id, success: false, error });
    }
};
</code></pre>
<h2 id="커스텀-훅으로-분리">커스텀 훅으로 분리</h2>
<pre><code class="language-ts">// src/hooks/useImageResizer.ts
import { useEffect, useRef, useCallback } from &#39;react&#39;;
import { nanoid } from &#39;nanoid&#39;;

interface WorkerResponse {
    id: string;
    success: boolean;
    file?: File;
    error?: Error;
}

interface ResizeOptions {
    file: File;
    compressFormat?: &#39;WEBP&#39; | &#39;JPEG&#39; | &#39;PNG&#39;;
    quality?: number;
}

export default function useImageResizer() {
    const workerRef = useRef&lt;Worker | null&gt;(null);
    const pendingPromises = useRef&lt;Map&lt;string, { resolve: (file: File) =&gt; void; reject: (err: Error) =&gt; void }&gt;&gt;(
        new Map(),
    );

    useEffect(() =&gt; {
        // Web Worker 인스턴스 생성
        // cf) Vite는 import.meta.url을 사용하여 워커 파일을 모듈로 로드하는 것을 지원함. 워커의 경로는 상대경로로 작성해야 함.
        try {
            workerRef.current = new Worker(new URL(&#39;../_workers/imageResizer.worker.ts&#39;, import.meta.url), {
                type: &#39;module&#39;,
            });

            // 메시지 수신 핸들러
            workerRef.current.onmessage = (event: MessageEvent&lt;WorkerResponse&gt;) =&gt; {
                const { id, success, file, error } = event.data;
                const resolver = pendingPromises.current.get(id);

                if (resolver) {
                    if (success &amp;&amp; file) {
                        resolver.resolve(file);
                    } else {
                        resolver.reject(error || new Error(&#39;Image resizing failed in worker&#39;));
                    }
                    pendingPromises.current.delete(id);
                }
            };

            workerRef.current.onerror = error =&gt; {
                console.error(&#39;Worker error:&#39;, error);
            };
        } catch (error) {
            console.error(&#39;Failed to initialize image resizer worker:&#39;, error);
        }

        return () =&gt; {
            // 컴포넌트 언마운트 시 워커 종료
            workerRef.current?.terminate();
            workerRef.current = null;
            pendingPromises.current.clear();
        };
    }, []);

    const resizeImage = useCallback(
        ({ file, compressFormat = &#39;WEBP&#39;, quality = 0.8 }: ResizeOptions): Promise&lt;File&gt; =&gt; {
            return new Promise((resolve, reject) =&gt; {
                if (!workerRef.current) {
                    // 워커 초기화 실패 시 또는 지원하지 않는 환경 처리
                    reject(new Error(&#39;Image resizer worker is not ready&#39;));
                    return;
                }

                const id = nanoid();
                pendingPromises.current.set(id, { resolve, reject });

                workerRef.current.postMessage({
                    id,
                    file,
                    compressFormat,
                    quality,
                });
            });
        },
        [],
    );

    return { resizeImage };
}
</code></pre>
<h1 id="😎결과">😎결과</h1>
<p>퍼포먼스 탭에서 리사이징을 메인 스레드 대신 워커가 수행했음을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/7bd91ac3-44db-432f-bc69-6b772e3a96f7/image.png" alt=""></p>
<p>콘솔을 통해 리사이징에 소요된 시간을 측정했다. 물론 현재 상황에서는 소요 시간보다 TBT(Total Blocking Time)과 같은 지표가 더 의미있을지 모른다. 하지만 페이지 로드 과정이 아닌 특정 상황에서의 TBT를 측정하는 방법은 알지 못하여 우선 동일 조건 하에 리사이징 소요 시간을 측정해보았다.</p>
<ul>
<li>개선 전
<img src="https://velog.velcdn.com/images/one1_programmer/post/693acac0-b6bf-4889-ae35-91a6727ec6c8/image.png" alt=""></li>
<li>개선 후
<img src="https://velog.velcdn.com/images/one1_programmer/post/e79c5a80-d3af-4e7d-a347-83db2b246eee/image.png" alt=""></li>
</ul>
<p>무엇보다도 UI 상호작용이 끊김없이 부드럽게 개선된 점이 돋보였다.</p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/8deff3aa-882e-4dab-a892-1ec5e8d619ec/image.gif" alt=""></p>
<p>결론적으로, 이번 최적화를 통해 얻은 개선점은 다음과 같다.</p>
<ul>
<li>대용량 이미지에 대한 업로드 속도 대폭 단축</li>
<li>UI 렌더링 끊김 최소화</li>
</ul>
<h1 id="배운-점">배운 점</h1>
<p>이번 리팩토링을 통해 단일 스레드로 동작하는 JavaScript 환경에서 OffscreenCanvas와 Web Worker의 조합이 얼마나 강력한지 체감할 수 있었다. 단순히 라이브러리를 사용하는 것을 넘어, 브라우저의 동작 원리를 이해하고 적절한 기술을 도입함으로써 사용자가 쾌적하게 느낄 수 있는 서비스를 만들 수 있었다.</p>
<blockquote>
<p>참고자료
<a href="https://oliveyoung.tech/2024-07-04/image-upload-speed-optimization/">올리브영 테크블로그</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js] Server Component, 서버에서만 실행되는 컴포넌트]]></title>
            <link>https://velog.io/@one1_programmer/Next.js-%EC%84%9C%EB%B2%84-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%84%9C%EB%B2%84%EC%97%90%EC%84%9C%EB%A7%8C-%EC%8B%A4%ED%96%89%EB%90%98%EB%8A%94-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8</link>
            <guid>https://velog.io/@one1_programmer/Next.js-%EC%84%9C%EB%B2%84-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%84%9C%EB%B2%84%EC%97%90%EC%84%9C%EB%A7%8C-%EC%8B%A4%ED%96%89%EB%90%98%EB%8A%94-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8</guid>
            <pubDate>Wed, 24 Dec 2025 14:21:15 GMT</pubDate>
            <description><![CDATA[<p><strong>서버 컴포넌트(Server Component)</strong>는 기존의 (클라이언트) 컴포넌트 개념을 서버로 확장한 컴포넌트 개념으로, 서버측에서만 실행되며 브라우저에서는 실행되지 않는다.</p>
<p>리액트에는 상호작용이 있는 컴포넌트와 상호작용이 없는 컴포넌트가 존재하며, 상호작용이 있는 컴포넌트는 hydration 과정이 필요하다. 서버에서는 hydration 이전에 JS 번들을 클라이언트(브라우저)에 보낸다. 이때, Pages Router에서는 상호작용의 여부와 관계없이 모든 컴포넌트들을 번들에 묶어 보냈었다. 하지만 이는 hydration이 불필요한 컴포넌트들까지 포함되어 번들 크기가 커져 hydration이 늦어지고 상호작용을 시작하는 TTI(Time to Interaction)도 늦어지는 비효율이 발생했다. 따라서 상호작용이 없는 컴포넌트는 JS 번들에 포함하지 않도록 할 필요가 생겼고, 서버 컴포넌트와 클라이언트 컴포넌트를 구별하게 된다.</p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/5d7c6395-e0b5-42e6-9ed0-a2f62325ef15/image.png" width="500">


<p>서버 컴포넌트는 서버측에서 사전 렌더링 시 단 한 번 실행되고, 클라이언트 컴포넌트는 사전 렌더링, hydration 총 두 번 실행된다. 따라서 페이지의 대부분을 서버 컴포넌트로 구성하고, 클라이언트 컴포넌트는 꼭 필요한 경우에만 사용할 것이 권장된다.</p>
<h1 id="🤔개념">🤔개념</h1>
<h2 id="💧클라이언트-컴포넌트">💧클라이언트 컴포넌트</h2>
<p>SSR(pre render) + CSR
Next.js에서 SSR은 모든 컴포넌트에서 발생한다. 이때, 컴포넌트를 클라이언트 측에서 hydrate 되게 만드려면 파일 최상단(import보다 위)에 React <code>&quot;use client&quot;</code> 지시어를 추가해야 한다.</p>
<p>결국, <code>use client</code> 는 서버와 클라이언트 컴포넌트 모듈 간의 경계를 선언하는 데 사용된다. 즉, 파일에 <code>&quot;use client&quot;</code>를 정의하면 하위 컴포넌트를 포함하여 해당 파일로 가져온 다른 모든 모듈을 클라이언트 번들의 일부로 간주하게 된다.</p>
<h2 id="⚠️주의사항">⚠️주의사항</h2>
<ul>
<li><p><strong>브라우저에서 실행될 코드가 포함되면 안된다.</strong></p>
<ul>
<li>브라우저 환경에 의존적인 <code>useState</code>, <code>useEffect</code> 등의 리액트 hooks</li>
<li>이벤트 핸들러</li>
<li>브라우저에서 실행되는 기능을 담고 있는 라이브러리</li>
</ul>
</li>
<li><p><strong>클라이언트 컴포넌트라고 클라이언트에서만 실행되는 것이 아니다:</strong> 사전 렌더링을 위해 서버에서 1번, hydration을 위해 브라우저에서 1번 총 2번 실행된다.</p>
</li>
<li><p><strong>클라이언트 컴포넌트에서 서버 컴포넌트를 import 할 수 없다:</strong> 만약 import 하더라도 Nextjs는 해당 서버 컴포넌트를 클라이언트 컴포넌트로 변환한다. 즉, 클라이언트 컴포넌트 하위의 컴포넌트들은 모두 클라이언트 컴포넌트 취급하는 것이다. 만약 클라이언트 컴포넌트 하위에 서버 컴포넌트를 위치시키려면 <code>children</code> prop을 활용하여 Nextjs가 서버 컴포넌트의 결과물만 클라이언트 컴포넌트 내부에 끼워넣게 할 수 있다.</p>
</li>
<li><p><strong>서버 컴포넌트에서 클라이언트 컴포넌트에게 직렬화되지 않는 props(함수)는 전달 불가하다.</strong></p>
<blockquote>
<p><strong>직렬화(Serialization)</strong>
객체, 배열, 클래스 등의 복잡한 데이터 구조의 데이터를 네트워크 상으로 전송하기 위해 문자열, 바이트의 단순한 형태로 변환하는 것이다. 자바스크립트의 함수는 직렬화될 수 없는 특징을 지닌다.</p>
</blockquote>
</li>
</ul>
<h2 id="동작-과정">동작 과정</h2>
  <p><img src="https://velog.velcdn.com/images/one1_programmer/post/8dd741c8-2016-4b58-b857-c98490f6ccaf/image.png" width="550">

<h3 id="서버-측-동작"><strong>서버 측 동작</strong></h3>
<p>Rendering work: Route segment와 Suspense boundary를 기준으로 chunk 단위로 split한다. 페이지를 구성하는 모든 컴포넌트(서버+클라이언트 컴포넌트)들을 실행해서 완성된 HTML 페이지를 생성한다. </p>
<p>자세히는 서버 컴포넌들만 따로 먼저 실행하는 과정이 존재하고, 그 결과물로 RSC Payload가 생성된다. 이 RSC Payload와 클라이언트 컴포넌트용 JavaScript Instructions(자바스크립트 명령어 뭉치)의 일부(ex. useState의 초깃값 등)를 조합하여 HTML을 만든다.</p>
<h3 id="rscreact-server-component-payload"><strong>RSC(React Server Component) Payload</strong></h3>
<p>React 서버 컴포넌트의 순수한 데이터(결과물)로서, 서버 컴포넌트를 직렬화한 결과를 나타낸다.</p>
<ul>
<li><strong>구성요소</strong><ul>
<li>서버 컴포넌트의 렌더링된 결과물</li>
<li>placeholder(연결된(import한) 클라이언트 컴포넌트의 렌더 위치와 클라이언트 컴포넌트의 JavaScript 파일 위치)</li>
<li>서버 컴포넌트가 클라이언트 컴포넌트에게 전달하는 props
Next.js는 리액트가 렌더한 RSC Payload와 함께 자바스크립트 명령어 뭉치를 활용하여 HTML을 렌더한다. (서버 내 동작)</li>
</ul>
</li>
<li><strong>클라이언트 측 동작</strong><ol>
<li>HTML을 즉시 보여준다. (routes의 프리뷰)</li>
<li>클라이언트 컴포넌트가 들어갈 빈자리(placeholder)를 메꾸고, 서버 컴포넌트와 조합하는 reconcile(재조정) 단계를 거쳐 리액트 Virtual DOM 트리를 구성한다. 이후 실제 DOM에도 반영한다.</li>
<li>placeholder를 채운 컴포넌트들에 대해 interaction이 가능하도록 JavaScript Instructions(ex. useState의 setState)를 활용하여 hydrate한다.</li>
</ol>
</li>
</ul>
<blockquote>
<p><strong>도움되는 자료</strong>
<a href="https://www.plasmic.app/blog/how-react-server-components-work">How React server components work: an in-depth guide</a></p>
</blockquote>
<h2 id="prefetching"><strong>Prefetching</strong></h2>
<p>App Router에서는 페이지가 Static과 Dynamic으로 나뉜다.</p>
  <p><img src="https://velog.velcdn.com/images/one1_programmer/post/0b07e379-0926-4667-91fb-6b752aaaef32/image.png" width="500">

<ul>
<li><strong>Static:</strong> 추가적인 데이터 업데이트가 불필요하므로, RSC Payload와 JS번들 모두를 prefetching한다.</li>
<li><strong>Dynamic:</strong> url 파라미터나 쿼리스트링을 포함한 컴포넌트의 경우 RSC Payload만 prefetching한다. JS번들은 실제 페이지 이동이 일어난 경우에만 불러온다.</li>
</ul>
<h1 id="😎실습">😎실습</h1>
<h2 id="서버-컴포넌트의-rsc-payload-확인">서버 컴포넌트의 RSC Payload 확인</h2>
<h3 id="서버-컴포넌트">서버 컴포넌트</h3>
<p>먼저 서버 컴포넌트만 존재하는 경우를 살펴보자. 이 경우에는  </p>
<pre><code class="language-tsx">export default function RootLayout({
  children,
}: Readonly&lt;{
  children: React.ReactNode;
}&gt;) {
  return (
    &lt;html lang=&quot;en&quot;&gt;
      &lt;body className={`${geistSans.variable} ${geistMono.variable}`}&gt;
        &lt;header&gt;
          &lt;Link href={&quot;/&quot;}&gt;ONEBITE CINEMA&lt;/Link&gt;
          &amp;nbsp;
          &lt;Link href={&quot;/search&quot;}&gt;SEARCH&lt;/Link&gt;
          &amp;nbsp;
          &lt;Link href={&quot;/movie/1&quot;}&gt;MOVIE 1&lt;/Link&gt;
        &lt;/header&gt;
        {children}
      &lt;/body&gt;
    &lt;/html&gt;
  );
}</code></pre>
<pre><code class="language-tsx">export default async function Page({
  searchParams,
}: {
  searchParams: Promise&lt;{ q: string }&gt;;
}) {
  const { q } = await searchParams;

  return &lt;div&gt;Search : {q}&lt;/div&gt;;
}</code></pre>
  <p><img src="https://velog.velcdn.com/images/one1_programmer/post/6f099e71-f6a0-4ad1-b53e-c4da4afe210c/image.png" width="800">

<h3 id="서버클라이언트-컴포넌트">서버+클라이언트 컴포넌트</h3>
<p>만약, 서버 컴포넌트 하위에 클라이언트 컴포넌트가 존재한다면 네트워크 탭의 응답이 어떻게 달라질까? </p>
<pre><code class="language-tsx">&quot;use client&quot;;

export default function ClientComponent() {
  return &lt;div&gt;ClientComponent&lt;/div&gt;;
}</code></pre>
<pre><code class="language-tsx">// movie/[id]/page.tsx
import ClientComponent from &quot;@/components/client-component&quot;;

export default async function Page({
  params,
}: {
  params: Promise&lt;{ id: string }&gt;;
}) {
  const { id } = await params;

  return (
    &lt;&gt;
      &lt;div&gt;Movie : {id}&lt;/div&gt;
      &lt;ClientComponent /&gt;
    &lt;/&gt;
  );
}</code></pre>
  <p><img src="https://velog.velcdn.com/images/one1_programmer/post/43778a27-b089-455e-8250-b6e9cb9d1715/image.png" width="800">

<blockquote>
<p><strong>참고자료</strong>
한 입 크기로 잘라먹는 Next.js (이정환)</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js] 기본개념]]></title>
            <link>https://velog.io/@one1_programmer/Next.js-%EA%B8%B0%EB%B3%B8%EA%B0%9C%EB%85%90</link>
            <guid>https://velog.io/@one1_programmer/Next.js-%EA%B8%B0%EB%B3%B8%EA%B0%9C%EB%85%90</guid>
            <pubDate>Mon, 22 Dec 2025 04:39:27 GMT</pubDate>
            <description><![CDATA[<p>지난 포스팅에서 Next.js의 등장배경에 대해 간략히 알아보았다. 이번 포스팅에서는 Next.js의 Pages Router, App Router를 배우기 앞서 기본적으로 알아야 할 개념들에 대해 짚고 넘어가고자 한다.</p>
<h1 id="🤔개념">🤔개념</h1>
<h2 id="routing">Routing</h2>
<p>CSR에서는 기본적으로 경로(URL)가 하나, SSR에서는 여러 페이지가 존재하며 각각의 경로가 존재한다. 따라서 SSR에서 경로에 따라 페이지를 선택하는 방법이 필요했고, 이때 경로에 페이지를 대응시키는 프로세스가 Routing이다.</p>
<ul>
<li><p><strong>명시적 라우팅:</strong> CSR의 React Router</p>
<pre><code class="language-jsx">  &lt;Route path=&#39;/&#39; components = {&lt;Home /&gt;}/&gt;</code></pre>
</li>
<li><p><strong>코드 기반 라우팅:</strong> nodejs 기반의 express의 라우팅</p>
<pre><code class="language-jsx">  app.get(&#39;/&#39;, (req, res) =&gt; { res.send(&#39;Hello&#39;); };</code></pre>
</li>
<li><p><strong>데코레이터 기반 라우팅:</strong> NestJS</p>
<pre><code class="language-jsx">  @Controller(&#39;users&#39;)
  export class Users Controller {
      @Get()
      findAll(): string {
          return &#39;Hello&#39;;
      }
  }</code></pre>
</li>
</ul>
<p>Next.js는 파일 기반 라우팅을 선택했다. 말 그대로 파일명에 따라 그 기능을 고정하고 경로를 지정한다. 예시로 <code>about/age.tsx</code> 는 ‘/about’ 경로에 존재하는 페이지이다. 이렇게 Next.js는 개발자들이 자주 사용하는 구조를 쉽게 사용할 수 있게 추상화된 구조를 파일 컨벤션으로써 제공한다.</p>
<p>파일 구조 기반을 채택함으로써 얻는 장점은 다음과 같다. </p>
<p>첫째, 직관적이다.</p>
<p>둘째, 경로가 모두 나누어져 있어 특정 경로에서 필요한 JavaScript 번들만 받아오면 되고, 응답 시간이 줄어든다. React로 개발할 때를 생각해보면, 별도의 router 파일을 생성하고 해당 파일 내에서 <code>React.lazy</code> 로 지연 로딩을 수행했었다. 하지만 Next.js는 서로 다른 경로에 따라 자동적으로 코드 스플리팅을 수행해준다.</p>
<blockquote>
<p><strong>Navigation</strong>
페이지 간 이동 과정 자체를 의미한다.</p>
</blockquote>
<h2 id="사전-렌더링">사전 렌더링</h2>
<p>브라우저의 요청에 대하여 사전에 렌더링이 완료된 HTML을 응답하는 렌더링 방식이다. 이 방식을 사용하면 늦은 FCP, 낮은 SEO와 같은 CSR의 단점을 효율적으로 해결할 수 있다. 이를 통해, 빠른 FCP로 React의 단점을 해소하고 hydration 이후 빠른 페이지 이동으로 React의 장점을 살린다.</p>
<h3 id="ssrserver-side-rendering">SSR(Server-Side Rendering)</h3>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/10e6da33-6de6-44d4-a450-626565337ada/image.png" width="550">

<blockquote>
<p>서버에서 일어나는 &#39;렌더링&#39;은 JavaScript 코드(React 컴포넌트)를 HTML로 변환하는 과정을 의미하고, 브라우저에서 일어나는 &#39;렌더링&#39;은 HTML 코드를 브라우저가 화면에 그리는 작업을 의미한다.</p>
</blockquote>
<p>브라우저가 화면에 HTML을 렌더링한 이후에도 여전히 사용자는 화면과 상호작용할 수 없다. 왜냐하면 JS 코드가 아닌 HTML만 전달받은 상태이기 때문이다. 따라서 JS 번들을 통해서 HTML을 Hydration 시켜주어야 한다.</p>
<h3 id="hydration">Hydration</h3>
<blockquote>
<p>Hydration: 수화. 어떤 물질이 물과 회합하거나 결합하여 수화물이 되는 현상. [출처: 네이버 어학사전]</p>
</blockquote>
<p>서버사이드 렌더링(SSR)을 통해 만들어진 interactive 하지 않은 HTML을 클라이언트 측 JavaScript를 사용하여 interactive한 리액트 컴포넌트로 변환하는 과정(서버 환경에서 이미 렌더링된 HTML에 React를 붙이는 것)이다. 이때, hydration을 위한 interactive한 JavaScript만 다운받으면 되므로, 다운받아야 할 JavaScript 코드 양이 줄어드는 장점이 있다. 다시 말해, 현재 페이지에 필요한 JS 번들만 전달되므로 용량 경량화로 인해 hydration 시간이 단축된다. </p>
<h2 id="data-fetching">Data Fetching</h2>
<p>기존 React에서의 기본적인 데이터 페칭 방식은 아래와 같았다. 하지만 이런 방식은 초기 접속 요청부터 데이터 로딩까지 오랜 시간이 걸린다는 단점이 있다. 왜냐하면 컴포넌트 마운트된 이후에 데이터 페칭 함수가 호출되기 때문이다. 즉, 데이터 요청 시점 자체가 늦다.</p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/1702f631-694f-4fe5-acd8-7d5a92bc4a8c/image.png" width="450">


<p>이러한 문제를 Next.js는 사전 렌더링 시점에 데이터 페칭을 수행함으로써 해결한다. 사전 렌더링에서 응답받은 데이터는 HTML에 삽입되며, 사용자에게 완성된 HTML로써 화면을 보여줄 수 있다.</p>
  <p><img src="https://velog.velcdn.com/images/one1_programmer/post/1c940f05-057c-4c3f-bb59-6349c475521d/image.png" width="600">


<table>
<thead>
<tr>
<th></th>
<th>React</th>
<th>Next.js</th>
</tr>
</thead>
<tbody><tr>
<td>시점</td>
<td>컴포넌트 마운트 이후에 발생</td>
<td>사전 렌더링 중 발생 (컴포넌트 마운트 이후에도 발생 가능)</td>
</tr>
<tr>
<td>특징</td>
<td>데이터 요청 시점이 상대적으로 늦음</td>
<td>데이터 요청 시점이 매우 빨라짐</td>
</tr>
</tbody></table>
<p>하지만 데이터 페칭 자체가 오래걸리면 이런 방식은 오히려 초기 로딩을 늦출 수도 있다. 따라서 Next.js는 빌드 타입에 데이터 페칭을 미리 수행하는 SSG 등의 방법을 제공하여 이러한 우려를 해소한다.</p>
<table>
<thead>
<tr>
<th>페이지 라우터</th>
<th>앱 라우터</th>
</tr>
</thead>
<tbody><tr>
<td>모든 컴포넌트가 클라이언트 컴포넌트</td>
<td>클라이언트 컴포넌트 + 서버 컴포넌트 (비동기 함수로서 서버에서만 동작)</td>
</tr>
<tr>
<td>getServerSideProps(SSR), getStaticProps(SSG), Dynamic SSG</td>
<td>fetch</td>
</tr>
</tbody></table>
<h2 id="prefetching">Prefetching</h2>
<p>필요한 JS 번들을 그때마다 가져오는(Code Splitting) 덕분에 Hydration 시간이 단축되는 장점이 있다고 했다. 하지만 이후에 사용자가 페이지 이동을 요청했을 때, 그에 맞는 JS 번들을 가져와야 하므로 추가적인 시간이 필요하다는 단점이 존재한다. </p>
<p>따라서 이러한 문제를 방지하기 위한 방법이 필요했고, 필요한 페이지를 미리 가져오는 Pre(미리)-Fetching(불러오기) 기능이 등장했다. 이 기능은 사용자가 특정 링크를 클릭하기도 전에 백그라운드 상에서 미리 해당 라우트를 로드한다. 덕분에 렌더링이 최적화되고 사용자는 빠른 페이지 이동 경험을 제공받을 수 있다.</p>
<blockquote>
<p>이 기능은 운영 환경에서만 적용됨에 유의한다.</p>
</blockquote>
<p>기본적으로 <code>&lt;Link&gt;</code> 컴포넌트에서만 동작한다. 따라서 <code>useRouter()</code> 를 통해 생성한 라우터 객체로 페이지를 이동하는 경우, 즉 프로그래밍 방식으로 라우트를 변경을 사용한다면 별도의 설정이 필요하다.</p>
<p><code>&lt;Link&gt;</code> 의 경우, 뷰포트에서 보일 때 자동으로 라우트를 미리 가져온다.</p>
<ul>
<li><p><code>router.prefetch(&#39;경로&#39;)</code> : <code>useEffect</code> 로 마운트 시 prefetching하도록 설정한다.</p>
<pre><code class="language-jsx">  import { useRouter } from &quot;next/router&quot;;

  const router = useRouter();
  const onClickButton = () =&gt; {
    router.push(&quot;/test&quot;);
  };

  useEffect(() =&gt; {
    router.prefetch(&quot;/test&quot;);
  }, []);</code></pre>
<blockquote>
<p><code>useRouter</code> 훅은 App Router를 사용할 때 <code>next/router</code>가 아닌 <code>next/navigation</code>에서 가져와야 한다.</p>
</blockquote>
</li>
</ul>
<p><code>&lt;Link&gt;</code> 컴포넌트의 prefetching 기본 동작을 취소하려면 <code>prefetch</code> 속성을 false로 설정하면 된다. 단, 이때에도 사용자의 마우스 호버 시에 prefetching 동작이 수행된다.</p>
<h2 id="api-routes">API Routes</h2>
<p>public API를 설계하는 기능을 제공한다. 프런트엔드 프로젝트 내에서 서버리스 함수처럼 백엔드 API 엔드포인트를 직접 만들 수 있도록 해준다. Pages router의 경우, <code>pages/api</code> 경로에 존재하는 파일들은 페이지 기능 대신 API 엔드포인트로써 동작한다. 서버 사이드의 번들이므로 클라이언트 사이드의 번들 용량을 늘리지는 않는다. </p>
<pre><code class="language-jsx">// Pages Router
import type { NextApiRequest, NextApiResponse } from &#39;next&#39;

type ResponseData = {
  message: string
}

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse&lt;ResponseData&gt;
) {
  res.status(200).json({ message: &#39;Hello from Next.js!&#39; })
}</code></pre>
<pre><code class="language-jsx">// App router
export async function GET(request: Request) {}</code></pre>
<p>App router의 경우에는 데이터 페칭 시 서버 컴포넌트를 활용하는 방안도 존재한다.</p>
<h2 id="layouts">Layouts</h2>
<p>다수의 라우트 간 UI 공유를 위해 사용되며, <code>layout.js</code> 파일로부터 리액트 컴포넌트를 export 함으로써 정의할 수 있다. 이때, 리액트 컴포넌트는 <code>children</code> 을 prop으로 받아야 한다. NextJS는 layout 컴포넌트를 렌더링한 후 URL을 확인하여 무엇을 렌더링해야 할 지 결정한다.</p>
<ul>
<li>특징<ul>
<li>탐색 시 레이아웃은 상태를 유지하고, 상호작용을 유지하며, 다시 렌더링되지 않는다.</li>
<li>글로벌 레이아웃은 웹사이트 전체에 공통적으로 적용되는 반면, 페이지별 레이아웃은 특정 페이지나 페이지 그룹에만 적용된다.</li>
</ul>
</li>
</ul>
<pre><code class="language-tsx">export default function DashboardLayout({
  children, // will be a page or nested layout
}: {
  children: React.ReactNode
}) {
  return (
    &lt;section&gt;
      {/* Include shared UI here e.g. a header or sidebar */}
      &lt;nav&gt;&lt;/nav&gt;

      {children}
    &lt;/section&gt;
  )
}
</code></pre>
<h3 id="root-layout">Root Layout</h3>
<p><code>app</code> 폴더 내 최상단에 위치해야하며, 모든 라우트에 적용되는 레이아웃이다. 이는 필수이며 <code>html</code> 및 <code>body</code> 태그를 포함하여야 한다. (서버로부터 반환되는 초기 HTML을 수정하게끔 한다.)</p>
<pre><code class="language-tsx">export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    &lt;html lang=&quot;en&quot;&gt;
      &lt;body&gt;
        {/* Layout UI */}
        &lt;main&gt;{children}&lt;/main&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  )
}</code></pre>
<h3 id="nesting-layouts">Nesting Layouts</h3>
<p>중첩 레이아웃을 수행할 수 있다. 렌더링할 페이지와 가장 가까운 레이아웃을 찾은 후, 다음 가까운 레이아웃을 찾는 과정을 반복한다.</p>
<pre><code class="language-tsx">export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return &lt;section&gt;{children}&lt;/section&gt;
}</code></pre>
<h2 id="etc">etc</h2>
<h3 id="다크모드">다크모드</h3>
<p>next.js에서 다크모드를 손쉽게 적용하게 해주는 라이브러리인 <code>next-themes</code> 을 활용할 수 있다.</p>
<pre><code class="language-bash">npm install next-themes</code></pre>
<ul>
<li><p><strong>사용방법</strong></p>
<ul>
<li><p><strong>`layout.tsx</strong>ThemeProvider<code>는 클라이언트 컴포넌트이므로, 만약</code>layout<code>이 클라이언트 컴포넌트가 아니라면(</code>&quot;use client&quot;<code>를 사용하지 않은 경우)</code>ThemeProvider<code>를 별도의 클라이언트 컴포넌트로 분리하여</code>layout` 에 제공해야 함</p>
<pre><code class="language-jsx">  // app/layout.jsx
  import { ThemeProvider } from &#39;next-themes&#39;

  export default function Layout({ children }) {
    return (
      &lt;html suppressHydrationWarning&gt;
        &lt;head /&gt;
        &lt;body&gt;
          &lt;ThemeProvider&gt;{children}&lt;/ThemeProvider&gt;
        &lt;/body&gt;
      &lt;/html&gt;
    )
  }
</code></pre>
</li>
<li><p><strong><code>ThemeProvider</code> 분리</strong></p>
<pre><code class="language-jsx">  &quot;use client&quot;;

  import { ThemeProvider } from &quot;next-themes&quot;;

  export default function ThemeChangeProvider({
    children,
  }: {
    children: React.ReactNode;
  }) {
    return (
      &lt;ThemeProvider enableSystem={true} attribute=&quot;class&quot;&gt;
        {children}
      &lt;/ThemeProvider&gt;
    );
  }
</code></pre>
</li>
<li><p><strong>`tailwind.config.ts</strong>darkMode` 를 class로 지정</p>
<pre><code class="language-jsx">  const config: Config = {
    mode: &quot;jit&quot;,
    darkMode: &quot;class&quot;,
    ...
  };</code></pre>
</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[네트워크] ARP]]></title>
            <link>https://velog.io/@one1_programmer/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-ARP-IP</link>
            <guid>https://velog.io/@one1_programmer/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-ARP-IP</guid>
            <pubDate>Thu, 18 Dec 2025 14:39:14 GMT</pubDate>
            <description><![CDATA[<p><strong>ARP(Address Resolution Protocol, ARP):</strong> 네트워크 상에서 논리적 IP 주소(32비트)를 물리적 네트워크 주소인 MAC 주소(48비트)로 대응(bind)시키기 위해 사용되는 주소 변환 프로토콜이다.</p>
<blockquote>
<p><strong>RARP</strong>
역주소 변환 프로토콜로 MAC주소를 IP주소로 변환한다.</p>
</blockquote>
<ul>
<li><strong>ARP 헤더</strong></li>
</ul>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/746d6589-9a69-4020-a6a2-b7afc74f5be6/image.png" width="350">
[출처: geeksforgeeks]


<table>
<thead>
<tr>
<th><strong>구성</strong></th>
<th><strong>설명</strong></th>
</tr>
</thead>
<tbody><tr>
<td>Hardware Type</td>
<td>대부분 이더넷이므로, 이더넷의 타입 번호인 0x01이 들어간다.</td>
</tr>
<tr>
<td>Protocol Type</td>
<td>대부분 IPv4이므로, IP의 타입 번호인 0x0800이 들어간다.</td>
</tr>
<tr>
<td>Hardware Length</td>
<td>MAC 주소의 길이(6byte)</td>
</tr>
<tr>
<td>Protocol Length</td>
<td>IPv4인 경우 4byte</td>
</tr>
<tr>
<td>Opcode(Operation Code)</td>
<td>요청=1, 응답=2</td>
</tr>
</tbody></table>
<p>동일한 이더넷에 속한 호스트 A, B에 대해서 ARP 과정을 살펴보자.</p>
<ul>
<li><p><strong>과정</strong></p>
<pre><code>  1. A는 `/etc/hosts` 파일 검색을 통해 B의 IP 주소를 찾는다.
  2. B의 주소에서 넷마스크를 사용해 네트워크 주소를 알아내어 네트워크 대역이 로컬 영역인지 아닌지를 확인한다.
  3. 외부 영역이면 게이트웨이의 MAC을, 로컬 영역이면 ARP 캐시 테이블에서 B의 MAC을 찾는다.
  4. 3에서 B의 MAC이 없다면 ARP request broadcast packet을 보낸다.
  5. 모든 host는 해당 패킷을 수신하여 request IP와 자신의 IP를 비교한다.
  6. 5의 비교 결과 일치하는 host만이 자신의 MAC 정보를 실어 unicast respond한다.
  7. A는 자신의 ARP 캐시 테이블을 업데이트한다.</code></pre><blockquote>
<p><strong>ARP 캐시 테이블</strong>
통신했던 host들의 주소가 남으며, 사용자가 수동으로 등록을 하지 않는 한 일정 시간이 지나면 삭제된다. <code>arp -a</code> 명령어로 확인할 수 있다.</p>
<img src="https://velog.velcdn.com/images/one1_programmer/post/e856d599-7692-4c35-8d17-83bbec0f190c/image.png" width="450">
</blockquote>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[네트워크] IP, 호스트가 연결된 네트워크 끝 단의 주소 ]]></title>
            <link>https://velog.io/@one1_programmer/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-IP-%ED%98%B8%EC%8A%A4%ED%8A%B8%EA%B0%80-%EC%97%B0%EA%B2%B0%EB%90%9C-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EB%81%9D-%EB%8B%A8%EC%9D%98-%EC%A3%BC%EC%86%8C</link>
            <guid>https://velog.io/@one1_programmer/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-IP-%ED%98%B8%EC%8A%A4%ED%8A%B8%EA%B0%80-%EC%97%B0%EA%B2%B0%EB%90%9C-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EB%81%9D-%EB%8B%A8%EC%9D%98-%EC%A3%BC%EC%86%8C</guid>
            <pubDate>Thu, 18 Dec 2025 14:25:21 GMT</pubDate>
            <description><![CDATA[<p><strong>IP(Internet Protocol)</strong>는 송신 호스트와 수신 호스트가 패킷 교환 네트워크에서 정보를 주고받는 데 사용하는 정보 위주의 규약이다. OSI 네트워크 계층에서 호스트의 주소지정과 패킷 분할 및 조립 기능을 담당한다.</p>
<ul>
<li><strong>특징</strong><ul>
<li><strong>비신뢰성(unreliability):</strong> 흐름에 관여하지 않기 때문에 보낸 정보가 제대로 갔는지 보장하지 않는다. 즉, 전송 과정에서의 패킷의 손상/유실/중복 등의 문제에 관여하지 않는 것이다. 만약 패킷의 전송과 정확한 순서를 보장하기 위해서는 TCP 프로토콜과 같은 상위 프로토콜을 이용해야 한다.</li>
<li><strong>비연결성(connectionlessness):</strong> 데이터 전송 전에 사전 연결 설정을 하지 않고, 각 패킷을 독립적으로 전송한다.</li>
</ul>
</li>
</ul>
<h1 id="🤔개념">🤔개념</h1>
<h2 id="ip-주소">IP 주소</h2>
<p>컴퓨터가 연결된 네트워크 끝단의 주소로서, 기기가 인터넷에 접속한 곳의 네트워크상 위치이다.</p>
<ul>
<li><strong>역할:</strong> IP 패킷의 헤더 안에 기록되어 있는 도착지 IP 주소와 출발지 IP 주소에 따라 패킷을 출발지 호스트로부터 도착지 호스트까지 전달한다.</li>
<li><strong>IP 엔티티:</strong> 라우터와 호스트 컴퓨터에서 동작</li>
<li><strong>공인/사설 IP:</strong> 실제 인터넷 상에서는 공인 IP로만 통신하며, 외부 네트워크 대역에서는 사설 IP 대역은 보이지 않는다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/23fa78dd-266f-4d36-a089-6c2cd28d3141/image.png" width="400"/>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/09e78ecb-9263-4b55-916c-3f881e0d015f/image.png" width="400"/>
[출처: 유튜브 얄팍한 코딩사전]


<ul>
<li><strong>구성</strong><ul>
<li><strong>네트워크 주소:</strong> 네트워크 고유 식별자를 가리키는 일련의 숫자</li>
<li><strong>호스트 주소:</strong> 네트워크 상의 호스트 또는 개별 디바이스 식별자를 나타내는 일련의 숫자</li>
</ul>
</li>
</ul>
<h3 id="사설-ip">사설 IP</h3>
<p>공인 IP 주소가 공개형이라면, 사설 IP 주소는 폐쇄형이다. 즉, 사설 IP주소는 외부에 공개되지 않아 외부에서 검색 및 접근이 불가능하다. </p>
<p>사설 IP 주소는 인터넷 유무선 공유기를 사용할 때 흔히 접하게 되는데, 인터넷 유무선 공유기의 IP주소를 공유하여 여러 대의 컴퓨터가 인터넷에 접속되게 하려면 사설 IP 주소가 필요하기 때문이다. 이후 사설 네트워크에 속한 여러 개의 호스트는 하나의 공인 IP 주소를 사용해서 인터넷에 접속하게 된다.</p>
<ul>
<li>주소 대역<ul>
<li>192.168.xxx.xxx</li>
<li>172.10.xxx.xxx</li>
<li>10.xxx.xxx.xxx</li>
</ul>
</li>
</ul>
<h2 id="ipv4">IPv4</h2>
<p>패킷 교환 네트워크 상에서 데이터를 교환하기 위한 프로토콜로서, 32비트의 주소길이를 가진다. 8비트씩 끊어서 표시되어 3개의 마침표로 구분된다. 인터넷 프로토콜의 4번째 버전이며 전 세계적으로 사용된 첫 번째 인터넷 프로토콜로 과거에 인터넷에서 사용되는 유일한 프로토콜이였으나, IP 고갈 문제로 오늘날에는 IPv6와 같이 사용되고 있다.</p>
<ul>
<li><strong>특징</strong><ul>
<li>데이터가 정확하게 전달될 것을 보장하지 않는다. 즉, 중복된 패킷을 전달하거나 패킷의 순서를 잘못 전달할 가능성도 있다.</li>
</ul>
</li>
</ul>
<h3 id="클래스-기반-주소"><strong>클래스 기반 주소</strong></h3>
<table>
<thead>
<tr>
<th>클래스</th>
<th>범위 (이진)</th>
<th>범위 (십진)</th>
<th>기본 서브넷 마스크</th>
<th>예</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>A</td>
<td>0xxxxxxx …</td>
<td>1.0.0.1~126.255.255.254</td>
<td>255.0.0.0</td>
<td>61.211.123.22</td>
<td>0~127 중 0은 로컬, 1은 예약</td>
</tr>
<tr>
<td>B</td>
<td>10xxxxxx …</td>
<td>128.0.0.1~191.255.255.254</td>
<td>255.255.0.0</td>
<td>181.123.211.33</td>
<td></td>
</tr>
<tr>
<td>C</td>
<td>110xxxxx …</td>
<td>192.0.0.1~223.255.255.254</td>
<td>255.255.255.0</td>
<td>221.23.222.222</td>
<td></td>
</tr>
<tr>
<td>D</td>
<td>1110xxxx …</td>
<td>224.0.0.0~239.255.255.255</td>
<td></td>
<td></td>
<td>Multicast 주소</td>
</tr>
<tr>
<td>E</td>
<td></td>
<td>240.0.0.0~254.255.255.254</td>
<td></td>
<td></td>
<td>예약 주소</td>
</tr>
</tbody></table>
<p>⇒ 낭비가 심하다.</p>
<ul>
<li><strong>A클래스(네트워크 접두사 비트가 8개, 0 고정):</strong> 최고위의 클래스로서, 첫 번째 단위는 1~126(접두사 비트가 8비트)의 범위의 IP주소(네트워크 주소)를 가지며 나머지 단위의 세 숫자(호스트 주소)는 A 클래스가 자유롭게 네트워크 사용자에게 부여가 가능한 IP이다.</li>
<li><strong>B클래스(네트워크 접두사 비트가 16개, 10 고정):</strong> 두 번째로 높은 단위의 클래스로서, IP구성에서 첫 번째 단위의 세 숫자는 128~191 가운데 하나를 가지며, 두 번째 단위의 세 숫자는 B 클래스가 접속할 수 있는 네트워크를 지시한다.</li>
<li><strong>C클래스(네트워크 접두사 비트가 24개, 110 고정):</strong> 최하위의 클래스로서, IP구성에서 첫 번째 단위의 세 숫자는 192~223 가운데 하나를 가지며, 두 번째와 세 번째 단위의 세 숫자는 C클래스가 접속할 수 있는 네트워크를 지시한다. C클래스가 자유로이 부여할 수 있는 IP는 마지막 네 번째 단위의 254개이다.</li>
</ul>
<h3 id="헤더"><strong>헤더</strong></h3>
  <p><img src="https://velog.velcdn.com/images/one1_programmer/post/697f5092-bc40-4ce9-8b89-e328f0f259b4/image.png" width="400"/>


<ul>
<li><strong>첫째줄</strong><ul>
<li><strong>버전:</strong> IP 버전 번호</li>
<li><strong>IHL(Internet Header Length):</strong> IP 헤더 길이 표시</li>
<li><strong>TOS(Type of Service):</strong> DSCP + ECN ⇒ IP 프로토콜 장치에 대한 입력</li>
<li><strong>전체 길이:</strong> 프로토콜 헤더를 포함한 패킷 길이</li>
</ul>
</li>
<li><strong>둘째줄:</strong> 단편화와 관련된 정보<ul>
<li><strong>식별 정보:</strong> 송신 호스트에 의해 생성되는 유일한 식별자로 단편들의 연결조각을 식별하는데 사용</li>
<li><strong>플래그/프래그먼트 오프셋</strong></li>
</ul>
</li>
<li><strong>셋째줄</strong><ul>
<li><strong>TTL(Time to Live):</strong> 데이터그램이 인터넷 시스템에 남아있도록 허락된 최대시간으로 라우터를 거칠 때마다 TTL 값이 1씩 감소한다. ⇒ 8비트이므로 카운트는 255</li>
<li><strong>프로토콜:</strong> 데이터 내 포함된 상위 계층 프로토콜</li>
<li><strong>헤더 체크섬:</strong> 헤더에 대한 오류 검출</li>
</ul>
</li>
</ul>
<h2 id="통신-방식">통신 방식</h2>
<ul>
<li><strong>멀티캐스트 프로토콜:</strong> 인터넷에서 같은 내용의 데이터를 여러 명의 특정한 그룹의 수신자들에게 동시에 전송할 수 있는 프로토콜로, 멀티캐스트 라우팅 프로토콜과 IGMP(그룹관리 프로토콜)이 합쳐진 형태이다.<ul>
<li><strong>멀티캐스트 라우팅 프로토콜:</strong> IP 주소로 구분되는 네트워크 상의 특정 그룹의 모든 사용자에게 동일한 메시지를 전송하기 위한 라우팅 프로토콜</li>
<li><strong>IGMP(Internet Group Management Protocol):</strong> 멀티캐스트를 지원하는 라우터가 멀티캐스트 그룹에 가입한 네트워크 내의 호스트를 관리하기 위해 사용하는 프로토콜 ex) IPTV</li>
</ul>
</li>
<li><strong>유니캐스트 프로토콜:</strong> 고유 주소로 식별된 하나의 네트워크 목적지에 1:1로 트래픽 또는 메시지를 전송하는 프로토콜</li>
<li><strong>브로드캐스트 프로토콜:</strong> 하나의 송신자가 동일한 서브 네트워크 상의 모든 수신자에게 데이터를 전송하는 프로토콜로, 특정 송신자의 브로드캐스트 중에는 다른 모든 수신자는 신호를 보낼 수 없다.  호스트 id 부분이 255(이진수로 모두 1)인 경우에 해당한다.</li>
<li><strong>애니캐스트 프로토콜:</strong> 하나의 호스트에서 그룹 내 가장 가까운 곳에 있는 수신자에게 데이터를 전달하는 프로토콜</li>
</ul>
<h2 id="서브넷subnetwork-부분망"><strong>서브넷(Subnetwork, 부분망)</strong></h2>
<p>서브넷(Subnet)은 네트워크 내에 존재하는 소규모 네트워크로, 네트워크 영역을 부분적으로 나눈 부분 네트워크이다. 인터넷 연결유무에 따라 Public Subnet과 Private Subnet으로 구분될 수 있다. </p>
<h3 id="서브넷-마스크"><strong>서브넷 마스크</strong></h3>
<p>서브넷 마스크는 서브넷을 만들 때 사용되는 것으로, 호스트 주소를 0으로 변환하여 IP 주소의 네트워크 주소를 반환하는 식별자 집합이다. 이를 이용하여 IP주소에서 네트워크 주소의 길이를 알 수 있다. 즉, IP 주소 체계의 네트워크 ID와 호스트 ID를 분리하는 역할을 한다. IP 주소에 서브넷 마스크를 AND 연산하면 네트워크 ID가 된다. </p>
<ul>
<li>호스트 IP의 개수는 <code>2^(호스트주소를 나타내는 비트 개수 - 2</code> 이다. 이때 2를 빼는 이유는 호스트주소 <code>111...1</code> 은 브로드 캐스트 주소로, <code>000...0</code> 은 네트워크 주소로 사용되기 때문이다.<ul>
<li><strong>예약 주소:</strong> 각 서브넷의 CIDR 블록에서 4개의 IP 주소와 마지막 IP 주소는 예약 주소로 사용자가 사용할 수 없다. 만일 서브넷 주소가 172.16.1.0/24일 경우,<ul>
<li>172.16.1.0 : 네트워크 주소(Network ID)</li>
<li>172.16.1.255 : 네트워크 브로드캐스트 주소</li>
</ul>
</li>
</ul>
</li>
<li><strong>서브넷팅:</strong> 서브넷팅을 수행하면 효율적인 네트워크 운용이 가능해진다. 즉, IP 주소 낭비를 방지할 수 있다. 서브넷 마스크의 bit 수를 1씩 증가시키면 네트워크 수는 2배수로 증가하고, 호스트 수는 2배수로 감소한다.</li>
</ul>
<blockquote>
<p><strong>CIDR(Classless Inter-Domain Routing)</strong></p>
<p>클래스를 없애고 비트 단위로 네트워크를 표현하는 주소 체계로, /24, /20 같은 프리픽스 길이를 사용한다. 인터넷상의 데이터 라우팅 효율성을 향상시킨다. 네트워크 라우터에서 표시된 서브넷에 따라 해당하는 디바이스로 데이터 패킷을 라우팅할 수 있다.</p>
</blockquote>
<h2 id="ipv6">IPv6</h2>
<p>초창기 인터넷은 IPv4 프로토콜로 구축되다가 32비트라는 제한된 주소 공간 및 국가별 할당 주소가 거의 소진되고 있다는 한계점으로 인해 지속적인 인터넷 발전에 문제가 예상되었다. 이에 대한 대안으로써 IPv6 프로토콜이 탄생하였다(휴대폰 및 컴퓨터에 할당되어 적용되고 있다).</p>
<p><strong>IPv6(Internet Protocol version 6)</strong>는 인터넷 프로토콜 스택 중 네트워크 계층의 프로토콜로서 버전 6 인터넷 프로토콜로 제정된 차세대 인터넷 프로토콜을 말한다.</p>
<ul>
<li><strong>주소 유형</strong></li>
</ul>
<pre><code>| 주소 | 구분 | 함축표현 |
| --- | --- | --- |
| 1080:0:0:0:8:800:200C:417A | Unicast | 1080::8:800:200C:417A |
| FF01:0:0:0:0:0:0:101 | Multicast | FF01::101 |
| 0:0:0:0:0:0:0:1 | Loopback | ::1 |
| 0:0:0:0:0:0:0:0 | Unsepcified | :: |</code></pre><h3 id="헤더-1"><strong>헤더</strong></h3>
<ul>
<li><strong>IPv4와 비교시 없어진 것</strong><ul>
<li><strong>IHL:</strong> IPv6에서 헤더의 길이가 항상 40바이트로 고정이기 때문에 헤더 필드 길이가 포함되지 않아도 된다.</li>
<li><strong>Identification, Flags, Fragment Offset:</strong> IPv6 Datagram Extension Headers에 별도로 정의된다.</li>
<li><strong>Header Checksum:</strong> IPv6 패킷의 비트레벨 오류 감지는 링크 계층에서 수행된다.</li>
</ul>
</li>
</ul>
<table>
<thead>
<tr>
<th>구분</th>
<th>IPv4</th>
<th>IPv6</th>
</tr>
</thead>
<tbody><tr>
<td>주소 길이</td>
<td>32bit</td>
<td>128bit</td>
</tr>
<tr>
<td>표시 방법</td>
<td>8비트씩 4부분으로 나뉜 10진수</td>
<td>16비트씩 8부분으로 나뉜 16진수</td>
</tr>
<tr>
<td>주소 개수</td>
<td>약 43억 개</td>
<td>3.4 x 10^38</td>
</tr>
<tr>
<td>주소 할당</td>
<td>클래스 단위 비순차적 할당</td>
<td>네트워크 규모 및 단말기 수에 따른 순차적 할당</td>
</tr>
<tr>
<td>헤더 크기</td>
<td>20바이트의 기본 헤더 부분과 가변적인 길이를 가지고 있는 옵션 부분으로 구성</td>
<td>40바이트의 고정길이</td>
</tr>
<tr>
<td>QoS</td>
<td>Best Effort 방식</td>
<td>등급별, 서비스별 패킷 구분 보장</td>
</tr>
<tr>
<td>보안 기능</td>
<td>IPSec 프로토콜 별도 설치</td>
<td>보안과 인증 확장 헤더 사용</td>
</tr>
<tr>
<td>Plug &amp; Play</td>
<td>x</td>
<td>o</td>
</tr>
<tr>
<td>모바일 IP</td>
<td>어려움</td>
<td>용이</td>
</tr>
<tr>
<td>웹 캐스팅</td>
<td>어려움</td>
<td>용이</td>
</tr>
<tr>
<td>전송 방식</td>
<td>유니캐스트, 멀티캐스트, 브로드캐스트</td>
<td>유니캐스트, 멀티캐스트, 애니캐스트</td>
</tr>
</tbody></table>
<h2 id="dhcp">DHCP</h2>
<p><strong>DHCP(Dynamic Host Configuration Protocol)</strong>는 동적 호스트 구성 프로토콜로 IP 구성 관리를 단순화하는 IP 표준이다. DHCP 표준은 DHCP 서버를 사용하여 IP 주소 및 관련된 기타 구성 세부 정보를 네트워크의 DHCP 사용 클라이언트에게 동적으로 할당하는 방법을 제공한다.</p>
<ul>
<li><strong>역할</strong><ul>
<li>네트워크 관리자들이 조직 내 네트워크 상에서 IP 주소를 중앙에서 관리하고 할당해줄 수 있도록 한다.</li>
<li><strong>DHCP를 사용하지 않는 경우:</strong> 각 컴퓨터마다 IP 주소가 수작업으로 입력되어야만 하며, 만약 컴퓨터가 네트워크 상 다른 장소로 이동되면 새로운 IP 주소가 입력되어야 한다.</li>
<li><strong>DHCP를 사용하는 경우:</strong> 네트워크 관리자가 DHCP 서버를 통해 중앙에서 IP 주소를 관리 및 담당할 수 있게 되고, 컴퓨터가 네트워크 상 다른 장소에 접속되면 자동으로 새로운 IP 주소를 보내줄 수 있게 한다.</li>
</ul>
</li>
</ul>
<h3 id="임대"><strong>임대</strong></h3>
<p>DHCP는 주어진 IP 주소가 일정한 시간동안만 해당 컴퓨터에 유효하도록 하는 ‘임대’ 개념을 사용하며, 임대 기간은 사용자가 특정한 장소에 얼마나 오랫동안 인터넷 접속이 필요할 것인지에 따라 달라진다.예를 들어, 사용자들이 자주 바뀌는 학교와 같은 환경에서 유용하게 활용될 수 있다.</p>
<p>사용 가능한 IP 주소의 개수보다 더 많은 컴퓨터가 있는 경우에도 IP 주소의 임대시간을 짧게 함으로써 네트워크를 동적으로 재구성할 수 있다(단, 영구적인 IP 주소를 필요로 하는 웹서버에 대해서는 정적인 주소를 제공).</p>
<h2 id="nat">NAT</h2>
<p><strong>NAT(Network Address Translation)</strong>은 IP 패킷의 TCP/UDP 포트 숫자, 출발지 및 목적지 IP 주소 등을 재기록하면서 라우터를 통해 네트워크 트래픽을 주고 받는 기술을 말한다. 대개 사설 네트워크 내 여러 개의 호스트가 하나의 공인 IP 주소를 사용하여 인터넷에 접속하기 위해 사용된다.</p>
<ul>
<li>특징<ul>
<li>패킷의 변화로 인해 IP나 TCP/UDP의 체크섬을 다시 계산해야 한다.</li>
<li>일반적으로 사설 IP는 외부에서 안보이기 때문에, 먼저 내부에서 외부로 나갔던 패킷만 다시 들어올 수 있다. 다만, 포트포워딩을 통해 외부에서 내부로 먼저 들어오게 허용할 수 있다.</li>
<li><strong>SNAT(Source NAT):</strong> 출발지 주소를 변경하는 NAT</li>
<li><strong>DNAT(Destination NAT):</strong> 도착지 주소를 변경하는 NAT</li>
</ul>
</li>
</ul>
<h2 id="도메인-접속-과정">도메인 접속 과정</h2>
<p>이전 DNS 포스팅에서 DNS 관점에서 특정 도메인 주소에 접속하는 과정을 언급한 바 있다. 이번에는 TCP/IP 통신 관점에서 살펴보고자 한다.</p>
<ul>
<li>순서<ol>
<li>주소창에 URL 입력 후 Enter를 입력한다.</li>
<li>웹 브라우저가 URL을 해석한 후, 문법에 맞으면 URL에 인코딩을 적용한다.</li>
<li>HSTS(HTTP Strict Transport Security) 목록을 로드한다. 이때, HSTS는 HTTP 대신 HTTPS만을 사용하여 통신해야 한다고 웹 사이트가 웹 브라우저에 알리는 보안 기능으로, 사이트가 해당 목록에 있는 경우에는 HTTPS로 요청하게 된다.</li>
<li>DNS를 조회하여 IP 주소를 얻는다.</li>
<li>ARP(Address Resolution Protocol)로 IP 주소에 해당하는 MAC 주소를 얻는다.</li>
<li>3-way handshake로 TCP 연결을 수립하고, 4-way handshake로 종료한다.  </li>
<li>HTTPS인 경우 SSL/TLS handshake 과정이 추가된다.</li>
<li>HTTP로 요청한 후 웹 브라우저는 응답을 바탕으로 화면을 렌더링한다.</li>
</ol>
</li>
</ul>
<h1 id="😎실습">😎실습</h1>
<p>리눅스 터미널에서의 <code>ip a</code> 명령어 결과를 살펴보자.</p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/0e961dff-85fc-4482-9e81-7259625af4f2/image.png" alt=""></p>
<ul>
<li><p><code>link/ether</code> 뒤 IPv6 주소는 내부 통신 주소</p>
</li>
<li><p><code>inet</code> 뒤 IPv4 주소가 사설 주소</p>
</li>
<li><p>브로드캐스트 주소로 <code>ping</code> 보내기
<img src="https://velog.velcdn.com/images/one1_programmer/post/f16531e0-ea99-4235-80a4-007901a55c6c/image.png" alt=""></p>
</li>
</ul>
<blockquote>
<p>참고자료
<a href="https://www.youtube.com/watch?v=GK3h936Co-k&amp;t=28s">IP주소를 알아봅시다! (Feat. 공인/사설/고정/유동IP, 포트포워딩, DMZ, DDNS)</a>
<a href="https://www.youtube.com/watch?v=W0x88b_dYhw">IP주소의 종류와 특징</a>
<a href="https://www.youtube.com/watch?v=gOMljj6K2V0&amp;list=PLXvgR_grOs1BFH-TuqFsfHqbh-gpMbFoy&amp;index=4">IPv4주소 체계에 대한 암기사항</a>
<a href="https://www.youtube.com/watch?v=9MPzEwZrRqo&amp;list=PLXvgR_grOs1BFH-TuqFsfHqbh-gpMbFoy&amp;index=16">IP헤더 형식과 의미 요약</a>
<a href="https://code-lab1.tistory.com/34">[네트워크] 서브넷,  서브넷마스크, 서브넷팅이란? | 서브넷팅 예제</a>
<a href="https://www.youtube.com/watch?v=5D9qz2CEIus">Unicast, Broadcast, Multicast</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프론트엔드] 모바일 브라우저 줌인/줌아웃 비활성화]]></title>
            <link>https://velog.io/@one1_programmer/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EB%AA%A8%EB%B0%94%EC%9D%BC-%EC%A4%8C%EC%9D%B8%EC%A4%8C%EC%95%84%EC%9B%83-%EB%B9%84%ED%99%9C%EC%84%B1%ED%99%94</link>
            <guid>https://velog.io/@one1_programmer/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EB%AA%A8%EB%B0%94%EC%9D%BC-%EC%A4%8C%EC%9D%B8%EC%A4%8C%EC%95%84%EC%9B%83-%EB%B9%84%ED%99%9C%EC%84%B1%ED%99%94</guid>
            <pubDate>Thu, 18 Dec 2025 13:12:29 GMT</pubDate>
            <description><![CDATA[<p>모바일 환경의 애플리케이션에서는 보통 두 손가락을 이용한 pinch zoom in/out 되지 않는다. 현재 진행하고 있는 프로젝트는 푸시 알림 지원을 위해 Next.js App Router + PWA 조합을 사용하고 있었다. </p>
<h1 id="🤔문제">🤔문제</h1>
<p>핀치 줌인/줌아웃에 대한 아무런 설정이 적용되어 있지 않아, 어느 화면에서나 두 손가락으로 확대/축소가 가능했다. 모바일에서 입력창 포커스 시 줌인되는 현상을 종종 경험한 적이 있을텐데, 이때 줌인으로 인해 네비게이션바가 뷰포트를 벗어나서 다시 줌아웃 하는 불편함을 느꼈을 수 있다. 
현재 프로젝트에서도 특별히 줌인을 지원하는 페이지가 없었기 때문에 이러한 동작을 비활성화할 필요가 생겼다.</p>
<h1 id="😎해결">😎해결</h1>
<p><code>&lt;meta&gt;</code> 태그의 뷰포트를 조정하면 쉽게 해결할 수 있다.</p>
<pre><code class="language-html">&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, user-scalable=no&quot; /&gt;</code></pre>
<p>Next.js에서는 layout.js(ts)에서 viewport를 지정할 수 있다.</p>
<pre><code class="language-js">// app/layout.ts
import type { Viewport } from &quot;next&quot;; // ts 사용하는 경우

export const viewport: Viewport = {
  width: &quot;device-width&quot;,
  initialScale: 1,
  maximumScale: 1,
  userScalable: false,
};</code></pre>
<p>이 방식은 PWA 환경에서는 잘 적용되었지만, Safari, Chrome 모바일 웹 브라우저에서는 동작하지 않았다. 찾아보니 iOS 특정 버전 이상부터는 해당 동작을 막았다고 한다. 따라서 CSS의 <code>touch-action</code> 속성을 사용하여 우회적으로 동작을 막아주어야 한다.</p>
<pre><code class="language-css">body {
  touch-action: pan-x pan-y;
}</code></pre>
<p>해당 속성에 대해서는 <a href="https://wit.nts-corp.com/2021/07/16/6397">이 블로그</a>에 정말 자세히 설명되어 있다. 간단히 설명하자면 &#39;한 손가락으로 터치 후 끌기&#39;를 수직/수평 방향에 대해 활성화 하는 것이다.</p>
<h2 id="주의할-점">주의할 점</h2>
<p>이러한 동작은 저시력자 등의 사용자에게 좋지 않은 웹 경험 제공하여 접근성을 해칠 여지가 있다. 따라서 텍스트 사이즈를 조정 가능하게 하는 등의 대비책이 필요하다.</p>
<blockquote>
<p>참고자료
<a href="https://nextjs.org/docs/app/api-reference/functions/generate-viewport#width-initialscale-maximumscale-and-userscalable">Next.js - Genrate Viewport</a>
<a href="https://pythontoomuchinformation.tistory.com/593#google_vignette">블로그</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js] Pages Router 기본 개념 ]]></title>
            <link>https://velog.io/@one1_programmer/Next.js-Pages-Router-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90</link>
            <guid>https://velog.io/@one1_programmer/Next.js-Pages-Router-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90</guid>
            <pubDate>Tue, 16 Dec 2025 23:14:59 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>한 입 크기로 잘라먹는 Next.js (이정환) 강의를 듣고 학습 내용을 기록하였습니다.</p>
</blockquote>
<p>Next.js의 최신 버전은 모두 App Router 방식을 취하고 있다. 이전 버전의 Pages Router는 파일 기반 라우팅, CSR 중심 구조, getServerSideProps, getStaticProps 같은 데이터 패칭 방식을 통해 React SPA의 단점을 보완하려는 첫 번째 해법이었다. App Router는 이 구조의 문제점(복잡한 데이터 패칭, 서버·클라이언트 경계 불명확, 중복 렌더링)을 해결하기 위해 나왔다. App Router의 핵심 개념들이 Pages Router에서 출발하므로, Pages Router에 대한 대략적인 이해는 충분히 도움이 될 것이다. </p>
<table>
<thead>
<tr>
<th>Pages Router</th>
<th>App Router</th>
</tr>
</thead>
<tbody><tr>
<td>SSR / SSG</td>
<td>Server Components</td>
</tr>
<tr>
<td>API Routes</td>
<td>Route Handlers</td>
</tr>
<tr>
<td><code>_app.tsx</code>, <code>_document.tsx</code></td>
<td>layout, template</td>
</tr>
<tr>
<td>CSR / hydration</td>
<td>Streaming / Suspense</td>
</tr>
</tbody></table>
<p>Next.js의 <strong>Pages Router</strong>는 각 파일을 라우트에 대응시키는 직관적인 파일 시스템 형식의 라우팅을 제공한다. 이는 react router와 같이 페이지 라우팅 기능을 제공하는 라이브러리의 기능을 거의 유사하게 제공한다.</p>
<ul>
<li><p>기본 파일 구조</p>
<ul>
<li><p><code>app.tsx</code>: 리액트의 App.tsx와 동일한 역할을 하며, 전체 페이지에 공통적으로 포함되는 헤더, 레이아웃, 비즈니스 로직 등을 포함한다.</p>
</li>
<li><p><code>document.tsx</code>: 리액트의 index.html과 비슷한 역할을 하며, 모든 페이지에 공통적으로 적용되어야 할 html을 지정한다.</p>
</li>
<li><p><code>index.tsx</code>: 특정 경로의 페이지 진입점을 나타내며, <code>[경로명]/index.tsx</code> 와 같이 설정도 가능하다.</p>
</li>
<li><p><code>[id].tsx</code>: url 파라미터에 해당하는 페이지를 반환한다.</p>
<p>  <img src="https://velog.velcdn.com/images/one1_programmer/post/94156e9b-19c5-4a9d-bbca-ffab1da452e9/image.png" alt=""></p>
</li>
</ul>
</li>
</ul>
<pre><code>- `[...id].tsx` (catch all segment 방식): url 파라미터 내의 연속하는 모든 segment(구간)에 대응하는 페이지를 나타낼 수 있으며, 특정 경로에 대한 기본 페이지를 반환하려면 index.tsx를 따로 작성이 필요하다.

    ![](https://velog.velcdn.com/images/one1_programmer/post/4b85b690-6a14-4110-9367-3e94239ba6f1/image.png)


- `[[...idx]].tsx` (optional catch all segment 방식): index.tsx의 역할까지 수행한다. 즉 특정 경로에 대한 기본 페이지를 반환할 수 있다.
- `404.tsx`: 없는 경로에 대한 페이지를 반환한다.</code></pre><ul>
<li><p><code>useRouter</code>: next/router(page router 전용 패키지)로부터 라우팅 정보를 가져오며, 쿼리 스트링, url 파라미터와 같은 정보를 추출할 수 있다.</p>
</li>
<li><p><code>Link</code>: <code>&lt;a&gt;</code> 태그는 기본적으로 서버에서 페이지를 불러와 네비게이팅하므로, 클라이언트 사이드에서의 네비게이팅을 지원하는 next의 Link 컴포넌트를 사용한다.</p>
</li>
<li><p>API Routes: Next.js에서 API를 구축할 수 있게 해주는 기능으로, api 폴더 내에 정의하면 <code>/api/폴더명</code> 으로 요청이 가능하다.</p>
<pre><code class="language-tsx">  import type { NextApiRequest, NextApiResponse } from &quot;next&quot;;

  export default function handler(req: NextApiRequest, res: NextApiResponse) {
      const date = new Date();
      res.json({ time: date.toLocaleString() });
  }</code></pre>
</li>
<li><p><strong>장점</strong></p>
<ul>
<li>파일 시스템 기반의 간편한 페이지 라우팅을 제공한다.</li>
<li>다양한 방식의 사전 렌더링을 제공한다.</li>
</ul>
</li>
<li><p><strong>단점</strong></p>
<ul>
<li>페이지별 레이아웃 설정이 번거롭다. (getLayout 메서드)</li>
<li>데이터 페칭이 페이지 컴포넌트에 집중된다. 페이지 컴포넌트의 자식 컴포넌트들에 props를 계속 넘겨줘야 한다(Props Drilling).</li>
<li>불필요한 컴포넌트들도 JS Bundle에 포함된다. 즉, 상호작용이 없는 컴포넌트들까지 클라이언트 측에서 hydration 과정을 거친다. 이들은 서버에서 한 번만 실행되면 충분하다는 뜻이다. =&gt; TTI(Time to Interaction) 증가</li>
</ul>
</li>
</ul>
<h1 id="🤔개념">🤔개념</h1>
<h2 id="ssr-server-side-rendering">SSR (Server Side Rendering)</h2>
<p>가장 기본적인 사전 렌더링 방식으로, 요청이 들어올 때마다 사전 렌더링을 수행한다.</p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/1e19ab7a-13ff-44b3-871b-92bcd850bda6/image.png" width="700">

<h3 id="getserversideprops"><strong><code>getServerSideProps</code></strong></h3>
<p>컴포넌트 내에 이 함수를 선언하면 서버사이드 렌더링을 위한 컴포넌트로 지정하는 것이다. 즉, 컴포넌트보다 먼저 실행되어서 컴포넌트에 필요한 데이터를 불러오는 역할을 수행한다. </p>
<ul>
<li>특징<ul>
<li>반환값은 <code>props</code> 속성을 포함하는 단 하나의 객체여야 한다.</li>
<li>사용하는 컴포넌트 측에서는 해당 <code>props</code> 내부의 속성들을 props로써 사용할 수 있다. 또한, props의 타입은 Next.js가 제공하는 InferGetServerSidePropsType를 사용하여 추론할 수 있다.</li>
<li>서버측에서 실행된다.</li>
<li><strong><code>context: GetServerSidePropsContext</code>:</strong> <code>getServerSideProps</code> 함수는 context를 props로 받을 수 있으며, 여기에는 url파라미터나 쿼리스트링 정보를 포함한 ~정보가 담겨있다.</li>
</ul>
</li>
</ul>
<pre><code class="language-tsx">import { InferGetServerSidePropsType } from &quot;next&quot;;

export const getServerSideProps = () =&gt; {
  const data = &quot;hello&quot;;
  console.log(&quot;서버사이드props!&quot;);

  return {
    props: {
      data,
    },
  };
};

export default function Home({
  data,
}: InferGetServerSidePropsType&lt;typeof getServerSideProps&gt;) {
  console.log(&quot;🚀 ~ Home ~ data:&quot;, data); // 서버에서 한번, 브라우저에서 한번 실행되어 총 두 번 실행됨

  return (&lt;&gt;&lt;/&gt;);
}

Home.getLayout = (page: ReactNode) =&gt; {
  return &lt;SearchableLayout&gt;{page}&lt;/SearchableLayout&gt;;
};</code></pre>
<h2 id="ssg-static-site-generation">SSG (Static Site Generation)</h2>
<p>하지만 데이터 페칭 자체가 오래걸리면 SSR은 오히려 초기 로딩을 늦출 수도 있다. 따라서 Next.js는 빌드 타입에 데이터 페칭을 미리 수행하는 SSG 등의 방법을 제공하여 이러한 우려를 해소한다. 즉, SSG는 빌드 타임에 페이지를 사전 렌더링 해둔다.</p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/4d561b0b-dc2a-434e-8853-f208a5ab557d/image.png" width="700">

<ul>
<li>장점<ul>
<li>사전 렌더링에 많은 시간이 소요되는 페이지더라도 사용자의 요청에는 매우 빠른 속도로 응답이 가능하다.</li>
</ul>
</li>
<li>단점<ul>
<li>매번 똑같은 페이지만 응답하므로 최신 데이터 반영은 어렵다.</li>
</ul>
</li>
</ul>
<h3 id="getstaticprops"><code>getStaticProps</code></h3>
<p>컴포넌트 내에서 이 함수를 선언하면 정적 페이지 렌더링을 위한 컴포넌트로 지정하는 것이며, 빌드 타임에 한 번만 실행된다.</p>
<pre><code class="language-tsx">import { InferGetStaticPropsType } from &quot;next&quot;;

export const getStaticProps = async () =&gt; {
  console.log(&quot;SSG 빌드 시작&quot;); // 미리 빌드한 페이지를 출력하므로, 빌드 타임에서만 출력됨
  const [allBooks, recoBooks] = await Promise.all([
    fetchBooks(),
    fetchRandomBooks(),
  ]);

  return {
    props: {
      allBooks,
      recoBooks,
    },
  };
};

export default function Home({
  allBooks,
  recoBooks,
}: InferGetStaticPropsType&lt;typeof getStaticProps&gt;) {
  return (&lt;&gt;&lt;/&gt;);
}</code></pre>
<p>SSR의 <code>/</code> 경로의 페이지 빌드 결과는 아래와 같이 Dynamic 페이지로 생성된다. 즉, 해당 페이지는 on demand(주문형)으로 불러오게 된다.</p>
  <p><img src="https://velog.velcdn.com/images/one1_programmer/post/7a1100ce-6c34-4819-b621-b635ba150bd1/image.png" width="400">

<p>반면, SSG 적용 후 빌드 결과는 아래와 같다.</p>
  <p><img src="https://velog.velcdn.com/images/one1_programmer/post/987d6551-e5d9-4183-b670-800217c419d1/image.png" width="400">

<p>한편, <code>404</code> 페이지와 같이 아무런 설정을 해주지 않은 페이지에 대해서 Next.js는 기본적으로 Static으로써 제공한다.</p>
<h3 id="쿼리스트링에-접근해야-하는-경우">쿼리스트링에 접근해야 하는 경우</h3>
<p>SSG 방식으로는 SSR과 달리 <code>context</code>를 통해 쿼리스트링에 접근할 수 없다. 따라서, 클라이언트 사이드에서 이를 조작해야 한다. Next.js는 기본적으로 페이지를 Static으로 제공하므로, 이 경우에는 검색바 레이아웃 등 정적으로 제공해도 되는 html의 일부만 미리 빌드해 반환한다.</p>
<pre><code class="language-tsx">export default function Page() {
  const [books, setBooks] = useState&lt;BookData[]&gt;([]);
  const router = useRouter();
  const q = router.query.q;

  const fetchSearchResult = async () =&gt; {
    const data = await fetchBooks(q as string); // 쿼리스트링을 통해 검색된 도서 목록을 가져옴
    setBooks(data); // 가져온 도서 목록을 상태에 저장

  };

  useEffect(() =&gt; {
    if (q) {
      fetchSearchResult();
    }
  }, [q]);

  return (
    &lt;div&gt;
      {books.map((book) =&gt; (
        &lt;BookItem key={book.id} {...book} /&gt;
      ))}
    &lt;/div&gt;
  );
}</code></pre>
  <p><img src="https://velog.velcdn.com/images/one1_programmer/post/711f7b9d-9513-49d8-9c8e-980a1a1cfc54/image.png" width="500">

<h3 id="동적-경로에-적용">동적 경로에 적용</h3>
<p><code>[id].jsx</code> 와 같이 동적으로 결정되는 페이지의 경우, <code>getStaticPaths</code> 함수 내에서 SSG를 원하는 경로에 대해 url 파라미터의 값을 미리 설정해주어야 한다. 그래야만 Next.js가 사전렌더링 해야 할 페이지로 인식하여 빌드타임에 사전렌더링을 수행할 수 있다.  </p>
<pre><code class="language-tsx">// [id].tsx
import {
  GetStaticPropsContext,
  InferGetStaticPropsType,
} from &quot;next&quot;;

export const getStaticPaths = () =&gt; {
  return {
    // 정적 생성할 경로를 지정하는 부분
    paths: [
      { params: { id: &quot;1&quot; } },
      { params: { id: &quot;2&quot; } },
      { params: { id: &quot;3&quot; } },
    ],
    fallback: false,
  };
};

export const getStaticProps = async (context: GetStaticPropsContext) =&gt; {
  const id = context.params!.id;
  const movie = await fetchOneMovie(Number(id));

  return {
    props: { movie },
  };
};

export default function Page({ movie }: InferGetStaticPropsType&lt;typeof getStaticProps&gt;) {
  return ();
}</code></pre>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/5500cac9-43de-4d14-91c3-095f979de25f/image.png" width="500">


<h3 id="fallback">fallback</h3>
<p>정적 생성되지 않은 경로에 대한 fallback을 처리하며, <code>true</code>, <code>false</code>, <code>&#39;blocking&#39;</code> 중 하나를 선택할 수 있다.</p>
<ul>
<li><p><strong>false:</strong> 정적 생성되지 않은 경로에 대한 fallback을 제공하지 않으며, 404 페이지로 이동시킨다.</p>
</li>
<li><p><strong>&#39;blocking&#39;:</strong> 정적 생성되지 않은 경로에 대한 fallback을 제공하며, 페이지가 로드될 때 서버에서 데이터를 가져온다(SSR과 유사). 클라이언트에게는 페이지가 준비될 때까지 기다리게 한다. =&gt; 새로운 데이터가 계속 추가되어야 하는 상황에서 사용 가능하다.</p>
</li>
<li><p><strong>true:</strong> 정적 생성되지 않은 경로에 대한 fallback을 제공하며, 페이지가 로드될 때 서버에서 데이터를 가져온다. 이때, 데이터 요청 시간이 길어질 경우 사용자 경험에 영향을 주는 것을 방지하기 위해 props가 없는 페이지를 먼저 반환한 후, props가 계산되면 props만 따로 반환하는 방식으로 동작한다. 즉, 데이터가 없는 상태의 페이지를 먼저 렌더링 해주는 것이다.</p>
<ul>
<li><p><strong>props 계산 중 보여줄 화면 생성</strong></p>
<pre><code class="language-tsx">  export default function Page({
    book,
  }: InferGetStaticPropsType&lt;typeof getStaticProps&gt;) {
    const router = useRouter();
    if (router.isFallback) return &quot;로딩중입니다.&quot;;
  }</code></pre>
</li>
<li><p><strong>데이터가 없는 경우에도 404 페이지로 보내고 싶은 경우</strong></p>
<pre><code class="language-tsx">  export const getStaticProps = async (context: GetStaticPropsContext) =&gt; {
    const id = context.params!.id as string;
    const movie = await fetchOneMovie(id);

    // 영화 데이터가 없으면 404 페이지로 이동
    if (!movie) {
      return {
        notFound: true,
      };
    }

    return {
      props: {
        movie,
      },
    };
  };</code></pre>
</li>
</ul>
</li>
</ul>
<h2 id="isr-incremental-static-regeneration">ISR (Incremental Static Regeneration)</h2>
<p>SSG 방식으로 생성된 정적 페이지를 일정 시간을 주기로 다시 생성하는 기술</p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/a4e5754b-592a-4069-8fb8-a626a69321e8/image.png" width="600">

<ul>
<li><strong>특징</strong><ul>
<li><strong>기존 SSG 방식의 장점:</strong> 매우 빠른 속도로 응답 가능</li>
<li><strong>기존 SSR 방식의 장점:</strong> 최신 데이터 반영 가능</li>
</ul>
</li>
</ul>
<h3 id="revalidate"><code>revalidate</code></h3>
<p>기존 SSG 방식의 <code>getStaticProps</code>의 반환문에 <code>revalidate</code> 옵션만 추가하면 된다.</p>
<pre><code class="language-tsx">// SSG -&gt; ISR
export const getStaticProps = async () =&gt; {
  console.log(&quot;ssg 인덱스 페이지&quot;);

  const [allBooks, recoBooks] = await Promise.all([
    fetchBooks(),
    fetchRandomBooks(),
  ]);

  return {
    props: {
      allBooks,
      recoBooks,
    },
    // ISR 적용
    revalidate: 3, // 3초에 한 번 재생성
  };
};
</code></pre>
  <p><img src="https://velog.velcdn.com/images/one1_programmer/post/5d9a4099-1d08-4aca-9ff2-30a9fc43e002/image.png" width="500">

<h3 id="on-demand-isr">On-demand ISR</h3>
<p>하지만 단순히 시간 기반으로 페이지를 재성성하게 되면 시간과 관계없이 사용자의 행동에 따라 데이터가 업데이트되는 페이지에 대처하기 어렵다. 이러한 ISR 방식의 단점을 해소하기 위해 특정 경우에 재생성할 수 있게 한다.</p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/c1ee01e3-d256-4a16-81a1-c001593bd233/image.png" width="600">

<p>이를 위해 api 폴더 내부에 특정 api 요청(API Route)에 대한 res에 <code>revalidate</code> 메서드를 지정한다.</p>
<pre><code class="language-tsx">// api/revalidate.ts
import { NextApiRequest, NextApiResponse } from &quot;next&quot;;

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    await res.revalidate(&quot;/&quot;); // &#39;/&#39; 경로에 대한 재생성을 수행
    return res.json({ revalidate: true });
  } catch (err) {
    console.error(err);
    res.status(500).json({ message: &quot;revalidate error&quot; });
  }
}</code></pre>
<h2 id="seo">SEO</h2>
<p>next/head의 <code>Head</code> 태그를 사용하여 기본적인 메타태그를 적용할 수 있다.</p>
<pre><code class="language-tsx">import Head from &quot;next/head&quot;;

export default function Home({
  allBooks,
  recoBooks,
}: InferGetStaticPropsType&lt;typeof getStaticProps&gt;) {
  return (
    &lt;&gt;
      &lt;Head&gt;
        &lt;title&gt;네스트넷 도서관&lt;/title&gt;
        &lt;meta property=&quot;og:image&quot; content=&quot;/thumbnail.png&quot; /&gt;
        &lt;meta property=&quot;og:title&quot; content=&quot;네스트넷 도서관&quot; /&gt;
        &lt;meta
          property=&quot;og:description&quot;
          content=&quot;네스트넷 도서관에 등록된 도서들을 만나보세요.&quot;
        /&gt;
      &lt;/Head&gt;
      &lt;div className={s.container}&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
}</code></pre>
<ul>
<li><p><strong>SSG인 경우 메타태그 처리:</strong> SSG로 페이지를 불러오는 경우, fallback 상태일 때 메타태그를 불러오지 못하는 문제가 생긴다. 따라서 조건문을 통해 fallback 상태일 때 나타낼 메타태그를 지정할 필요가 있다.</p>
<pre><code class="language-tsx">  import Head from &quot;next/head&quot;;

  export default function Page({
    book,
  }: InferGetStaticPropsType&lt;typeof getStaticProps&gt;) {
    const router = useRouter();
    if (router.isFallback) {
      // fallback 상태일 때도 메타태그를 보여줄 수 있음
      return (
        &lt;&gt;
          &lt;Head&gt;
            &lt;title&gt;네스트넷 도서관&lt;/title&gt;
            &lt;meta property=&quot;og:image&quot; content=&quot;/thumbnail.png&quot; /&gt;
            &lt;meta property=&quot;og:title&quot; content=&quot;네스트넷 도서관&quot; /&gt;
            &lt;meta
              property=&quot;og:description&quot;
              content=&quot;네스트넷 도서관에 등록된 도서들을 만나보세요.&quot;
            /&gt;
          &lt;/Head&gt;
        &lt;/&gt;
      );
    }

    if (!book) return &quot;문제가 발생했습니다 다시 시도하세요&quot;;

    const { id, title, subTitle, description, author, publisher, coverImgUrl } =
      book;

    return (
      &lt;&gt;
        &lt;Head&gt;
          &lt;title&gt;{title}&lt;/title&gt;
          &lt;meta property=&quot;og:image&quot; content={coverImgUrl} /&gt;
          &lt;meta property=&quot;og:title&quot; content={title} /&gt;
          &lt;meta property=&quot;og:description&quot; content={description} /&gt;
        &lt;/Head&gt;
        &lt;div key={id} className={s.container}&gt;
        &lt;/div&gt;
      &lt;/&gt;
    );
  }</code></pre>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js] 웹 개발 생태계의 진화와 함께 보는 등장배경]]></title>
            <link>https://velog.io/@one1_programmer/Next.js-%EC%9B%B9-%EA%B0%9C%EB%B0%9C-%EC%83%9D%ED%83%9C%EA%B3%84%EC%9D%98-%EC%A7%84%ED%99%94%EC%99%80-%ED%95%A8%EA%BB%98-%EB%B3%B4%EB%8A%94-%EB%93%B1%EC%9E%A5%EB%B0%B0%EA%B2%BD</link>
            <guid>https://velog.io/@one1_programmer/Next.js-%EC%9B%B9-%EA%B0%9C%EB%B0%9C-%EC%83%9D%ED%83%9C%EA%B3%84%EC%9D%98-%EC%A7%84%ED%99%94%EC%99%80-%ED%95%A8%EA%BB%98-%EB%B3%B4%EB%8A%94-%EB%93%B1%EC%9E%A5%EB%B0%B0%EA%B2%BD</guid>
            <pubDate>Mon, 15 Dec 2025 06:14:51 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><em>Next.js는 풀스택 웹 애플리케이션을 구축하기 위한 React 프레임워크입니다. React Components를 사용하여 사용자 인터페이스를 구축하고, Next.js를 사용하여 추가 기능과 최적화를 수행합니다. 또한 번들러 및 컴파일러와 같은 하위 수준의 도구를 자동으로 구성합니다. 대신 제품을 구축하고 빠르게 배송하는 데 집중할 수 있습니다. 개인 개발자이든 더 큰 팀의 일원이든, Next.js는 인터랙티브하고 역동적이며 빠른 리액트 애플리케이션을 구축하는 데 도움을 줄 수 있습니다.</em>
[출처: Next.js 공식문서]</p>
</blockquote>
<p>위는 Next.js 공식문서의 첫 페이지의 소개 내용이다. 이를 조금 더 깊게 이해하기 위해서 등장 배경을 조금 더 알아보기로 했다.</p>
<h2 id="등장-배경">등장 배경</h2>
<p>프론트엔드 등장 이전에는 서버 개발자가 JSP와 같은 템플릿 엔진을 사용하여 웹 개발을 수행하였다. 하지만 더 많은 상호 작용이 이뤄져야 하는 현대의 웹에서 템플릿 엔진만으로 개발하는 것은 한계를 보였다. 이러한 방식은 구현이 단순하고 초기 웹 환경에서는 충분히 효과적이었으나, 페이지 단위의 갱신으로 인해 사용자 경험 측면에서 한계를 지니고 있었다.</p>
<blockquote>
<p><strong>템플릿 엔진</strong>
HTML에 고정되거나 변동되는 데이터를 채워 넣는 과정을 담당하는 도구</p>
</blockquote>
<h4 id="ajax의-등장">AJAX의 등장</h4>
<p>이후, Ajax(Asynchronous JavaScript and XML)의 등장으로 서버와의 통신을 비동기적으로 처리할 수 있게 되었고, 전체 페이지를 새로 고치지 않고도 필요한 데이터만을 교환할 수 있게 되었다. 이로 인해 데이터 교환 레이어와 프레젠테이션 레이어가 분리되었고, 사용자는 보다 매끄럽고 즉각적인 웹 인터랙션을 경험할 수 있게 되었다. </p>
<h4 id="프론트엔드과-백엔드-영역의-분리">프론트엔드과 백엔드 영역의 분리</h4>
<p>이러한 변화는 클라이언트 측 JavaScript의 역할을 크게 확장시켰다. 서버는 점차 HTML을 생성하는 역할에서 벗어나 데이터를 제공하는 API 서버로 전환되었고, 클라이언트는 전달받은 데이터를 기반으로 화면을 구성하는 책임을 맡게 되었다. 이 과정에서 프론트엔드와 백엔드의 역할이 명확히 분리되었으며, 프론트엔드는 기본적인 HTML 위에 JavaScript 번들을 실행하여 동적으로 UI를 구성하는 독립적인 영역으로 자리 잡게 되었다.</p>
<h4 id="spa의-등장">SPA의 등장</h4>
<p>이 흐름 속에서 React와 같은 <strong>SPA(Single Page Application)</strong> 라이브러리가 등장하였다. SPA는 페이지 전체를 다시 로드하지 않고도 상태 변화에 따라 화면을 갱신함으로써 뛰어난 사용자 경험을 제공하였다. 그러나 모든 렌더링을 클라이언트에서 수행하는 구조는 초기 로딩 지연, 검색 엔진 최적화(SEO)의 어려움, 라우팅 및 코드 분할과 같은 공통 문제를 개발자가 직접 해결해야 한다는 한계를 동반하였다.</p>
<h4 id="nextjs의-등장">Next.js의 등장</h4>
<p>Next.js는 이러한 SPA 기반 개발의 한계를 보완하기 위해 등장하였다. 서버 사이드 렌더링(SSR), 정적 사이트 생성(SSG), 파일 기반 라우팅, 코드 분할 등 웹 애플리케이션에 반복적으로 요구되는 기능을 프레임워크 차원에서 기본 제공함으로써, 개발자가 인프라와 구조 설계에 과도한 부담을 지지 않도록 한다. 이를 통해 React의 선언적 UI 개발 방식은 유지하면서도, 성능과 SEO, 초기 로딩 문제를 효과적으로 해결할 수 있는 구조를 제공한다.</p>
<p>결국, 라이브러리인 React와 달리 Next.js는 프레임워크로서 애플리케이션의 구조와 실행 흐름을 일정 부분 규정한다. 이는 기능 구현의 주도권이 전적으로 개발자에게 있지 않음을 의미하며, 대신 검증된 패턴과 기본 설정을 통해 일관된 개발 경험과 생산성을 제공한다는 점에서 Next.js의 등장 의의를 찾을 수 있다.</p>
<h1 id="🤔개념">🤔개념</h1>
<h2 id="routing">Routing</h2>
<p>CSR에서는 기본적으로 경로(URL)가 하나, SSR에서는 여러 페이지가 존재하며 각각의 경로가 존재한다. 따라서 SSR에서 경로에 따라 페이지를 선택하는 방법이 필요했고, 이때 경로에 페이지를 대응시키는 프로세스가 Routing이다.</p>
<ul>
<li><p><strong>명시적 라우팅</strong> ex) CSR의 React Router</p>
<pre><code class="language-jsx">  &lt;Route path=&#39;/&#39; components = {&lt;Home /&gt;}/&gt;</code></pre>
</li>
<li><p><strong>코드 기반 라우팅:</strong> ex) nodejs 기반의 express의 라우팅</p>
<pre><code class="language-jsx">  app.get(&#39;/&#39;, (req, res) =&gt; { res.send(&#39;Hello&#39;); };</code></pre>
</li>
<li><p><strong>데코레이터 기반 라우팅:</strong> ex) NestJS</p>
<pre><code class="language-jsx">  @Controller(&#39;users&#39;)
  export class Users Controller {
      @Get()
      findAll(): string {
          return &#39;Hello&#39;;
      }
  }</code></pre>
</li>
</ul>
<p>Next.js는 파일 기반 라우팅을 선택했다. 말 그대로 파일명에 따라 그 기능을 고정하고 경로를 지정한다. 예시로 <code>about/age.tsx</code> 는 ‘/about’ 경로에 존재하는 페이지이다. 이렇게 Next.js는 개발자들이 자주 사용하는 구조를 쉽게 사용할 수 있게 추상화된 구조를 파일 컨벤션으로써 제공한다.</p>
<p>파일 구조 기반을 채택함으로써 얻는 장점은 다음과 같다. </p>
<p>첫째, 직관적이다.</p>
<p>둘째, 경로가 모두 나누어져 있어 특정 경로에서 필요한 JavaScript 번들만 받아오면 되고, 응답 시간이 줄어든다. React로 개발할 때를 생각해보면, 별도의 router 파일을 생성하고 해당 파일 내에서 <code>React.lazy</code> 로 지연 로딩을 수행했었다. 하지만 Next.js는 서로 다른 경로에 따라 자동적으로 코드 스플리팅을 수행해준다.</p>
<blockquote>
<p><strong>Navigation</strong>
페이지 간 이동 과정 자체를 의미한다.</p>
</blockquote>
<blockquote>
<p>참고자료
<a href="https://www.youtube.com/watch?v=EwY6hbAxdV8&amp;list=PLpq56DBY9U2AyFtF0ajuFZX3IGgDIXgcb">Nextjs 가 등장한 기술적인 배경(프론트엔드 개발 역사와 함께 살펴보기)</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[네트워크] TCP의 오류/흐름/혼잡 제어, 신뢰성 있는 전송을 위한 메커니즘]]></title>
            <link>https://velog.io/@one1_programmer/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-TCP%EC%9D%98-%EC%98%A4%EB%A5%98%ED%9D%90%EB%A6%84%ED%98%BC%EC%9E%A1-%EC%A0%9C%EC%96%B4-%EC%8B%A0%EB%A2%B0%EC%84%B1-%EC%9E%88%EB%8A%94-%EC%A0%84%EC%86%A1%EC%9D%84-%EC%9C%84%ED%95%9C-%EB%A9%94%EC%BB%A4%EB%8B%88%EC%A6%98</link>
            <guid>https://velog.io/@one1_programmer/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-TCP%EC%9D%98-%EC%98%A4%EB%A5%98%ED%9D%90%EB%A6%84%ED%98%BC%EC%9E%A1-%EC%A0%9C%EC%96%B4-%EC%8B%A0%EB%A2%B0%EC%84%B1-%EC%9E%88%EB%8A%94-%EC%A0%84%EC%86%A1%EC%9D%84-%EC%9C%84%ED%95%9C-%EB%A9%94%EC%BB%A4%EB%8B%88%EC%A6%98</guid>
            <pubDate>Thu, 11 Dec 2025 13:58:08 GMT</pubDate>
            <description><![CDATA[<p>TCP 세그먼트의 체크섬 필드는 단순히 데이터의 훼손 여부만 나타내므로 패킷 자체의 유실, 잘못된 순서로 전송되는 등의 상황에는 이것만으로는 대처할 수 없다. 따라서 TCP는 연결 수립 이후 오류/흐름/혼잡 제어를 통해 신뢰성있는 전송을 보장한다.</p>
<h1 id="🤔개념">🤔개념</h1>
<h2 id="오류-제어">오류 제어</h2>
<ul>
<li><strong>중복된 ACK 세그먼트를 수신하는 상황:</strong> 송신한 세그먼트의 일부가 전송 중 유실되어 중복으로 ACK 세그먼트를 수신하게 되는 상황</li>
<li><strong>타임아웃이 발생하는 상황:</strong> 세그먼트를 송신하는 호스트는 재전송 타이머라는 특별한 값을 유지하고, 세그먼트를 전송할 때마다 이 재전송 타이머를 시작한다. 타임아웃 발생 시점까지 ACK 세그먼트를 받지 못하면 문제가 발생했다고 간주하고 재전송하게 된다.</li>
</ul>
<p>ARQ(Automatic Repeat Request, 자동 재전송 요구)란 수신 호스트의 응답(ACK)과 타임아웃을 토대로 문제를 진단하고, 문제가 생긴 메시지를 재전송함으로써 신뢰성을 확보하는 방식이다.</p>
<blockquote>
<p>ARQ를 사용하는 프로토콜이나 계층은 TCP나 전송 계층에 국한된 것은 아니다.</p>
</blockquote>
<h3 id="stop-and-wait-arq">Stop-and-Wait ARQ</h3>
<p>제대로 전달했음을 확인하기 전까지는 새로운 메시지를 보내지 않는 기법이다.</p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/47cf5a24-eb03-4a0d-929f-ee5b170776fe/image.png" width="400"/>


<ul>
<li><strong>장점:</strong> 단순하지만, 높은 신뢰성을 보장한다.</li>
<li><strong>단점:</strong> 네트워크의 이용 효율이 낮아지고 성능이 저하된다.<ul>
<li><strong>파이프라이닝 전송:</strong> 오늘날의 TCP는 확인 응답을 받기 전이라도 여러 메시지를 보내는 방식으로 세그먼트를 송신한다.</li>
</ul>
</li>
</ul>
<h3 id="go-back-n-arq">Go-Back-N ARQ</h3>
<p>파이프라이닝 기반 ARQ의 일종으로, 여러 세그먼트 전송 중 오류가 발생하면 해당 세그먼트부터 전부 재전송하는 기법이다.</p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/05f74a1e-25c6-4286-b78e-5d16d2bc7163/image.png" width="400"/>


<ul>
<li><strong>누적 확인 응답(CACK, Cumulative Acknowledgement):</strong> 순서 번호 N번에 대한 ACK 세그먼트는 n번까지의 누적 확인 응답이라 볼 수 있다.</li>
</ul>
<blockquote>
<p><strong>TCP의 빠른 재전송</strong>
재전송 타이머가 만료되기 전이라도 세 번의 동일한 ACK 세그먼트를 받았다면, 타임아웃이 발생했다고 간주하고 곧바로 세그먼트를 재전송한다.</p>
</blockquote>
<h3 id="selective-repeat-arq">Selective Repeat ARQ</h3>
<p>선택적으로 반복하여 세그먼트를 재전송하는 기법이다. 오늘날 대부분의 호스트는 Selective Repeat ARQ를 지원하며, 이를 사용하지 않을 경우 Go-Back-N ARQ로 동작하게 된다.</p>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/90c5ef07-39e9-4951-b702-6328c9c8acc3/image.png" width="400"/>

<ul>
<li><strong>개별 확인 응답(Selective Acknowledgement):</strong> 누락된 개별 세그먼트에 한정된 확인 응답이라 볼 수 있다.</li>
</ul>
<blockquote>
<p><strong>Go-Back-N ARQ와 Selective ARQ를 시각적으로 확인할 수 있는 학습 사이트</strong>
<a href="https://www.tkn.tu-berlin.de/teaching/rn/animations/gbn_sr/">https://www.tkn.tu-berlin.de/teaching/rn/animations/gbn_sr/</a></p>
</blockquote>
<h2 id="흐름-제어">흐름 제어</h2>
<p>수신 호스트가 한 번에 받아 처리할 수 있을 만큼만 전송하는 기법이다. 수신 호스트가 한 번에 n개의 바이트를 처리할 수 있다면, 송신 호스트는 n개의 바이트를 넘지 않는 선에서 송신해야 한다. TCP 헤더의 윈도우 필드에는 수신 호스트가 한 번에 처리할 수 있는 수신 윈도우 크기가 명시되어 있다.</p>
<p>Stop-and-Wait ARQ를 사용하면 별도의 흐름 제어가 필요없지만, 파이프라이닝 기반의 Go-Back-N ARQ와 Selective Repeat ARQ에서는 흐름 제어가 필요하다. TCP의 흐름 제어는 슬라이딩 윈도우를 사용하여 실현된다. </p>
<h3 id="슬라이딩-윈도우">슬라이딩 윈도우</h3>
<ul>
<li><strong>윈도우(window):</strong> 송신 호스트가 파이프라이닝할 수 있는 최대량으로, 윈도우의 크기만큼은 확인 응답을 받지 않고도 한 번에 전송 가능하다는 의미이다.</li>
</ul>
<p>수신 호스트로부터 1, 2, 3, …,k 번째 세그먼트에 대한 ACK 응답을 받았다면, 윈도우가 점차 오른쪽으로 미끄러지듯이 움직이며 그 다음 세그먼트를 전송하게 된다.</p>
<blockquote>
<p>송신 측의 윈도우도 있다. 따라서 수신 호스트는 TCP 헤더를 통해 송신 호스트에게 자신이 받을 수 있는 데이터의 양을 미리 알려준다.</p>
</blockquote>
<h2 id="혼잡-제어">혼잡 제어</h2>
<ul>
<li><strong>혼잡(congestion):</strong> 많은 트래픽으로 인해 패킷의 처리 속도가 느려지거나 유실될 우려가 있는 네트워크 상황</li>
</ul>
<p>TCP의 혼잡 제어에서 송신 호스트는 네트워크의 혼잡한 정도에 맞춰 유동적으로 전송량을 조절한다. 흐름 제어에서 수신 호스트가 헤더로 알려주는 반면, 혼잡 제어에서는 송신 호스트가 알아서 혼잡 윈도우를 직접 계산하여 알아내야 한다. 따라서 흐름 제어의 주체가 수신 호스트에, 혼잡 제어의 주체는 송신 호스트에 가깝다고 할 수 있다.</p>
<h3 id="혼잡-제어-알고리즘">혼잡 제어 알고리즘</h3>
<p>그렇다면 송신 호스트는 혼잡 윈도우의 크기를 어떻게 결정할 수 있을까? 이를 결정하는 대표적인 알고리즘은 AIMD(Additive Increase/Multiplicative Decrease, 합으로 증가/곱으로 감소)가 있다. AIMD는 혼잡이 감지되지 않으면 혼잡 윈도우를 RTT(Round Trip Time)마다 1씩 선형적으로 증가시키고, 혼잡이 감지되면 혼잡 윈도우를 절반으로 감소시키는 동작을 반복한다.</p>
<blockquote>
<p><strong>RTT</strong>
메시지를 전송한 뒤 그에 대한 답변을 받는 데까지 걸리는 시간</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/one1_programmer/post/76f278ec-41d9-4ca4-83e8-5b9a719f3694/image.png" width="550"/>

<p>[출처: GeeksforGeeks]</p>
<p>더 나아가, 송신 호스트는 이 ‘혼잡’ 상황을 어떻게 인지할 수 있을까? 그에 대한 답은 이전에 설명한 ‘오류 제어’의 오류 판단과 동일하다. 즉, ‘중복된 ACK 세그먼트를 수신하는 상황’과  ‘타임아웃이 발생하는 상황&#39;이다.</p>
<blockquote>
<p><strong>참고자료</strong>
<a href="https://www.youtube.com/watch?v=zEZbCULOQdY">https://www.youtube.com/watch?v=zEZbCULOQdY</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MongoDB] mongoose DB 연결 시 connect ECONNREFUSED ::1:27017 에러 해결 (feat. IPv6) ]]></title>
            <link>https://velog.io/@one1_programmer/MongoDB-mongoose-DB-%EC%97%B0%EA%B2%B0-%EC%8B%9C-connect-ECONNREFUSED-127017-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0-feat.-IPv6</link>
            <guid>https://velog.io/@one1_programmer/MongoDB-mongoose-DB-%EC%97%B0%EA%B2%B0-%EC%8B%9C-connect-ECONNREFUSED-127017-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0-feat.-IPv6</guid>
            <pubDate>Sun, 07 Dec 2025 11:42:37 GMT</pubDate>
            <description><![CDATA[<p>로컬 환경에서 mongoose로 MongoDB 데이터 베이스 서버 연결이 아래와 같은 문구와 함께 실패했다. </p>
<p><code>connect ECONNREFUSED ::1:27017</code></p>
<p>이를 해결하기 위해 구글링해서 바로 해결할 수 있었다.</p>
<p>환경변수 설정 파일에 아래와 같이 데이터 베이스 URI를 변경했다.</p>
<pre><code class="language-bash"># localhost -&gt; 127.0.0.1
MONGODB_URI=mongodb://127.0.0.1:27017/vuisiness</code></pre>
<p>하지만 여기서 의문이 생겼다.
아니 127.0.0.1이 localhost의 루프백 주소니까 정상 동작해야 하는거 아닌가?</p>
<p>에러 메시지를 다시 보자! 
<code>::1:27017</code> 부분의 <code>::1</code>는 <code>127.0.0.1</code>랑 확실히 다른데..</p>
<p>혹시나 IPv6를 나타내는 것이 아닐까 구글링을 다시 해보니 아니나 다를까 IPv6의 루프백 주소인 것을 알게 되었다.</p>
<p><a href="https://stackoverflow.com/questions/40189084/what-is-ipv6-for-localhost-and-0-0-0-0">스택오버플로우 - What is IPv6 for localhost and 0.0.0.0?</a></p>
<p>그렇다면 mongoose는 왜 localhost를 IPv4로 해석하지 못하는가?</p>
<blockquote>
<p><em>This is the minimum needed to connect the myapp database running locally on the default port (27017). For local MongoDB databases, we recommend using 127.0.0.1 instead of localhost. That is because Node.js 18 and up prefer IPv6 addresses, which means, on many machines, <strong>Node.js will resolve localhost to the IPv6 address ::1 and Mongoose will be unable to connect, unless the mongodb instance is running with ipv6 enabled.</strong></em>
[출처: mongoose 공식문서]</p>
</blockquote>
<p>현재 환경은 Next.js로 결국 node 환경에서 돌아가고 있다.</p>
<p>이를 해결하기 위해서는 아래 두 가지 방법이 있다.</p>
<ol>
<li>명시적 IPv4 명시 (위 방법과 동일)</li>
<li>mongoose 연결 <code>family</code> 옵션 명시</li>
</ol>
<p>2번 방법은 mongoose에서 DB 연결 시 직접적으로 IP 패밀리를 명시할 수 있게 하는 설정이다.</p>
<blockquote>
<p><strong>family 옵션</strong>
Whether to connect using IPv4 or IPv6. This option passed to Node.js&#39; dns.lookup() function. If you don&#39;t specify this option, the MongoDB driver will try IPv6 first and then IPv4 if IPv6 fails. If your mongoose.connect(uri) call takes a long time, try mongoose.connect(uri, { family: 4 })
[출처: mongoose 공식문서]</p>
</blockquote>
<pre><code class="language-javascript">import mongoose from &quot;mongoose&quot;;

const connectDB = async () =&gt; {
  if (mongoose.connections[0].readyState) {
    return;
  }
  await mongoose.connect(process.env.MONGODB_URI, {
    family: 4,
  });
};

export default connectDB;
</code></pre>
]]></description>
        </item>
    </channel>
</rss>