<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>feels_bot_man.log</title>
        <link>https://velog.io/</link>
        <description>안드로이드 페페</description>
        <lastBuildDate>Wed, 23 Apr 2025 06:56:13 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>feels_bot_man.log</title>
            <url>https://velog.velcdn.com/images/feels_bot_man/profile/807d2bed-c0ba-415f-b143-e061f34eabd0/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. feels_bot_man.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/feels_bot_man" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[PM2 자동 시작 설정 방법]]></title>
            <link>https://velog.io/@feels_bot_man/PM2-%EC%9E%90%EB%8F%99-%EC%8B%9C%EC%9E%91-%EC%84%A4%EC%A0%95-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@feels_bot_man/PM2-%EC%9E%90%EB%8F%99-%EC%8B%9C%EC%9E%91-%EC%84%A4%EC%A0%95-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Wed, 23 Apr 2025 06:56:13 GMT</pubDate>
            <description><![CDATA[<p>기본적으로 <strong>PM2는 AWS EC2 인스턴스를 재시작하면 자동으로 실행되지 않는다.</strong><br>EC2가 재부팅되면, PM2 프로세스는 사라지고 수동으로 다시 실행해야 한다.</p>
<hr>
<h3 id="✅-pm2-자동-시작-설정-방법">✅ PM2 자동 시작 설정 방법</h3>
<p>PM2에서 제공하는 <code>startup</code> 명령어로 <strong>부팅 시 자동 실행</strong> 설정이 가능하다.</p>
<pre><code class="language-bash">pm2 startup</code></pre>
<p>이 명령을 실행하면 아래와 같은 출력이 나올 것이다:</p>
<pre><code>[PM2] Init system found: systemd
[PM2] To setup the Startup Script, copy/paste the following command:
sudo env PATH=$PATH:/home/ubuntu/.nvm/versions/node/v18.15.0/bin pm2 startup systemd -u ubuntu --hp /home/ubuntu</code></pre><p>출력된 이 명령어를 <strong>복사해서 그대로 실행</strong>하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/feels_bot_man/post/d1993e6c-36d3-407d-8703-2ecc5122f9dc/image.png" alt=""></p>
<hr>
<h3 id="✅-설정-마무리-현재-상태-저장">✅ 설정 마무리 (현재 상태 저장)</h3>
<p>현재 실행 중인 앱들을 부팅 시에도 실행되도록 저장하려면:</p>
<pre><code class="language-bash">pm2 save</code></pre>
<hr>
<h3 id="🔁-요약">🔁 요약</h3>
<ol>
<li>PM2 부팅 자동 실행 스크립트 등록:<pre><code class="language-bash">pm2 startup
sudo env ... # 위 출력된 명령어 실행</code></pre>
</li>
<li>현재 상태 저장:<pre><code class="language-bash">pm2 save</code></pre>
</li>
</ol>
<p>이제 EC2가 재시작되어도 PM2가 자동으로 실행되고, 앱도 같이 실행된다 🙌</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Google JSON Style Guide]]></title>
            <link>https://velog.io/@feels_bot_man/Google-JSON-Style-Guide</link>
            <guid>https://velog.io/@feels_bot_man/Google-JSON-Style-Guide</guid>
            <pubDate>Mon, 21 Apr 2025 06:29:06 GMT</pubDate>
            <description><![CDATA[<p><a href="https://google.github.io/styleguide/jsoncstyleguide.xml">https://google.github.io/styleguide/jsoncstyleguide.xml</a></p>
<p>Google의 JSON Style Guide는 JSON 데이터의 일관성과 가독성을 높이기 위한 규칙과 권장사항을 제시한다. 주요 요점을 아래와 같이 정리할 수 있다.</p>
<pre><code class="language-json">{
  &quot;apiVersion&quot;: &quot;...&quot;,
  &quot;context&quot;: &quot;...&quot;,
  &quot;id&quot;: &quot;...&quot;,
  &quot;method&quot;: &quot;...&quot;,
  &quot;params&quot;: { ... },
  &quot;data&quot;: { ... },
  &quot;error&quot;: { ... }
}</code></pre>
<ul>
<li>최상위 필드들<ul>
<li>apiVersion:    이 API의 버전 (&quot;v1&quot; 등)</li>
<li>context:    이 요청이 어떤 컨텍스트에서 발생했는지 설명 (예: 트래킹 ID)</li>
<li>id:    요청 또는 응답의 고유 식별자</li>
<li>method:    호출할 메서드 이름 (RPC 스타일일 때 사용)</li>
<li>params:    호출할 메서드에 전달할 인자들 (파라미터 객체)</li>
<li>data: 성공 시 응답</li>
<li>error: 실패 시 응답</li>
</ul>
</li>
</ul>
<p>data와 error는 동시에 포함되면 안 되고, 둘 중 하나만 있어야 한다.</p>
<h3 id="data-객체-성공-시-응답">data 객체 (성공 시 응답)</h3>
<p>실제로 유저가 받고자 하는 데이터를 포함한다.
대부분의 유용한 정보는 이 안에 들어간다.</p>
<ul>
<li>kind:    리소스 종류 (예: &quot;calendar#event&quot;)</li>
<li>fields:    필드 필터링용 (필요한 필드만 지정할 때 사용)</li>
<li>etag:    데이터의 버전(캐싱 용도)</li>
<li>id:    데이터 자체의 ID</li>
<li>lang:    언어 코드 (&quot;en&quot;, &quot;ko&quot;)</li>
<li>updated:    마지막 수정 날짜 (RFC 3339 형식)</li>
<li>deleted:    삭제 여부 (boolean)</li>
<li>currentItemCount, totalItems, itemsPerPage, startIndex, pageIndex, totalPages:    페이지네이션 관련 정보</li>
<li>pageLinkTemplate:    페이지 링크 형식 템플릿</li>
<li>next, nextLink, previous, previousLink, self, selfLink, edit, editLink:    링크 객체들 (REST 스타일 탐색을 위한 링크)</li>
<li>items:    실제 데이터 배열 (예: 게시글 리스트, 유저 리스트 등)</li>
</ul>
<h3 id="error-객체-오류-발생-시-응답">error 객체 (오류 발생 시 응답)</h3>
<p>요청이 실패했을 때 포함된다. data는 이 경우 생략된다.</p>
<ul>
<li>code:    에러 코드 (HTTP 상태 코드와 유사)</li>
<li>message:    에러 메시지 (사용자 또는 개발자용)</li>
<li>errors:    에러 상세 항목들 (복수 가능)</li>
<li>errors[].domain:    에러가 발생한 도메인 (예: &quot;global&quot; 등)</li>
<li>errors[].reason:    발생 이유 (예: &quot;notFound&quot; 등)</li>
<li>errors[].message:    구체적 설명</li>
<li>errors[].location:    어떤 파라미터에서 에러가 났는지</li>
<li>errors[].locationType:    예: &quot;parameter&quot;</li>
<li>errors[].extendedHelp:    추가 설명 URL</li>
<li>errors[].sendReport:    버그 리포트 제출 링크</li>
</ul>
<h2 id="결론">결론</h2>
<p>이 가이드는 API 설계자와 개발자들이 상호운용성, 유지보수성, 일관성을 확보하는 데 큰 도움이 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RESTful API 설계 규칙]]></title>
            <link>https://velog.io/@feels_bot_man/RESTful-API-%EC%84%A4%EA%B3%84-%EA%B7%9C%EC%B9%99</link>
            <guid>https://velog.io/@feels_bot_man/RESTful-API-%EC%84%A4%EA%B3%84-%EA%B7%9C%EC%B9%99</guid>
            <pubDate>Mon, 14 Apr 2025 14:28:46 GMT</pubDate>
            <description><![CDATA[<p>REST(Representational State Transfer) 기반이란, 웹 서비스와 API(Application Programming Interface)를 설계하는 아키텍처 스타일로, REST의 주요 원칙은 다음과 같다.</p>
<ul>
<li>클라이언트-서버 구조: 클라이언트와 서버는 서로 독립적이며, 클라이언트는 서버로부터 데이터를 요청하고 서버는 해당 요청에 응답하는 역할을 한다.</li>
<li>무상태성(Stateless): 서버는 각 요청을 독립적으로 처리하며, 이전 요청의 상태를 저장하지 않는다. 모든 요청은 필요한 모든 정보를 포함해야 한다.</li>
<li>캐시 처리 가능(Cachability): 클라이언트는 서버 응답을 캐시할 수 있으며, 이를 통해 성능을 최적화하고 서버에 대한 부하를 줄일 수 있다.</li>
<li>계층화 시스템(Layered System): 클라이언트는 중간 서버(게이트웨이, 프록시 등)를 통해 서버와 상호 작용할 수 있으며, 각 계층은 독립적으로 설계된다.</li>
<li>인터페이스 일관성(Uniform Interface): 리소스는 통일된 방식으로 접근되며, 특정 URL이 해당 리소스를 나타낸다.</li>
<li>리소스 기반 아키텍처: URL은 리소스를 나타내며, 리소스는 HTTP 메서드(주로 GET, POST, PUT, DELETE 등)에 따라 CRUD 작업을 수행한다.</li>
</ul>
<p>REST API: 이는 REST(Representational State Transfer) 원칙을 따르는 API를 가리킨다. API가 완전히 REST 원칙을 준수하지 않아도 이 용어를 사용할 수 있다.</p>
<p>RESTful API: 이 용어는 REST 원칙을 더 충실하게 따르는 API를 의미한다. 즉, API가 엔드포인트와 자원을 의도된 데이터와 작업에 맞게 설계하고, HTTP 메소드(GET, POST, PUT, DELETE)를 적절히 활용하여 표준적인 방식으로 자원을 표현하는 API를 가리킨다.</p>
<h2 id="api-엔드포인트-설계">API 엔드포인트 설계</h2>
<p><img src="https://velog.velcdn.com/images/feels_bot_man/post/1b98a6a2-a4c3-439f-a264-c0041e1533ad/image.png" alt="">
<a href="https://velog.io/@mover32/Rest-api-%EC%84%A4%EA%B3%84-%EB%AA%A8%EB%B2%94%EC%82%AC%EB%A1%80">이미지 출처</a></p>
<p>API 엔드포인트는 직관적이고 RESTful한 방식으로 구성하는 게 중요하다. RESTful API는 <strong>자원(Resource)</strong>을 중심으로 설계된다. 자원은 URL로 표현되며, 각 자원에 대해 HTTP 메서드를 사용해 CRUD(Create, Read, Update, Delete) 작업을 수행한다.</p>
<ul>
<li><h3 id="엔드포인트-설계-시-고려해야-할-요소">엔드포인트 설계 시 고려해야 할 요소</h3>
</li>
</ul>
<p>엔드포인트를 설계할 때는 다음과 같은 요소를 고려해야 한다.</p>
<ul>
<li>명확한 URL 구조 : URL은 간결하고 직관적이어야 한다. 어떤 자원과 관계된 것인지 URL만으로 유추할 수 있으면 좋다. 직관적인가 의심이 들 경우 AI의 의견을 참고해보는 것도 도움이 될 것이다.</li>
<li>HTTP 메서드의 적절한 사용 : 각 메서드는 특정 작업을 수행하기 위해 설계되었다. <code>GET</code>은 데이터를 가져오고, <code>POST</code>는 데이터를 생성하며, <code>PUT</code>은 데이터를 업데이트하고, <code>DELETE</code>는 데이터를 삭제한다.</li>
<li>상태 코드의 활용 : 클라이언트에게 요청의 결과를 알려주기 위해 적절한 HTTP 상태 코드를 반환해야 한다. 예를 들어, 요청이 성공하면 200 OK, 자원이 생성되면 201 Created, 요청이 잘못되면 400 Bad Request를 반환할 수 있다.</li>
</ul>
<h3 id="restful-api-설계-규칙">RESTful API 설계 규칙</h3>
<ol>
<li><p>리소스는 복수형 명사
리소스는 복수형 명사로 작성한다. 예를 들어, users, orders, products 등과 같이 작성한다.
예시:
GET /users → 모든 사용자 조회
POST /orders → 주문 생성</p>
</li>
<li><p>자원의 고유 ID는 URL에 포함
특정 자원에 접근할 때는 해당 자원의 고유 ID를 URL에 포함한다.
예시:
GET /users/123 → ID가 123인 사용자 조회
PUT /products/45 → ID가 45인 상품 수정</p>
</li>
<li><p>계층적 관계 표현
자원 간의 계층적 관계가 있을 경우 URL에 중첩해 표현한다. 자원 내의 하위 자원을 나타낼 때 유용하다.
예시:
GET /users/123/orders → ID가 123인 사용자의 주문 목록 조회
POST /users/123/orders → ID가 123인 사용자에게 주문 생성</p>
</li>
<li><p>HTTP 메서드를 활용한 동작 정의
HTTP 메서드(GET, POST, PUT, DELETE)를 통해 동작을 정의하므로, URL에는 동작을 나타내는 동사를 포함하지 않는다.
GET → 조회 (Read)
POST → 생성 (Create)
PUT 또는 PATCH → 수정 (Update)
DELETE → 삭제 (Delete)
예시:
DELETE /users/123 → ID가 123인 사용자 삭제
PUT /products/45 → ID가 45인 상품 정보 수정</p>
</li>
<li><p>URL에는 소문자만 사용
URL은 소문자만 사용하고, 단어는 하이픈(-)으로 구분한다. 대소문자 혼합은 일관성을 해칠 수 있다.
예시:
/user-profiles/123 (O)
/UserProfiles/123 (X)</p>
</li>
<li><p>필터링, 정렬, 페이징은 쿼리 파라미터로
필터링, 정렬, 페이징과 같은 추가 조건은 URL 경로 대신 쿼리 파라미터를 사용하여 표현한다.
예시:
GET /products?category=books&amp;sort=price_asc&amp;page=2 → 책 카테고리의 제품을 가격 오름차순으로 정렬하고, 2페이지 결과 조회</p>
</li>
<li><p>액션은 경로가 아닌 쿼리 파라미터로 표현
일반적인 CRUD 작업이 아닌 특별한 동작이 필요한 경우, URL 경로 대신 쿼리 파라미터나 추가 경로로 표시한다.
예시:
POST /users/123/activate → ID가 123인 사용자를 활성화
POST /orders/45/cancel → 주문을 취소</p>
</li>
<li><p>경로 마지막에 &#39;/&#39;를 넣지 않는다.</p>
</li>
</ol>
<h3 id="자동화-툴">자동화 툴</h3>
<p>향후에는 API문서화와 테스트를 자동화하여 관리하고 싶어서 Apidog와 같은 툴을 이용해볼까 한다.
<a href="https://apidog.com/kr/blog/rest-api-url-best-practices-examples-2/">Apidog의 REST API URL 모범 사례</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JWT와 CSRF 토큰, 세션 쿠키는 동시에 필요한 것일까?]]></title>
            <link>https://velog.io/@feels_bot_man/JWT%EC%99%80-CSRF-%ED%86%A0%ED%81%B0-%EC%84%B8%EC%85%98-%EC%BF%A0%ED%82%A4%EB%8A%94-%EB%8F%99%EC%8B%9C%EC%97%90-%ED%95%84%EC%9A%94%ED%95%9C-%EA%B2%83%EC%9D%BC%EA%B9%8C</link>
            <guid>https://velog.io/@feels_bot_man/JWT%EC%99%80-CSRF-%ED%86%A0%ED%81%B0-%EC%84%B8%EC%85%98-%EC%BF%A0%ED%82%A4%EB%8A%94-%EB%8F%99%EC%8B%9C%EC%97%90-%ED%95%84%EC%9A%94%ED%95%9C-%EA%B2%83%EC%9D%BC%EA%B9%8C</guid>
            <pubDate>Sun, 06 Apr 2025 19:13:36 GMT</pubDate>
            <description><![CDATA[<h2 id="기본-개념-정리">기본 개념 정리</h2>
<ol>
<li>JWT (JSON Web Token)</li>
</ol>
<ul>
<li>클라이언트가 로그인하면 서버는 JWT를 발급</li>
<li>클라이언트는 이 JWT를 Authorization 헤더나 쿠키에 담아 보냄</li>
<li>서버는 이 토큰을 검증해서 인증 처리</li>
<li>Stateless 방식 (서버에 세션 저장 X)</li>
</ul>
<ol start="2">
<li>세션 쿠키 (Session Cookie)</li>
</ol>
<ul>
<li>서버가 세션을 만들고, 식별용 세션 ID를 클라이언트에 쿠키로 저장</li>
<li>클라이언트는 요청마다 세션 쿠키를 보냄</li>
<li>서버는 세션 ID로 사용자 상태를 찾아서 인증</li>
<li>Stateful 방식 (세션 상태를 서버에 저장)</li>
</ul>
<ol start="3">
<li>CSRF 토큰</li>
</ol>
<ul>
<li>주로 쿠키 기반 인증에서 사용</li>
<li>이유: 쿠키는 자동으로 브라우저가 요청에 포함하므로, 악의적 요청도 세션이 살아있으면 처리될 수 있음 → 그래서 요청이 의도된 것인지 확인하려고 CSRF 토큰 사용</li>
</ul>
<h2 id="🤔-그렇다면-jwt--세션--csrf-토큰을-모두-써야-할까">🤔 그렇다면 <strong>JWT + 세션 + CSRF 토큰을 모두 써야 할까?</strong></h2>
<h3 id="🔹-상황-1-jwt를-authorization-헤더로-보낸다면">🔹 상황 1: <strong>JWT를 Authorization 헤더로 보낸다면</strong></h3>
<ul>
<li>✅ CSRF 토큰은 <strong>필요 없음</strong></li>
<li>❌ 세션도 필요 없음 (완전히 stateless)</li>
<li>JWT를 로컬스토리지에라도 보관? XSS 공격에 취약함</li>
</ul>
<h3 id="🔹-상황-2-jwt를-httponly-쿠키에-저장한다면">🔹 상황 2: <strong>JWT를 HttpOnly 쿠키에 저장한다면</strong></h3>
<ul>
<li>✅ <strong>CSRF 토큰 필요</strong><ul>
<li>이유: 악의적 사이트가 사용자의 쿠키로 요청을 보낼 수 있음</li>
</ul>
</li>
<li>⚠️ 동시에 세션을 굳이 쓸 필요는 없음 (JWT 자체가 인증 상태를 저장함)</li>
<li>SameSite=Strict와 HttpOnly 쿠키를 쓰는 경우, CSRF 토큰은 optional이지만, admin 등 민감 영역엔 추가하기도 함</li>
</ul>
<h3 id="🔹-상황-3-기존-세션-기반-인증을-쓰는-경우-express-session-등">🔹 상황 3: <strong>기존 세션 기반 인증을 쓰는 경우 (express-session 등)</strong></h3>
<ul>
<li>✅ CSRF 토큰 필요</li>
<li>❌ JWT는 필요 없음 (중복 인증 시스템)</li>
</ul>
<hr>
<h2 id="👉-추천-조합">👉 추천 조합</h2>
<ul>
<li><p>✅ 간편하고 RESTful하게 하고 싶다면:<br><strong>JWT + Authorization 헤더</strong> 방식 → CSRF 걱정 없음</p>
</li>
<li><p>✅ 보안 우선 + 브라우저 자동처리 + 쿠키 기반 원한다면:<br><strong>JWT in HttpOnly Cookie + CSRF Token</strong> → CSRF 보호 필수</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JWT (JSON Web Token) - 점진적 토큰 갱신]]></title>
            <link>https://velog.io/@feels_bot_man/JWT-JSON-Web-Token-%EC%A0%90%EC%A7%84%EC%A0%81-%ED%86%A0%ED%81%B0-%EA%B0%B1%EC%8B%A0</link>
            <guid>https://velog.io/@feels_bot_man/JWT-JSON-Web-Token-%EC%A0%90%EC%A7%84%EC%A0%81-%ED%86%A0%ED%81%B0-%EA%B0%B1%EC%8B%A0</guid>
            <pubDate>Fri, 07 Mar 2025 10:11:12 GMT</pubDate>
            <description><![CDATA[<p>점진적 토큰 갱신(Incremental Token Renewal)은 보안성과 사용자 경험을 고려하여 <strong>액세스 토큰(Access Token)을 갱신하는 방식</strong>이다.<br>기본적으로 <strong>액세스 토큰은 짧은 유효 기간</strong>, <strong>리프레시 토큰은 긴 유효 기간</strong>을 가지며, 점진적 갱신을 통해 리프레시 토큰의 보안성을 유지하면서 인증 상태를 지속한다.</p>
<hr>
<h3 id="점진적-토큰-갱신의-핵심-원칙"><strong>점진적 토큰 갱신의 핵심 원칙</strong></h3>
<ol>
<li><p><strong>액세스 토큰을 자주 갱신하되, 리프레시 토큰 갱신은 최소화</strong>  </p>
<ul>
<li>액세스 토큰이 만료될 때마다 새로운 액세스 토큰을 발급.  </li>
<li>리프레시 토큰은 일정 조건에서만 갱신하여 탈취 가능성을 줄임.</li>
</ul>
</li>
<li><p><strong>리프레시 토큰의 롤링(순환) 방식 적용</strong>  </p>
<ul>
<li>리프레시 토큰을 사용할 때마다 새로운 리프레시 토큰을 발급하고, 기존 토큰을 폐기.  </li>
<li>이를 통해 탈취된 리프레시 토큰의 재사용 공격을 방지.</li>
</ul>
</li>
<li><p><strong>사용자가 장기간 로그인 상태를 유지하면 리프레시 토큰도 주기적으로 갱신</strong>  </p>
<ul>
<li>일정 기간 동안 리프레시 토큰이 사용되지 않으면 폐기.</li>
</ul>
</li>
</ol>
<hr>
<h3 id="점진적-토큰-갱신-구현-방식"><strong>점진적 토큰 갱신 구현 방식</strong></h3>
<h4 id="1-기본-토큰-발급"><strong>1. 기본 토큰 발급</strong></h4>
<p>사용자가 로그인하면 <strong>액세스 토큰과 리프레시 토큰을 발급</strong>한다.</p>
<pre><code class="language-js">const accessToken = jwt.sign({ userId }, process.env.ACCESS_SECRET, {
  expiresIn: &quot;15m&quot;, // 짧은 유효기간
});

const refreshToken = jwt.sign({ userId }, process.env.REFRESH_SECRET, {
  expiresIn: &quot;7d&quot;, // 긴 유효기간
});

// 액세스 토큰은 클라이언트가 저장 (ex: 메모리, 상태관리)
// 리프레시 토큰은 쿠키에 저장 (HttpOnly, Secure 설정)
res.cookie(&quot;refreshToken&quot;, refreshToken, {
  httpOnly: true,
  secure: process.env.NODE_ENV === &quot;production&quot;,
  sameSite: &quot;Strict&quot;,
});</code></pre>
<hr>
<h4 id="2-액세스-토큰-자동-갱신-프론트엔드"><strong>2. 액세스 토큰 자동 갱신 (프론트엔드)</strong></h4>
<ul>
<li>액세스 토큰이 만료되기 <strong>약 1~2분 전</strong>에 새로운 액세스 토큰을 요청.</li>
<li><code>refreshToken</code>을 이용하여 <code>POST /auth/refresh</code> API를 호출.</li>
</ul>
<pre><code class="language-js">async function refreshAccessToken() {
  const response = await fetch(&quot;/auth/refresh&quot;, { method: &quot;POST&quot;, credentials: &quot;include&quot; });
  if (response.ok) {
    const data = await response.json();
    return data.accessToken;
  }
  return null;
}</code></pre>
<p>✅ <strong>효과</strong>: 사용자는 로그아웃 없이 지속적으로 로그인 유지 가능.</p>
<hr>
<h4 id="3-리프레시-토큰-갱신-조건"><strong>3. 리프레시 토큰 갱신 조건</strong></h4>
<p>리프레시 토큰을 <strong>매번 갱신하지 않고</strong>, 특정 조건에서만 교체한다.</p>
<p><strong>리프레시 토큰을 갱신하는 조건</strong></p>
<ul>
<li>① 액세스 토큰을 갱신할 때, 리프레시 토큰이 <strong>30% 이상의 유효 기간을 사용한 경우</strong><br>예) <code>7일짜리 토큰</code>이라면 <strong>2일 이상 사용되었을 때만 새 리프레시 토큰 발급</strong>  </li>
<li>② 로그인 유지 시간이 <code>X일 이상</code> 경과한 경우  </li>
</ul>
<pre><code class="language-js">function shouldRefreshToken(iat, exp) {
  const now = Math.floor(Date.now() / 1000);
  const usedTime = now - iat;
  const totalLifetime = exp - iat;

  return usedTime / totalLifetime &gt; 0.3; // 30% 이상 사용되었으면 갱신
}</code></pre>
<p>✅ <strong>효과</strong>: 리프레시 토큰을 자주 갱신하지 않아 보안성에 도움(리프레시 토큰을 자주 갱신하면 새로운 토큰이 클라이언트와 서버 간에 빈번하게 전송됨 → 네트워크에서 탈취될 가능성이 증가.)</p>
<hr>
<h4 id="4-리프레시-토큰을-사용한-액세스-토큰-갱신-api"><strong>4. 리프레시 토큰을 사용한 액세스 토큰 갱신 API</strong></h4>
<pre><code class="language-js">app.post(&quot;/auth/refresh&quot;, async (req, res) =&gt; {
  const refreshToken = req.cookies.refreshToken;
  if (!refreshToken) return res.status(401).json({ message: &quot;Unauthorized&quot; });

  try {
    const payload = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
    const newAccessToken = jwt.sign({ userId: payload.userId }, process.env.ACCESS_SECRET, { expiresIn: &quot;15m&quot; });

    // 리프레시 토큰이 30% 이상 사용되었으면 새로 발급
    if (shouldRefreshToken(payload.iat, payload.exp)) {
      const newRefreshToken = jwt.sign({ userId: payload.userId }, process.env.REFRESH_SECRET, { expiresIn: &quot;7d&quot; });

      res.cookie(&quot;refreshToken&quot;, newRefreshToken, {
        httpOnly: true,
        secure: process.env.NODE_ENV === &quot;production&quot;,
        sameSite: &quot;Strict&quot;,
      });
    }

    res.json({ accessToken: newAccessToken });
  } catch (err) {
    return res.status(403).json({ message: &quot;Invalid refresh token&quot; });
  }
});</code></pre>
<p>✅ <strong>효과</strong>: 리프레시 토큰의 사용량을 체크하고 필요할 때만 갱신하여 보안 유지.</p>
<hr>
<h4 id="5-로그아웃-및-리프레시-토큰-무효화"><strong>5. 로그아웃 및 리프레시 토큰 무효화</strong></h4>
<p>사용자가 로그아웃하면 리프레시 토큰을 즉시 삭제해야 한다.</p>
<pre><code class="language-js">app.post(&quot;/auth/logout&quot;, (req, res) =&gt; {
  res.clearCookie(&quot;refreshToken&quot;);
  res.json({ message: &quot;Logged out&quot; });
});</code></pre>
<p>✅ <strong>효과</strong>: 탈취된 리프레시 토큰을 이용한 불법 접근 차단.</p>
<hr>
<h3 id="정리-점진적-토큰-갱신의-흐름"><strong>정리: 점진적 토큰 갱신의 흐름</strong></h3>
<ol>
<li>로그인 시 <strong>액세스 토큰(15분)과 리프레시 토큰(7일) 발급</strong>  </li>
<li>액세스 토큰 만료 전에 <code>POST /auth/refresh</code> 호출하여 갱신  </li>
<li>리프레시 토큰이 <strong>30% 이상 사용되었을 때만 갱신</strong>  </li>
<li>새로운 리프레시 토큰 발급 시 <strong>기존 토큰은 폐기</strong>  </li>
<li>로그아웃 시 리프레시 토큰 삭제  </li>
</ol>
<p>✅ <strong>장점</strong></p>
<ul>
<li><strong>사용자 경험 개선</strong>: 로그아웃 없이 인증 유지 가능  </li>
<li><strong>보안 강화</strong>: 리프레시 토큰의 수명을 연장하면서도 과도한 재사용 방지  </li>
<li><strong>서버 부하 최소화</strong>: 리프레시 토큰을 자주 갱신하지 않아 데이터베이스 부하 감소  </li>
</ul>
<p>이 방식으로 구현하면 <strong>보안과 사용자 경험을 모두 최적화</strong>할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JWT (JSON Web Token) - 액세스 토큰 관리 비교]]></title>
            <link>https://velog.io/@feels_bot_man/JWT-JSON-Web-Token-%EC%95%A1%EC%84%B8%EC%8A%A4-%ED%86%A0%ED%81%B0-%EA%B4%80%EB%A6%AC-%EB%B9%84%EA%B5%90</link>
            <guid>https://velog.io/@feels_bot_man/JWT-JSON-Web-Token-%EC%95%A1%EC%84%B8%EC%8A%A4-%ED%86%A0%ED%81%B0-%EA%B4%80%EB%A6%AC-%EB%B9%84%EA%B5%90</guid>
            <pubDate>Fri, 07 Mar 2025 10:08:15 GMT</pubDate>
            <description><![CDATA[<p>액세스 토큰을 어디에 저장할지 선택하는 것은 <strong>보안</strong>과 <strong>사용성</strong>을 균형 있게 고려해야 하는 문제다. 각각의 저장 방식에 장단점이 있으므로, <strong>애플리케이션의 특성과 보안 요구사항</strong>에 따라 결정하는 것이 중요하다.</p>
<hr>
<h2 id="1-주요-저장-방법-비교"><strong>1. 주요 저장 방법 비교</strong></h2>
<table>
<thead>
<tr>
<th>저장 위치</th>
<th>보안성</th>
<th>사용성</th>
<th>XSS 위험</th>
<th>CSRF 위험</th>
</tr>
</thead>
<tbody><tr>
<td><strong>메모리 (변수/상태 관리)</strong></td>
<td>✅ 높음</td>
<td>❌ 새로고침 시 사라짐</td>
<td>✅ 없음</td>
<td>✅ 없음</td>
</tr>
<tr>
<td><strong>HTTP-Only 쿠키</strong></td>
<td>✅ 높음</td>
<td>✅ 자동 전송</td>
<td>✅ 없음</td>
<td>❌ 있음 (적절한 보호 필요)</td>
</tr>
<tr>
<td><strong>localStorage</strong></td>
<td>❌ 낮음</td>
<td>✅ 유지됨</td>
<td>❌ 높음</td>
<td>✅ 없음</td>
</tr>
<tr>
<td><strong>sessionStorage</strong></td>
<td>❌ 낮음</td>
<td>❌ 세션 종료 시 사라짐</td>
<td>❌ 높음</td>
<td>✅ 없음</td>
</tr>
</tbody></table>
<p>각 저장 방식의 자세한 설명과 고려할 사항을 살펴보자.</p>
<hr>
<h2 id="2-저장-방식별-상세-분석"><strong>2. 저장 방식별 상세 분석</strong></h2>
<h3 id="1-메모리-저장-javascript-변수-react-상태-관리-등"><strong>1) 메모리 저장 (JavaScript 변수, React 상태 관리 등)</strong></h3>
<p><strong>✅ 장점:</strong></p>
<ul>
<li>XSS 공격에 안전함 (스크립트가 실행되는 동안만 유지됨).</li>
<li>CSRF 공격에도 안전함 (브라우저가 자동으로 전송하지 않음).</li>
</ul>
<p><strong>❌ 단점:</strong></p>
<ul>
<li>새로고침하면 사라짐 → UX가 나빠짐.</li>
<li>페이지가 닫히면 다시 로그인해야 함.</li>
</ul>
<p><strong>🔹 추천 상황:</strong></p>
<ul>
<li>보안이 최우선인 애플리케이션 (ex: 금융 서비스, 관리자 페이지).</li>
<li>사용자 경험보다 보안이 더 중요한 경우.</li>
</ul>
<hr>
<h3 id="2-http-only-쿠키-저장-✅-추천"><strong>2) HTTP-Only 쿠키 저장 (✅ 추천)</strong></h3>
<p>쿠키에 <strong>httpOnly, secure, sameSite=strict</strong> 속성을 추가하면 <strong>XSS 공격에 안전</strong>하면서도 <strong>자동으로 액세스 토큰을 포함하여 요청</strong>할 수 있다.</p>
<pre><code class="language-js">res.cookie(&quot;accessToken&quot;, token, {
  httpOnly: true, 
  secure: process.env.NODE_ENV === &quot;production&quot;, 
  sameSite: &quot;strict&quot;
});</code></pre>
<p><strong>✅ 장점:</strong></p>
<ul>
<li><strong>XSS 공격에 안전</strong> (JavaScript에서 접근 불가).</li>
<li>브라우저가 자동으로 요청에 포함 → 개발이 편리함.</li>
</ul>
<p><strong>❌ 단점:</strong></p>
<ul>
<li><strong>CSRF 공격에 취약</strong> → CSRF 보호가 필요함.<ul>
<li><strong>대책:</strong> <code>SameSite=Strict</code> 설정 또는 CSRF 토큰 추가.</li>
</ul>
</li>
<li>도메인 간 요청 제한 (<code>sameSite=strict</code>로 설정 시 외부 사이트에서 요청 불가).</li>
</ul>
<p><strong>🔹 추천 상황:</strong></p>
<ul>
<li><strong>JWT 기반 인증 시스템에서 보안과 편의성을 함께 고려할 때</strong>.</li>
<li><strong>REST API를 사용하는 SPA/SSR 애플리케이션</strong>.</li>
</ul>
<p>✅ <strong>보안 대책 (CSRF 보호)</strong>:</p>
<ul>
<li>API 요청 시 <strong>CSRF 토큰 사용</strong> (ex: <code>X-CSRF-Token</code> 헤더).</li>
<li><strong>SameSite=Strict 설정</strong>으로 외부 사이트 요청 차단.</li>
</ul>
<hr>
<h3 id="3-localstorage-저장-❌-비추천"><strong>3) localStorage 저장 (❌ 비추천)</strong></h3>
<p>localStorage는 <strong>XSS 공격에 취약</strong>하므로, 보안이 중요한 애플리케이션에서는 사용하지 않는 것이 좋다.</p>
<p><strong>✅ 장점:</strong></p>
<ul>
<li>새로고침해도 유지됨.</li>
<li>CSRF 공격에 안전함.</li>
</ul>
<p><strong>❌ 단점:</strong></p>
<ul>
<li><strong>XSS 공격에 취약</strong> → 악성 스크립트가 localStorage에서 토큰을 가져갈 수 있음.</li>
<li>토큰을 직접 포함해서 요청해야 함 → 번거로움.</li>
</ul>
<p><strong>🔹 추천 상황:</strong></p>
<ul>
<li>보안이 <strong>크게 중요하지 않은 애플리케이션</strong>.</li>
<li>서버가 정적 리소스만 제공하고, API 요청 시만 인증이 필요한 경우.</li>
</ul>
<hr>
<h3 id="4-sessionstorage-저장-❌-비추천"><strong>4) sessionStorage 저장 (❌ 비추천)</strong></h3>
<p>sessionStorage는 <strong>XSS 공격에 취약</strong>하고, 브라우저를 닫으면 사라지므로 보안성이 높다고 보기 어렵다.</p>
<p><strong>✅ 장점:</strong></p>
<ul>
<li>새로고침해도 유지됨.</li>
<li>CSRF 공격에 안전함.</li>
</ul>
<p><strong>❌ 단점:</strong></p>
<ul>
<li><strong>XSS 공격에 취약</strong>.</li>
<li>브라우저를 닫으면 로그아웃됨 → 사용자 경험이 나쁨.</li>
</ul>
<p><strong>🔹 추천 상황:</strong></p>
<ul>
<li>보안이 중요하지 않고, 짧은 세션만 유지하면 되는 경우.</li>
</ul>
<hr>
<h2 id="3-결론-언제-어떤-저장-방식을-선택해야-할까"><strong>3. 결론: 언제 어떤 저장 방식을 선택해야 할까?</strong></h2>
<table>
<thead>
<tr>
<th>저장 방식</th>
<th>추천 사용 사례</th>
</tr>
</thead>
<tbody><tr>
<td><strong>메모리 저장</strong></td>
<td>보안이 최우선인 앱 (ex: 금융, 관리자 페이지)</td>
</tr>
<tr>
<td><strong>HTTP-Only 쿠키 저장</strong></td>
<td>JWT 기반 인증을 사용하는 대부분의 SPA/SSR</td>
</tr>
<tr>
<td><strong>localStorage 저장</strong></td>
<td>보안이 중요하지 않은 앱 (❌ 추천하지 않음)</td>
</tr>
<tr>
<td><strong>sessionStorage 저장</strong></td>
<td>보안이 중요하지 않으며, 세션만 유지하는 앱 (❌ 추천하지 않음)</td>
</tr>
</tbody></table>
<p><strong>✅ 최적의 선택:</strong></p>
<ul>
<li><strong>백엔드 API와 함께 JWT를 사용할 경우, <code>httpOnly</code> 쿠키 저장을 추천</strong>.</li>
<li><strong>CSRF 보호를 추가하면 보안성이 높아짐</strong>.</li>
<li><strong>XSS 위험을 최소화하려면 localStorage 사용을 피하는 것이 좋음</strong>.</li>
</ul>
<hr>
<h2 id="4-추가-보안-대책"><strong>4. 추가 보안 대책</strong></h2>
<ol>
<li><p><strong>CSRF 방어</strong></p>
<ul>
<li><code>SameSite=Strict</code> 또는 <code>X-CSRF-Token</code> 사용.</li>
<li>CSRF 보호가 필요한 경우 <strong>쿠키를 사용하지 않고, 헤더 기반 인증 (Authorization: Bearer)을 고려</strong>.</li>
</ul>
</li>
<li><p><strong>XSS 방어</strong></p>
<ul>
<li><code>Content Security Policy (CSP)</code>를 적용하여 악성 스크립트 실행 차단.</li>
<li>React 등에서 <code>dangerouslySetInnerHTML</code> 같은 XSS 위험 요소 피하기.</li>
</ul>
</li>
<li><p><strong>리프레시 토큰은 어디에 저장할까?</strong></p>
<ul>
<li><strong>httpOnly 쿠키에 저장하는 것이 가장 안전</strong>.</li>
<li>리프레시 토큰은 주로 보안성이 중요한 정보이므로 <strong>JavaScript에서 접근할 수 없게 설정하는 것이 좋음</strong>.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="5-최종-추천-구조"><strong>5. 최종 추천 구조</strong></h2>
<ul>
<li><strong>액세스 토큰</strong>: <code>httpOnly</code> 쿠키에 저장.</li>
<li><strong>리프레시 토큰</strong>: <code>httpOnly</code> 쿠키에 저장.</li>
<li><strong>CSRF 보호</strong>: <code>X-CSRF-Token</code>을 요청 헤더에 포함.</li>
</ul>
<p>이렇게 하면 <strong>XSS, CSRF 공격을 모두 방어하면서도 개발 편의성을 유지할 수 있다</strong>.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JWT (JSON Web Token) - 토큰 갱신과 관리]]></title>
            <link>https://velog.io/@feels_bot_man/JWT-JSON-Web-Token-%ED%86%A0%ED%81%B0-%EA%B0%B1%EC%8B%A0%EA%B3%BC-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@feels_bot_man/JWT-JSON-Web-Token-%ED%86%A0%ED%81%B0-%EA%B0%B1%EC%8B%A0%EA%B3%BC-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Fri, 07 Mar 2025 10:02:37 GMT</pubDate>
            <description><![CDATA[<h2 id="토큰-갱신">토큰 갱신</h2>
<p>JWT 갱신은 주로 액세스 토큰(Access Token)과 리프레시 토큰(Refresh Token)을 사용하여 이루어진다. 이 방식의 주요 특징과 과정은 다음과 같다:</p>
<p>토큰 구조</p>
<ul>
<li>액세스 토큰: 짧은 유효 기간(예: 1시간)을 가지며, 사용자 인증에 사용</li>
<li>리프레시 토큰: 긴 유효 기간(예: 일주일에서 한 달)을 가지며, 새로운 액세스 토큰을 발급받는 데 사용</li>
</ul>
<p>갱신 과정</p>
<ol>
<li>사용자 로그인 시, 서버는 액세스 토큰과 리프레시 토큰을 모두 발급한다.</li>
<li>액세스 토큰이 만료되면, 클라이언트는 리프레시 토큰을 사용하여 새로운 액세스 토큰을 요청한다.</li>
<li>서버는 리프레시 토큰의 유효성을 검증한 후, 새로운 액세스 토큰을 발급한다.</li>
</ol>
<h3 id="리프레시-토큰이-굳이-필요한가">리프레시 토큰이 굳이 필요한가?</h3>
<h4 id="액세스-토큰만-사용하면">액세스 토큰만 사용하면</h4>
<ol>
<li><p>만료된 후 자동 갱신 불가</p>
<ul>
<li>액세스 토큰이 만료되면 사용자는 다시 로그인해야 함.</li>
<li>UX가 나빠지고, 세션이 자주 끊어질 수 있음.</li>
</ul>
</li>
<li><p>긴 만료 시간은 보안 취약점</p>
<ul>
<li>만약 액세스 토큰을 길게 설정하면, 토큰이 탈취되었을 때 위험성이 증가함.</li>
<li>짧게 설정하면 사용자가 자주 로그인해야 하는 불편함이 있음.</li>
</ul>
</li>
<li><p>토큰을 탈취당하면 재사용 가능</p>
<ul>
<li>액세스 토큰만 사용하면 탈취된 토큰을 즉시 차단할 방법이 부족함. 서버는 그 토큰을 무효화할 방법이 없으므로 만료 시간이 지날 때까지 유효한 상태로 남아있음.</li>
<li>액세스 토큰을 차단하려면 블랙리스트를 사용해야 하는데, JWT는 기본적으로 서버에서 상태를 저장하지 않는 방식이므로, 블랙리스트를 유지하면 Stateless한 장점이 사라짐.</li>
</ul>
</li>
</ol>
<h4 id="리프레시-토큰이-필요한-이유">리프레시 토큰이 필요한 이유</h4>
<ol>
<li><p>짧은 만료 시간의 액세스 토큰 사용 가능</p>
<ul>
<li>액세스 토큰을 5~15분 정도의 짧은 시간으로 설정하고, 만료 시 리프레시 토큰을 이용해 자동으로 갱신.</li>
<li>보안이 강화되고, 사용자 경험도 개선됨.</li>
</ul>
</li>
<li><p>리프레시 토큰이 도난당해도 차단 가능</p>
<ul>
<li>리프레시 토큰을 서버에서 블랙리스트 관리할 수 있음.</li>
<li>탈취된 토큰을 무효화할 수 있기 때문에 보안성이 증가함.</li>
</ul>
</li>
</ol>
<h2 id="토큰-탈취를-인지하는-방법">토큰 탈취를 인지하는 방법</h2>
<p>리프레시 토큰이 탈취당했을 때 이를 <strong>인지하고 차단하는 방법</strong>을 고민해야 한다.<br>일반적으로 <strong>리프레시 토큰은 액세스 토큰보다 훨씬 긴 만료 시간</strong>을 가지므로, 보안이 중요하다.  </p>
<hr>
<h3 id="1-리프레시-토큰-탈취를-인지하는-방법"><strong>1. 리프레시 토큰 탈취를 인지하는 방법</strong></h3>
<h4 id="1-기존과-다른-ip-기기에서-요청-감지"><strong>1) 기존과 다른 IP, 기기에서 요청 감지</strong></h4>
<ul>
<li><strong>사용자의 최근 로그인 정보(IP, User-Agent, 기기 정보 등)를 기록</strong>하고, 리프레시 요청이 기존 정보와 다르면 의심해야 함.  </li>
<li>예를 들어:<ul>
<li>사용자가 한국에서 로그인했는데, 갑자기 다른 나라에서 리프레시 요청이 발생한다면 이상 징후로 간주 가능.  </li>
</ul>
</li>
</ul>
<p>✅ <strong>대응 방법</strong>  </p>
<ul>
<li>리프레시 요청이 <strong>기존과 다른 환경</strong>에서 발생하면 추가 인증(예: 이메일 인증, OTP 요청).  </li>
<li>비정상적인 경우 리프레시 토큰을 폐기하고 사용자에게 알림 전송.  </li>
</ul>
<hr>
<h4 id="2-동일한-리프레시-토큰이-여러-곳에서-사용됨"><strong>2) 동일한 리프레시 토큰이 여러 곳에서 사용됨</strong></h4>
<ul>
<li><strong>하나의 리프레시 토큰은 한 번만 사용되도록 설계</strong>하고, 한 번 사용된 토큰이 또 요청되면 탈취 가능성이 높음.  </li>
<li>즉, <strong>리프레시 토큰이 재사용되면 탈취된 것으로 판단 가능</strong>.  </li>
</ul>
<p>✅ <strong>대응 방법</strong>  </p>
<ul>
<li><strong>리프레시 토큰을 한 번 사용하면 즉시 폐기하고, 새 리프레시 토큰 발급</strong>(Rotating Refresh Token 방식).  </li>
<li><strong>이전 리프레시 토큰이 다시 사용되면 로그아웃 처리 및 사용자에게 알림 전송</strong>.  </li>
</ul>
<hr>
<h4 id="3-너무-짧은-시간에-다수의-리프레시-요청-발생"><strong>3) 너무 짧은 시간에 다수의 리프레시 요청 발생</strong></h4>
<ul>
<li>정상적인 사용자는 보통 <strong>액세스 토큰이 만료될 때만 리프레시 요청</strong>을 보냄(예: 15~30분에 한 번).  </li>
<li>하지만, <strong>탈취된 리프레시 토큰을 사용해 공격자가 여러 번 요청</strong>을 보내는 경우 이상 징후로 감지 가능.  </li>
</ul>
<p>✅ <strong>대응 방법</strong>  </p>
<ul>
<li>일정 시간 내 <strong>리프레시 요청 횟수를 제한(rate limiting)</strong>.  </li>
<li>예를 들어, <strong>30분 내에 2번 이상 리프레시 요청이 들어오면 계정 보호 모드 발동</strong>.  </li>
</ul>
<hr>
<h3 id="2-리프레시-토큰-탈취-대응-방법"><strong>2. 리프레시 토큰 탈취 대응 방법</strong></h3>
<h4 id="1-리프레시-토큰을-db에서-관리"><strong>1) 리프레시 토큰을 DB에서 관리</strong></h4>
<ul>
<li>리프레시 토큰을 <strong>데이터베이스에 저장</strong>하고, 탈취 시 폐기할 수 있도록 관리해야 함.  </li>
<li>Redis 같은 <strong>인메모리 저장소</strong>를 활용해 빠르게 블랙리스트 처리 가능.  </li>
</ul>
<p>✅ <strong>대응 방법</strong>  </p>
<ul>
<li>리프레시 토큰을 발급할 때마다 <strong>DB에 저장</strong>하고, 로그아웃하거나 탈취 의심 시 즉시 삭제.  </li>
<li><strong>블랙리스트 시스템 운영</strong> → 탈취된 토큰을 무효화하고 다시 사용되지 않도록 방지.  </li>
</ul>
<hr>
<h4 id="2-리프레시-토큰-rotation-적용-강력-추천"><strong>2) 리프레시 토큰 Rotation 적용 (강력 추천)</strong></h4>
<ul>
<li><strong>기존 리프레시 토큰을 사용하면 즉시 폐기하고 새 토큰을 발급</strong>.  </li>
<li>즉, 한 번 사용한 리프레시 토큰은 다시 사용할 수 없도록 제한.  </li>
<li>만약 <strong>이전 토큰이 다시 요청되면 탈취 가능성이 크므로, 해당 계정을 보호 모드로 전환</strong>.  </li>
</ul>
<p>✅ <strong>대응 방법</strong>  </p>
<ul>
<li><strong>JWT 내부에 jti(unique ID) 포함</strong>하고, 서버에서 관리.  </li>
<li>새 토큰이 발급되면 기존 토큰은 폐기하여 재사용 불가능하도록 함.  </li>
</ul>
<hr>
<h3 id="3-리프레시-토큰을-db에-저장하는-이유">3. 리프레시 토큰을 DB에 저장하는 이유</h3>
<ol>
<li><p>리프레시 토큰은 만료기간이 길다.
JWT 액세스 토큰과 달리, 길면 30일 정도인데 </p>
</li>
<li><p>사용자가 로그아웃할 때 무효화할 수 있음
DB에 저장하면 특정 사용자의 리프레시 토큰을 삭제해서 강제로 로그아웃시키는 게 가능하다</p>
</li>
<li><p>리프레시 토큰의 재사용 감지 가능
보통 &quot;1회용&quot;으로 사용하고, 새로운 리프레시 토큰을 발급할 때 기존 토큰을 폐기하는 식으로 관리하면, 토큰 탈취를 감지할 수 있다.</p>
</li>
</ol>
<h3 id="4-결론-리프레시-토큰-보안-강화를-위해-해야-할-것"><strong>4. 결론: 리프레시 토큰 보안 강화를 위해 해야 할 것</strong></h3>
<p>✔ <strong>IP, 기기 정보 비교</strong> → 다른 환경에서 요청하면 추가 인증 요구<br>✔ <strong>리프레시 토큰 재사용 방지</strong> → 한 번 사용된 토큰이 다시 사용되면 계정 보호<br>✔ <strong>요청 횟수 제한</strong> → 짧은 시간 동안 너무 많은 요청이 들어오면 차단<br>✔ <strong>리프레시 토큰을 서버에서 관리</strong> → 탈취된 토큰을 무효화 가능<br>✔ <strong>Rotation 적용</strong> → 매번 새로운 리프레시 토큰을 발급하여 보안 강화  </p>
<p>위 방식을 조합하면 <strong>리프레시 토큰이 탈취되더라도 빠르게 감지하고 대응할 수 있다</strong>.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JWT (JSON Web Token)]]></title>
            <link>https://velog.io/@feels_bot_man/JWT-JSON-Web-Token</link>
            <guid>https://velog.io/@feels_bot_man/JWT-JSON-Web-Token</guid>
            <pubDate>Fri, 07 Mar 2025 09:27:25 GMT</pubDate>
            <description><![CDATA[<p>JWT는 <strong>JSON 포맷</strong>으로 정보를 안전하게 표현하기 위해 사용되는 <strong>토큰 기반 인증 메커니즘</strong>이다. 서버는 클라이언트의 상태를 기억하지 않고, 토큰만 검증해서 인증 여부를 판단 후 토큰 안에 담긴 정보를 읽고 처리한다. 발급 이후 토큰 내용은 변경할 수 없다.</p>
<p>JWT의 장점</p>
<ul>
<li><strong>자체 포함(Self-contained)</strong>: 모든 정보가 토큰에 포함되어 별도의 데이터베이스 조회가 필요 없는 경우도 있음.</li>
<li><strong>가볍고 빠름</strong>: 단순한 문자열 기반 토큰으로, 빠르게 생성/검증 가능.</li>
<li><strong>확장성</strong>: JSON 형식이므로 다른 시스템과 통합이 용이.</li>
<li><strong>Cross-domain 지원</strong>: 상태를 서버에 유지하지 않으므로 <strong>분산 시스템</strong>에서 유용.</li>
</ul>
<p>JWT의 단점</p>
<ul>
<li><strong>토큰 크기</strong>: 토큰 크기가 비교적 크기 때문에 빈번한 요청에 부담이 될 수 있음.</li>
<li><strong>토큰 만료</strong>: 한 번 발급된 토큰은 만료 전까지는 서버에서 무효화하기 어려움(리프레시 토큰 사용으로 해결 가능).</li>
<li><strong>보안 문제</strong>: 민감한 데이터를 페이로드에 포함하면 안 됨(평문으로 해독 가능). XSS 공격에 취약할 수 있음(브라우저에 저장된 경우).</li>
</ul>
<p>JWT는 세 부분으로 나누어지며, 각 부분은 .으로 구분된다.</p>
<pre><code>&lt;Header&gt;.&lt;Payload&gt;.&lt;Signature&gt;</code></pre><p>Header (헤더)</p>
<ul>
<li>JWT의 타입과 암호화 알고리즘 정보를 담고 있다.</li>
<li>보통 아래와 같은 JSON형식이며, <strong>Base64Url</strong>로 인코딩된다.<pre><code class="language-json">{
&quot;alg&quot;: &quot;HS256&quot;, // 해싱 알고리즘 (예: HMAC SHA-256)
&quot;typ&quot;: &quot;JWT&quot;    // 토큰 타입
}</code></pre>
</li>
</ul>
<p>Payload (페이로드)</p>
<ul>
<li>토큰에 포함된 <strong>데이터(Claims)</strong>가 들어 있는 부분이다.</li>
<li>일반적으로 사용자 정보와 같은 데이터가 포함된다.</li>
<li>페이로드도 <strong>Base64Url</strong>로 인코딩된다. 즉, Base64Url로 디코딩하면, JSON 형태로 사용자 정보나 기타 클레임을 확인할 수 있다.</li>
<li><strong>Claims 종류</strong><ul>
<li><strong>등록된(Registered) Claims</strong>: 표준화된 클레임(필수 아님). 예: <code>iss</code>(발급자), <code>exp</code>(만료 시간), <code>sub</code>(주제), <code>aud</code>(대상).</li>
<li><strong>공개(Public) Claims</strong>: 사용자 정의 가능. 예: <code>userId</code>, <code>role</code>.</li>
<li><strong>비공개(Private) Claims</strong>: 클라이언트와 서버 간 약속된 데이터.<pre><code class="language-json">{
&quot;sub&quot;: &quot;1234567890&quot;,
&quot;name&quot;: &quot;John Doe&quot;,
&quot;admin&quot;: true,
&quot;exp&quot;: 1712179200 // 만료 시간 (Unix Timestamp)
}</code></pre>
</li>
</ul>
</li>
</ul>
<p>Signature (서명)</p>
<ul>
<li>토큰의 무결성을 보장하기 위해 사용(서명을 통해 토큰이 위조되지 않았음을 확인할 수 있다).</li>
<li>헤더와 페이로드를 조합하여 생성:</li>
<li>서명 변조가 불가능한 이유<ul>
<li>비밀 키(Secret Key) 관리: 서버에서만 비밀 키를 알고 있으며, 이를 노출하지 않는 한 서명을 위조할 수 없다.</li>
<li>강력한 암호화 알고리즘: HMAC-SHA256, RS256 등 강력한 해싱 및 암호화 알고리즘을 사용. 키를 알지 못하면 서명 결과를 맞추는 것은 거의 불가능하다.<pre><code>HMACSHA256(
base64UrlEncode(header) + &quot;.&quot; + base64UrlEncode(payload), 
secretKey
)</code></pre></li>
</ul>
</li>
</ul>
<p>서명이 위조되지 않는다고 해서 JWT 자체가 항상 안전한 것은 아니다. 몇 가지 주의사항을 지켜야 한다</p>
<ul>
<li>HttpOnly 쿠키 사용: JavaScript에서 접근 불가</li>
<li>HTTPS 사용: 네트워크에서 탈취 방지</li>
<li>서버는 알고리즘을 명시적으로 지정하고, none을 허용하지 않아야 함: 공격자가 Header의 알고리즘을 none으로 바꿔서 검증을 우회하려는 시도가 있을 수 있음</li>
<li>서명 키 관리: 비밀 키(secret)를 안전하게 저장하고 정기적으로 교체.</li>
<li>짧은 만료 시간 설정: exp를 짧게 설정하고, 리프레시 토큰을 사용. 서버 측에서 &quot;블랙리스트&quot;를 사용하여 강제 로그아웃 처리.</li>
<li>Scope 및 Audience 제한: JWT를 특정 리소스나 용도로만 제한. Payload는 인코딩(Base64Url)만 되어 있고 암호화되지 않으므로 민감한 정보는 JWT에 포함하지 말 것.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Router v7 Framework - Rendering Strategies]]></title>
            <link>https://velog.io/@feels_bot_man/React-Router-v7-Framework-Rendering-Strategies</link>
            <guid>https://velog.io/@feels_bot_man/React-Router-v7-Framework-Rendering-Strategies</guid>
            <pubDate>Thu, 20 Feb 2025 04:17:52 GMT</pubDate>
            <description><![CDATA[<p>React Router에서는 CSR(Client Side Rendering), SSR(Server Side Rendering), Static Pre-rendering을 지원한다. 각각의 특징과 사용법을 살펴보자.</p>
<h3 id="client-side-rendering">Client Side Rendering</h3>
<p><strong>초기 로드 후, 모든 라우팅과 데이터 로딩을 클라이언트에서 처리</strong>한다.</p>
<p>SPA(Single Page Application)를 만들 때 사용한다.</p>
<p><strong>특징</strong></p>
<ul>
<li>초기 로딩 시, 빈 HTML + JavaScript 번들만 받아온다.</li>
<li>이후 라우팅은 클라이언트에서만 처리 (페이지 새로고침 없음).</li>
<li><strong>SEO에 불리</strong>하지만, <strong>빠른 페이지 전환</strong>이 가능.</li>
</ul>
<p><strong>사용법</strong></p>
<pre><code class="language-tsx">import type { Config } from &quot;@react-router/dev/config&quot;;

export default {
  ssr: false, // 서버 사이드 렌더링 비활성화
} satisfies Config;</code></pre>
<hr>
<h3 id="server-side-rendering">Server Side Rendering</h3>
<p><strong>페이지를 요청할 때마다 서버에서 HTML을 생성</strong>해 클라이언트에 전달한다.</p>
<p><strong>특징</strong></p>
<ul>
<li>첫 페이지 로딩 속도가 빠름 (<strong>SEO에 유리</strong>).</li>
<li>클라이언트에 도달하기 전, 완성된 HTML을 받음.</li>
<li><strong>서버 인프라가 필요</strong>.</li>
</ul>
<p><strong>사용법</strong></p>
<pre><code class="language-tsx">import type { Config } from &quot;@react-router/dev/config&quot;;

export default {
  ssr: true, // 서버 사이드 렌더링 활성화
} satisfies Config;</code></pre>
<p>💡 <strong>추가 팁</strong></p>
<ul>
<li><strong>SSR을 사용해도</strong> <code>clientLoader</code>를 통해 <strong>클라이언트 측 데이터 로딩</strong> 가능.</li>
<li>일부 라우트는 <strong>정적 프리렌더링(Static Pre-rendering)</strong>으로 처리 가능.</li>
</ul>
<hr>
<h3 id="static-pre-rendering">Static Pre-rendering</h3>
<p><strong>빌드 시점에 미리 HTML을 생성</strong>해 정적으로 호스팅한다.</p>
<p><strong>SEO 최적화</strong>와 <strong>빠른 로딩 속도</strong>를 위해 사용된다.</p>
<p><strong>특징</strong></p>
<ul>
<li><strong>서버 필요 없음</strong> (정적 사이트 호스팅 가능, 예: GitHub Pages, Netlify).</li>
<li>특정 경로만 선택적으로 프리렌더링 가능.</li>
<li><em>정적 사이트 생성기(SSG)*</em>처럼 동작.</li>
</ul>
<p><strong>사용법</strong></p>
<pre><code class="language-tsx">import type { Config } from &quot;@react-router/dev/config&quot;;

export default {
  async prerender() {
    return [&quot;/&quot;, &quot;/about&quot;, &quot;/contact&quot;]; // 빌드 시 미리 렌더링할 경로 목록
  },
} satisfies Config;</code></pre>
<ul>
<li>빌드 시점에 <code>&quot;/&quot;</code>, <code>&quot;/about&quot;</code>, <code>&quot;/contact&quot;</code> 페이지를 <strong>정적 HTML</strong>로 생성.</li>
<li><strong>SEO 최적화</strong>와 <strong>정적 페이지 제공</strong>에 유리.</li>
</ul>
<hr>
<p><strong>렌더링 방식 비교</strong></p>
<table>
<thead>
<tr>
<th></th>
<th><strong>CSR</strong></th>
<th><strong>SSR</strong></th>
<th><strong>Static Pre-rendering</strong></th>
</tr>
</thead>
<tbody><tr>
<td>🌐 <strong>SEO</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>느림 (서버 호출)</td>
<td>빠름</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>가능</td>
<td>불가</td>
</tr>
</tbody></table>
<hr>
<p>🎯 <strong>언제 어떤 방식을 사용할까?</strong></p>
<ul>
<li>🛍️ <strong>쇼핑몰/블로그 등 SEO 중요한 서비스</strong> → <strong>SSR</strong> 또는 <strong>Static Pre-rendering</strong></li>
<li>⚡ <strong>SPA, 대시보드 등 인터랙티브 앱</strong> → <strong>CSR</strong></li>
<li>📖 <strong>정적 정보성 페이지</strong> → <strong>Static Pre-rendering</strong></li>
</ul>
<p>💡 <strong>Tip:</strong></p>
<ul>
<li>필요에 따라 <strong>CSR + Static Pre-rendering</strong>을 조합하거나,</li>
<li>일부 페이지만 <strong>SSR</strong>로 처리할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Router v7 Framework - Route Module]]></title>
            <link>https://velog.io/@feels_bot_man/React-Router-v7-Framework-Route-Module</link>
            <guid>https://velog.io/@feels_bot_man/React-Router-v7-Framework-Route-Module</guid>
            <pubDate>Thu, 20 Feb 2025 04:10:34 GMT</pubDate>
            <description><![CDATA[<h2 id="route-module">Route Module</h2>
<p><code>routes.ts</code>에서 지정하는 파일(예: &quot;./team.tsx&quot;)을 Route Module이라고 한다.</p>
<pre><code class="language-tsx">route(&quot;teams/:teamId&quot;, &quot;./team.tsx&quot;),
//                        ^^^^^^^^ Route Module</code></pre>
<p>Route Module이란?</p>
<p>React Router의 핵심 구조로, 각 라우트의 동작을 정의하는 모듈 파일
React 컴포넌트 외에도 데이터 로딩, 액션 처리, 오류 처리 등을 수행</p>
<p>Route Module은 단순한 페이지 컴포넌트가 아니라, <strong>다음과 같은 기능을 포함</strong>할 수 있다.</p>
<table>
<thead>
<tr>
<th>기능</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>코드 스플리팅</strong></td>
<td>필요한 페이지 코드만 로드</td>
</tr>
<tr>
<td><strong>데이터 로딩 (<code>loader</code>)</strong></td>
<td>서버에서 데이터를 미리 불러오기</td>
</tr>
<tr>
<td><strong>액션 (<code>action</code>)</strong></td>
<td>폼 제출 등의 이벤트 처리</td>
</tr>
<tr>
<td><strong>재검증 (<code>revalidation</code>)</strong></td>
<td>데이터를 최신 상태로 유지</td>
</tr>
<tr>
<td><strong>에러 경계 (<code>ErrorBoundary</code>)</strong></td>
<td>해당 라우트에서 발생한 오류 처리</td>
</tr>
</tbody></table>
<hr>
<h3 id="component-default">Component (default)</h3>
<p>React Router의 Route Module에서 export default로 정의한 컴포넌트는 해당 경로가 매칭될 때 렌더링되는 컴포넌트이다.</p>
<pre><code class="language-tsx">// app/routes/my-route.tsx
export default function MyRouteComponent() {
  return (
    &lt;div&gt;
      &lt;h1&gt;Look ma!&lt;/h1&gt;
      &lt;p&gt;I&#39;m still using React Router after like 10 years.&lt;/p&gt;
    &lt;/div&gt;
  );
}</code></pre>
<ul>
<li><code>export default</code>로 내보내면 React Router가 해당 경로에서 이 컴포넌트를 자동으로 렌더링한다.</li>
</ul>
<p>📌 <strong>컴포넌트에 전달되는 Props</strong></p>
<p>라우트가 렌더링될 때, React Router는 몇 가지 <strong>props</strong>를 자동으로 전달한다.</p>
<p>이 props는 <code>Route.ComponentProps</code> 타입을 따른다.</p>
<table>
<thead>
<tr>
<th>Props</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>loaderData</code></td>
<td>이 라우트 모듈의 <code>loader()</code> 함수가 반환한 데이터</td>
</tr>
<tr>
<td><code>actionData</code></td>
<td>이 라우트 모듈의 <code>action()</code> 함수가 반환한 데이터</td>
</tr>
<tr>
<td><code>params</code></td>
<td>URL에 포함된 <strong>동적 라우트 매개변수</strong></td>
</tr>
<tr>
<td><code>matches</code></td>
<td>현재 URL과 일치하는 모든 경로 정보 배열</td>
</tr>
</tbody></table>
<p>이 props를 활용하면 별도의 <strong>훅 (<code>useLoaderData</code>, <code>useParams</code> 등)</strong> 없이도 데이터를 사용할 수 있다.</p>
<p>Props를 활용한 예제</p>
<p>1️⃣ <strong>라우트 설정</strong></p>
<pre><code class="language-tsx">import { type RouteConfig, route } from &quot;@react-router/dev/routes&quot;;

export default [
  route(&quot;users/:userId&quot;, &quot;./user.tsx&quot;), // userId를 포함한 동적 라우트
] satisfies RouteConfig;
</code></pre>
<p>2️⃣ <strong>Route Module (<code>user.tsx</code>)</strong></p>
<pre><code class="language-tsx">import type { Route } from &quot;./+types/user&quot;;

export default function MyRouteComponent({
  loaderData,
  actionData,
  params,
  matches,
}: Route.ComponentProps) {
  return (
    &lt;div&gt;
      &lt;h1&gt;Welcome to My Route with Props!&lt;/h1&gt;
      &lt;p&gt;Loader Data: {JSON.stringify(loaderData)}&lt;/p&gt;
      &lt;p&gt;Action Data: {JSON.stringify(actionData)}&lt;/p&gt;
      &lt;p&gt;Route Parameters: {JSON.stringify(params)}&lt;/p&gt;
      &lt;p&gt;Matched Routes: {JSON.stringify(matches)}&lt;/p&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<ul>
<li><code>params.userId</code>를 통해 <code>users/:userId</code> 경로에서 <code>userId</code> 값을 가져올 수 있다.</li>
<li><code>loaderData</code>, <code>actionData</code>는 해당 라우트에서 데이터를 미리 불러올 때 활용된다.</li>
</ul>
<h3 id="loader">loader</h3>
<p><code>loader()</code>는 라우트 컴포넌트가 렌더링되기 전에 데이터를 제공하는 함수이다.
서버에서 실행되거나 빌드 시점(pre-rendering) 에만 호출된다.</p>
<p>📌 <strong>loader의 주요 특징</strong></p>
<table>
<thead>
<tr>
<th>특징</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>서버에서 실행</strong></td>
<td><code>loader()</code>는 <strong>서버에서만 실행</strong>되므로 클라이언트에서는 직접 호출되지 않음</td>
</tr>
<tr>
<td><strong>비동기 지원</strong></td>
<td>API 요청 등을 수행할 수 있도록 <code>async</code> 함수로 정의</td>
</tr>
<tr>
<td><strong>데이터 미리 로드</strong></td>
<td>컴포넌트가 렌더링되기 전에 데이터가 제공됨</td>
</tr>
<tr>
<td><strong>자동 전달</strong></td>
<td><code>loader()</code>의 반환 값이 <code>loaderData</code> props로 자동 전달</td>
</tr>
</tbody></table>
<pre><code class="language-tsx">export async function loader() {
  return { message: &quot;Hello, world!&quot; };
}

export default function MyRoute({ loaderData }) {
  return &lt;h1&gt;{loaderData.message}&lt;/h1&gt;;
}</code></pre>
<ul>
<li>loader()가 { message: &quot;Hello, world!&quot; } 데이터를 반환</li>
<li>MyRoute 컴포넌트에서 loaderData.message를 받아 화면에 출력</li>
</ul>
<p>참고 <a href="https://api.reactrouter.com/v7/interfaces/react_router.LoaderFunctionArgs">https://api.reactrouter.com/v7/interfaces/react_router.LoaderFunctionArgs</a></p>
<hr>
<h3 id="clientloader">clientLoader</h3>
<p><code>clientLoader()</code>는 <strong>브라우저에서만 실행되는 데이터 로딩 함수</strong>이다.
<code>serverLoader()</code>의 데이터를 활용할 수도 있고, 독립적으로 동작할 수도 있다.</p>
<p>📌 <strong>기본 개념</strong></p>
<table>
<thead>
<tr>
<th>특징</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>클라이언트에서 실행</strong></td>
<td><code>clientLoader()</code>는 오직 브라우저에서 실행됨</td>
</tr>
<tr>
<td><strong>서버 데이터 활용 가능</strong></td>
<td>기존 <code>serverLoader()</code>의 데이터를 활용할 수 있음</td>
</tr>
<tr>
<td><strong>클라이언트 전용 데이터 로딩</strong></td>
<td>브라우저에서만 필요한 데이터를 가져오는 데 사용됨</td>
</tr>
<tr>
<td><strong>Hydration 지원</strong></td>
<td>서버에서 렌더링된 데이터를 클라이언트에서 보강 가능</td>
</tr>
</tbody></table>
<pre><code class="language-tsx">export async function clientLoader({ serverLoader }) {
  // 서버에서 데이터를 가져옴
  const serverData = await serverLoader();

  // 클라이언트에서 추가 데이터 가져오기
  const clientData = getDataFromClient();

  // 최종 데이터를 반환
  return { ...serverData, clientData };
}</code></pre>
<ul>
<li><code>serverLoader()</code>를 실행하여 서버 데이터를 가져옴</li>
<li><code>getDataFromClient()</code>를 호출하여 <strong>클라이언트에서 추가 데이터를 로드</strong></li>
<li>두 데이터를 합쳐서 반환</li>
</ul>
<p>Hydration을 활성화하는 경우</p>
<pre><code class="language-tsx">export async function clientLoader() {
  // 클라이언트에서 실행할 데이터 로딩 로직
  const data = await fetch(&quot;/api/client-data&quot;).then((res) =&gt; res.json());
  return data;
}

// Hydration 활성화
clientLoader.hydrate = true as const;</code></pre>
<ul>
<li><code>clientLoader.hydrate = true as const;</code>를 설정하면 <strong>서버에서 제공된 데이터를 클라이언트에서 보강 가능</strong></li>
<li>TypeScript에서 <code>true as const</code>를 사용하면 타입이 <code>boolean</code>이 아닌 <code>true</code>로 고정됨</li>
<li>React Router는 이를 활용해 <strong>loaderData의 타입을 자동으로 유추</strong></li>
</ul>
<p><strong>언제 clientLoader를 사용할까?</strong></p>
<ol>
<li><strong>클라이언트에서만 필요한 데이터를 불러올 때</strong><ul>
<li>예: 사용자 인터랙션 후 특정 데이터를 요청하는 경우</li>
</ul>
</li>
<li><strong>서버 데이터와 클라이언트 데이터를 조합할 때</strong><ul>
<li>예: <code>serverLoader()</code>에서 받은 데이터에 <strong>추가적인 브라우저 정보</strong>를 결합</li>
</ul>
</li>
<li><strong>Hydration을 활용할 때</strong><ul>
<li>서버에서 렌더링된 데이터를 클라이언트에서 보강하고 싶을 때</li>
</ul>
</li>
</ol>
<p>📌 <strong>clientLoader vs loader 차이점</strong></p>
<table>
<thead>
<tr>
<th>비교 항목</th>
<th><code>loader()</code></th>
<th><code>clientLoader()</code></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><code>serverLoader()</code>를 호출하여 사용 가능</td>
</tr>
<tr>
<td><strong>Hydration 지원</strong></td>
<td>기본적으로 서버에서 데이터 전달</td>
<td><code>clientLoader.hydrate = true</code> 설정 시 지원</td>
</tr>
<tr>
<td><strong>사용 목적</strong></td>
<td>SSR, 초기 데이터 제공</td>
<td>클라이언트에서 추가 데이터 요청</td>
</tr>
</tbody></table>
<p>참고 <a href="https://api.reactrouter.com/v7/types/react_router.ClientLoaderFunctionArgs">https://api.reactrouter.com/v7/types/react_router.ClientLoaderFunctionArgs</a></p>
<hr>
<h3 id="action">action</h3>
<p><code>action()</code>은 <strong>서버에서 데이터를 변경하는 함수</strong>이다.
<code>loader()</code>와 함께 사용하면 <strong>데이터를 자동으로 갱신</strong>한다.
<code>&lt;Form&gt;</code>, <code>useFetcher()</code>, <code>useSubmit()</code>과 함께 사용 가능하다</p>
<p>1️⃣ <strong>기본적인 <code>loader()</code></strong></p>
<pre><code class="language-tsx">export async function loader() {
  const items = await fakeDb.getItems();
  return { items };
}</code></pre>
<ul>
<li><code>fakeDb.getItems()</code>를 통해 <strong>서버에서 데이터를 가져옴</strong></li>
<li>이 데이터는 <code>loaderData.items</code>로 <strong>컴포넌트에서 접근 가능</strong></li>
</ul>
<hr>
<p>2️⃣ <strong>컴포넌트에서 <code>loaderData</code> 사용</strong></p>
<pre><code class="language-tsx">export default function Items({ loaderData }) {
  return (
    &lt;div&gt;
      &lt;List items={loaderData.items} /&gt;
      &lt;Form method=&quot;post&quot; navigate={false} action=&quot;/list&quot;&gt;
        &lt;input type=&quot;text&quot; name=&quot;title&quot; /&gt;
        &lt;button type=&quot;submit&quot;&gt;Create Todo&lt;/button&gt;
      &lt;/Form&gt;
    &lt;/div&gt;
  );
}</code></pre>
<ul>
<li><code>List items={loaderData.items}</code> → <code>loaderData</code>에서 받은 데이터를 리스트로 표시</li>
<li><code>&lt;Form&gt;</code> 사용<ul>
<li><code>method=&quot;post&quot;</code> → <code>POST</code> 요청을 보냄</li>
<li><code>action=&quot;/list&quot;</code> → <code>/list</code> 경로의 <code>action()</code>이 호출됨</li>
<li><code>navigate={false}</code> → <strong>페이지 새로고침 없이 데이터만 갱신</strong></li>
</ul>
</li>
</ul>
<p>3️⃣ <strong>서버에서 데이터 변경 (action())</strong></p>
<pre><code class="language-tsx">export async function action({ request }) {
  const data = await request.formData();
  const todo = await fakeDb.addItem({
    title: data.get(&quot;title&quot;),
  });
  return { ok: true };
}</code></pre>
<ul>
<li><code>request.formData()</code>를 사용해 <strong>폼 데이터 가져오기</strong></li>
<li><code>fakeDb.addItem()</code>을 호출해 <strong>새로운 할 일 추가</strong></li>
<li><code>{ ok: true }</code> 반환 (필수는 아니지만, 성공 여부를 전달 가능)</li>
<li><strong>자동으로 <code>loader()</code>가 다시 실행되며 데이터가 갱신됨</strong></li>
</ul>
<p><strong>언제 <code>action()</code>을 사용할까?</strong></p>
<ol>
<li><strong>서버에서 데이터를 추가, 수정, 삭제할 때</strong><ul>
<li>예: 할 일 목록에 새로운 항목 추가 (<code>POST</code>), 항목 수정 (<code>PUT</code>), 삭제 (<code>DELETE</code>)</li>
</ul>
</li>
<li><strong>폼 제출 후 자동으로 리스트를 갱신하고 싶을 때</strong><ul>
<li><code>action()</code> 실행 후 <strong>React Router가 자동으로 <code>loader()</code>를 다시 실행</strong></li>
</ul>
</li>
<li><strong>페이지 새로고침 없이 서버 데이터를 갱신할 때</strong><ul>
<li><code>&lt;Form navigate={false}&gt;</code> → <strong>SPA 경험 유지</strong></li>
</ul>
</li>
</ol>
<p>📌 <strong>action()과 loader()의 관계</strong></p>
<table>
<thead>
<tr>
<th>기능</th>
<th><code>loader()</code></th>
<th><code>action()</code></th>
</tr>
</thead>
<tbody><tr>
<td>실행 위치</td>
<td>서버</td>
<td>서버</td>
</tr>
<tr>
<td>역할</td>
<td>데이터 조회</td>
<td>데이터 변경 (POST, PUT, DELETE)</td>
</tr>
<tr>
<td>호출 방식</td>
<td>페이지 로드 시 실행</td>
<td><code>&lt;Form&gt;</code>, <code>useSubmit()</code>, <code>useFetcher()</code> 호출 시 실행</td>
</tr>
<tr>
<td>자동 갱신</td>
<td>X</td>
<td>실행 후 <code>loader()</code>가 자동 재실행</td>
</tr>
</tbody></table>
<h3 id="clientaction">clientAction</h3>
<p><code>clientAction()</code>은 클라이언트에서 실행되는 <code>action()</code>이다.</p>
<p>✔ 캐시를 무효화하거나 UI 상태를 변경할 때 유용 
✔ 필요하면 <code>serverAction()</code>을 호출해 서버에서도 데이터를 변경 가능
✔ 서버 요청 없이 데이터를 변경할 때 효과적</p>
<pre><code class="language-tsx">export async function clientAction({ serverAction }) {
  fakeInvalidateClientSideCache(); // 캐시 초기화
  const data = await serverAction(); // 서버 데이터 다시 요청
  return data;
}</code></pre>
<ul>
<li>클라이언트 캐시를 무효화 (fakeInvalidateClientSideCache())</li>
<li>필요하면 serverAction()을 호출해 서버에서도 데이터 변경 가능</li>
<li>데이터를 반환하면 클라이언트에서 활용할 수 있음</li>
</ul>
<p>참고 <a href="https://api.reactrouter.com/v7/types/react_router.ClientActionFunctionArgs">https://api.reactrouter.com/v7/types/react_router.ClientActionFunctionArgs</a></p>
<hr>
<h3 id="errorboundary">ErrorBoundary</h3>
<p><code>ErrorBoundary()</code>를 정의하면 해당 라우트에서 발생한 오류를 잡아 사용자에게 표시한다.</p>
<p>✔ <strong>HTTP 응답 에러(<code>404</code>, <code>500</code>)와 일반적인 JavaScript 에러를 구분하여 처리 가능</strong>
✔ <strong>예상하지 못한 에러도 대비하여 안전한 UI 제공</strong></p>
<pre><code class="language-tsx">import {
  isRouteErrorResponse,
  useRouteError,
} from &quot;react-router&quot;;

export function ErrorBoundary() {
  const error = useRouteError();</code></pre>
<ul>
<li><code>useRouteError()</code> → 현재 라우트에서 발생한 <strong>에러 정보를 가져오는 React Router 훅</strong></li>
<li><code>error</code> 변수에 <strong>라우트에서 발생한 에러 정보</strong>가 담김</li>
</ul>
<p>1️⃣ HTTP 응답 에러 처리</p>
<pre><code class="language-tsx">  if (isRouteErrorResponse(error)) {
    return (
      &lt;div&gt;
        &lt;h1&gt;
          {error.status} {error.statusText}
        &lt;/h1&gt;
        &lt;p&gt;{error.data}&lt;/p&gt;
      &lt;/div&gt;
    );
  }</code></pre>
<ul>
<li><code>isRouteErrorResponse(error)</code> → 에러가 <strong>HTTP 응답 관련 에러인지 확인</strong> (<code>404</code>, <code>500</code> 등)</li>
<li><code>error.status</code>와 <code>error.statusText</code>를 이용해 <strong>HTTP 상태 코드와 메시지 표시</strong></li>
<li><code>error.data</code>를 표시해 <strong>서버에서 전달된 추가 에러 메시지를 출력</strong></li>
</ul>
<p><strong>예제:</strong></p>
<pre><code class="language-tsx">throw new Response(&quot;Not Found&quot;, { status: 404 });</code></pre>
<p>이런 응답이 발생하면, 에러 화면은 아래와 같이 표시됨:</p>
<pre><code>404 Not Found</code></pre><hr>
<p>2️⃣ 일반적인 JavaScript 에러 처리</p>
<pre><code class="language-tsx">  else if (error instanceof Error) {
    return (
      &lt;div&gt;
        &lt;h1&gt;Error&lt;/h1&gt;
        &lt;p&gt;{error.message}&lt;/p&gt;
        &lt;p&gt;The stack trace is:&lt;/p&gt;
        &lt;pre&gt;{error.stack}&lt;/pre&gt;
      &lt;/div&gt;
    );
  }</code></pre>
<ul>
<li>에러가 <strong>일반적인 JavaScript <code>Error</code> 객체인지 확인</strong></li>
<li><code>error.message</code> → 에러 메시지 표시</li>
<li><code>error.stack</code> → 에러가 발생한 위치(콜스택) 표시</li>
</ul>
<p><strong>예제:</strong></p>
<pre><code class="language-tsx">throw new Error(&quot;Something went wrong!&quot;);</code></pre>
<p>이런 에러가 발생하면 화면에는 아래와 같이 표시됨:</p>
<pre><code>Error
Something went wrong!
The stack trace is:
...</code></pre><hr>
<p>3️⃣ 예상하지 못한 에러 처리</p>
<pre><code class="language-tsx">  else {
    return &lt;h1&gt;Unknown Error&lt;/h1&gt;;
  }</code></pre>
<ul>
<li><strong>위 두 가지 조건을 만족하지 않는 경우</strong> (<code>null</code>, <code>undefined</code>, 기타 예외적인 값)</li>
<li><code>&quot;Unknown Error&quot;</code> 메시지를 표시</li>
</ul>
<hr>
<h3 id="hydratefallback">HydrateFallback</h3>
<p><code>HydrateFallback</code>은 <strong>초기 페이지 로드 시</strong>, <code>clientLoader</code>가 데이터를 불러오는 동안 <strong>임시로 보여줄 UI</strong>를 정의한다.</p>
<p>✔ <strong>초기 로딩 지연</strong> → <code>clientLoader</code>가 데이터를 가져오는 동안 <strong>로딩 화면</strong>을 표시
✔ <strong>클라이언트 전용</strong> → 서버 렌더링 없이 클라이언트에서만 작동
✔ <strong>로딩 UX 개선</strong> → 사용자에게 빈 화면 대신 <strong>로딩 메시지나 스피너</strong>를 보여줄 수 있음</p>
<p>1️⃣ <strong><code>clientLoader</code> – 클라이언트 데이터 로딩</strong></p>
<pre><code class="language-tsx">export async function clientLoader() {
  const data = await fakeLoadLocalGameData();
  return data;
}</code></pre>
<ul>
<li><code>clientLoader</code>가 데이터를 <strong>비동기적으로 가져옴</strong></li>
<li>이 과정에서 <strong>컴포넌트가 바로 렌더링되지 않음</strong> → 데이터 로딩이 끝난 후에야 렌더링됨</li>
</ul>
<hr>
<p>2️⃣ <strong><code>HydrateFallback</code> – 로딩 중 임시 화면</strong></p>
<pre><code class="language-tsx">export function HydrateFallback() {
  return &lt;p&gt;Loading Game...&lt;/p&gt;;
}
</code></pre>
<ul>
<li><strong><code>clientLoader</code>가 데이터 로딩 중일 때</strong> 이 컴포넌트가 대신 렌더링됨</li>
<li>사용자에게 <strong>&quot;Loading Game...&quot;</strong> 메시지를 보여줌</li>
<li><strong>로딩 스피너, 애니메이션 등</strong>으로 대체 가능</li>
</ul>
<hr>
<p>3️⃣ <strong><code>Component</code> – 데이터 로딩 후 렌더링</strong></p>
<pre><code class="language-tsx">export default function Component({ loaderData }) {
  return &lt;Game data={loaderData} /&gt;;
}</code></pre>
<ul>
<li><code>clientLoader</code>가 데이터를 모두 가져오면, <code>Component</code>가 렌더링됨</li>
<li><code>loaderData</code>를 받아서 실제 <strong>게임 컴포넌트</strong>에 데이터를 넘김</li>
</ul>
<hr>
<h3 id="headers">headers</h3>
<p><code>headers</code> 함수는 <strong>서버 렌더링(SSR)</strong> 시 클라이언트에 전달할 <strong>HTTP 헤더</strong>를 설정한다.</p>
<p>✔ <strong>SEO 최적화</strong>, <strong>보안 설정</strong>, <strong>캐싱</strong> 등 다양한 목적에 사용
✔ 클라이언트가 아닌 <strong>서버에서만 호출</strong>됨
✔ 특정 라우트에 <strong>커스텀 헤더</strong>를 추가 가능</p>
<pre><code class="language-tsx">export function headers() {
  return {
    &quot;X-Stretchy-Pants&quot;: &quot;its for fun&quot;,
    &quot;Cache-Control&quot;: &quot;max-age=300, s-maxage=3600&quot;,
  };
}</code></pre>
<p>1️⃣ <strong><code>X-Stretchy-Pants</code>: 커스텀 헤더</strong></p>
<ul>
<li><code>&quot;X-&quot;</code>로 시작하는 커스텀 헤더</li>
<li>브라우저에 직접적인 영향은 없지만, <strong>디버깅이나 로그 추적</strong>에 사용 가능</li>
</ul>
<p>2️⃣ <strong><code>Cache-Control</code>: 캐싱 전략</strong></p>
<ul>
<li><strong><code>max-age=300</code></strong> → 클라이언트 브라우저가 <strong>5분(300초)</strong> 동안 캐시</li>
<li><strong><code>s-maxage=3600</code></strong> → CDN 또는 프록시 서버가 <strong>1시간(3600초)</strong> 동안 캐시</li>
<li>캐싱을 통해 <strong>서버 부하 감소</strong> 및 <strong>페이지 로딩 속도 향상</strong></li>
</ul>
<table>
<thead>
<tr>
<th>목적</th>
<th>사용 예시</th>
</tr>
</thead>
<tbody><tr>
<td>🔒 <strong>보안 강화</strong></td>
<td><code>Strict-Transport-Security</code>, <code>Content-Security-Policy</code></td>
</tr>
<tr>
<td>🚀 <strong>캐싱 최적화</strong></td>
<td><code>Cache-Control</code>, <code>ETag</code></td>
</tr>
<tr>
<td>🔍 <strong>SEO 개선</strong></td>
<td><code>X-Robots-Tag</code>, <code>Link</code> (rel=&quot;canonical&quot;)</td>
</tr>
<tr>
<td>📊 <strong>로그/디버깅</strong></td>
<td>커스텀 헤더 (<code>X-Custom-Header</code>)</td>
</tr>
</tbody></table>
<h3 id="handle">handle</h3>
<p><code>handle</code>객체는 라우트에 커스텀 데이터를 추가할 때 사용한다. 이를 통해 라우트에 부가 정보를 넣어 다양한 기능을 구현할 수 있다.</p>
<p>✔ 메타데이터, 브레드크럼, 권한 정보 등 저장 가능
✔ <code>useMatches</code> 훅을 통해 모든 라우트의 <code>handle</code> 정보 접근 가능
✔ 단순히 데이터 전달용으로 사용됨 (렌더링에는 직접 관여하지 않음)</p>
<pre><code class="language-tsx">export const handle = {
  its: &quot;all yours&quot;,
};</code></pre>
<ul>
<li>이 라우트를 사용할 때 <code>useMatches</code> 훅으로 접근 가능</li>
<li>결과: <code>{ its: &quot;all yours&quot; }</code></li>
</ul>
<hr>
<h3 id="links">links</h3>
<p><code>links</code> 함수는 HTML <code>&lt;head&gt;</code>에 <code>&lt;link&gt;</code> 태그를 추가하기 위한 용도로 사용된다. 이 함수가 반환한 링크들은 전역적으로 적용되며, 주로 아이콘, 스타일시트, 폰트, 프리로드 등을 설정할 때 사용한다.</p>
<p>✔ 파비콘(favicon), 외부 CSS, 자원 프리로드 설정 가능
✔ 모든 라우트의 링크들이 자동으로 통합되어 <code>&lt;head&gt;</code>에 적용
✔ <code>&lt;Links /&gt;</code> 컴포넌트를 통해 <code>&lt;head&gt;</code>에 삽입</p>
<pre><code class="language-tsx">export function links() {
  return [
    {
      rel: &quot;icon&quot;, // 브라우저 탭의 아이콘 설정
      href: &quot;/favicon.png&quot;,
      type: &quot;image/png&quot;,
    },
    {
      rel: &quot;stylesheet&quot;, // 외부 CSS 파일 불러오기
      href: &quot;https://example.com/some/styles.css&quot;,
    },
    {
      rel: &quot;preload&quot;, // 이미지 미리 불러오기
      href: &quot;/images/banner.jpg&quot;,
      as: &quot;image&quot;,
    },
  ];
}</code></pre>
<p>결과적으로 생성되는 <code>&lt;head&gt;</code> 태그</p>
<pre><code class="language-html">&lt;head&gt;
  &lt;link rel=&quot;icon&quot; href=&quot;/favicon.png&quot; type=&quot;image/png&quot; /&gt;
  &lt;link rel=&quot;stylesheet&quot; href=&quot;https://example.com/some/styles.css&quot; /&gt;
  &lt;link rel=&quot;preload&quot; href=&quot;/images/banner.jpg&quot; as=&quot;image&quot; /&gt;
&lt;/head&gt;</code></pre>
<p><code>&lt;Links /&gt;</code> 컴포넌트로 적용</p>
<pre><code class="language-tsx">import { Links } from &quot;react-router&quot;;

export default function Root() {
  return (
    &lt;html&gt;
      &lt;head&gt;
        &lt;Links /&gt; {/* 모든 라우트의 links를 적용 */}
      &lt;/head&gt;
      &lt;body&gt;
        &lt;h1&gt;My App&lt;/h1&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}</code></pre>
<p><strong>활용 예시</strong></p>
<table>
<thead>
<tr>
<th>용도</th>
<th>코드 예시</th>
</tr>
</thead>
<tbody><tr>
<td>🎨 <strong>파비콘 설정</strong></td>
<td><code>{ rel: &quot;icon&quot;, href: &quot;/favicon.ico&quot; }</code></td>
</tr>
<tr>
<td>📄 <strong>외부 폰트 불러오기</strong></td>
<td><code>{ rel: &quot;stylesheet&quot;, href: &quot;https://fonts.googleapis.com/css2?family=Roboto&quot; }</code></td>
</tr>
<tr>
<td>⚡ <strong>이미지 프리로드</strong></td>
<td><code>{ rel: &quot;preload&quot;, href: &quot;/hero.jpg&quot;, as: &quot;image&quot; }</code></td>
</tr>
<tr>
<td>🛠️ <strong>PWA 설정</strong></td>
<td><code>{ rel: &quot;manifest&quot;, href: &quot;/site.webmanifest&quot; }</code></td>
</tr>
</tbody></table>
<hr>
<h3 id="meta">meta</h3>
<p><code>meta</code> 함수는 HTML <code>&lt;head&gt;</code>에 메타 태그를 추가하기 위한 용도로 사용된다. 주로 SEO 최적화, SNS 공유 설정(Open Graph), 문서 정보 제공 등에 활용된다.</p>
<p>✔ <code>&lt;title&gt;</code> 태그 설정
✔ <code>meta</code> 태그로 SEO 및 SNS 미리보기 설정
✔ 모든 라우트의 메타 태그가 통합되어 <code>&lt;head&gt;</code>에 삽입됨</p>
<pre><code class="language-tsx">export function meta() {
  return [
    { title: &quot;Very cool app&quot; }, // &lt;title&gt; 태그 설정
    {
      property: &quot;og:title&quot;,     // Open Graph: SNS 공유 시 제목
      content: &quot;Very cool app&quot;,
    },
    {
      name: &quot;description&quot;,      // 검색 엔진 및 미리보기에 표시될 설명
      content: &quot;This app is the best&quot;,
    },
  ];
}</code></pre>
<p>결과적으로 생성되는 <code>&lt;head&gt;</code> 태그</p>
<pre><code class="language-html">&lt;head&gt;
  &lt;title&gt;Very cool app&lt;/title&gt;
  &lt;meta property=&quot;og:title&quot; content=&quot;Very cool app&quot; /&gt;
  &lt;meta name=&quot;description&quot; content=&quot;This app is the best&quot; /&gt;
&lt;/head&gt;</code></pre>
<p><code>&lt;Meta /&gt;</code> 컴포넌트로 적용</p>
<pre><code class="language-tsx">import { Meta } from &quot;react-router&quot;;

export default function Root() {
  return (
    &lt;html&gt;
      &lt;head&gt;
        &lt;Meta /&gt; {/* 모든 라우트의 meta를 적용 */}
      &lt;/head&gt;
      &lt;body&gt;
        &lt;h1&gt;My App&lt;/h1&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}</code></pre>
<p><strong>활용 예시</strong></p>
<table>
<thead>
<tr>
<th>용도</th>
<th>코드 예시</th>
</tr>
</thead>
<tbody><tr>
<td>🏷️ <strong>페이지 제목</strong></td>
<td><code>{ title: &quot;My Awesome App&quot; }</code></td>
</tr>
<tr>
<td>🖼️ <strong>Open Graph 이미지</strong></td>
<td><code>{ property: &quot;og:image&quot;, content: &quot;https://example.com/image.png&quot; }</code></td>
</tr>
<tr>
<td>💬 <strong>페이지 설명</strong></td>
<td><code>{ name: &quot;description&quot;, content: &quot;A cool web app for everyone.&quot; }</code></td>
</tr>
<tr>
<td>🐦 <strong>Twitter 카드 설정</strong></td>
<td><code>{ name: &quot;twitter:card&quot;, content: &quot;summary_large_image&quot; }</code></td>
</tr>
<tr>
<td>📱 <strong>반응형 뷰포트 설정</strong></td>
<td><code>{ name: &quot;viewport&quot;, content: &quot;width=device-width, initial-scale=1&quot; }</code></td>
</tr>
</tbody></table>
<p>참고 <a href="https://api.reactrouter.com/v7/interfaces/react_router.MetaArgs">https://api.reactrouter.com/v7/interfaces/react_router.MetaArgs</a></p>
<hr>
<h3 id="shouldrevalidate">shouldRevalidate</h3>
<p><code>shouldRevalidate</code> 함수는 라우트의 데이터가 다시 로드되어야 하는지를 결정한다. 기본적으로, 모든 라우트는 액션 후에 자동으로 데이터가 재검증(revalidate) 된다. 하지만, 데이터에 영향이 없는 경우 재검증을 건너뛸 수 있다.</p>
<p>✔ 성능 최적화: 필요할 때만 데이터 다시 가져오기
✔ 불필요한 API 호출 방지로 서버 비용 절감
✔ 복잡한 폼이나 대용량 데이터 페이지에 유용</p>
<pre><code class="language-tsx">import type { ShouldRevalidateFunctionArgs } from &quot;react-router&quot;;

export function shouldRevalidate(arg: ShouldRevalidateFunctionArgs) {
  return true; // true면 데이터를 다시 가져오고, false면 재검증 X
}</code></pre>
<p>특정 액션에 대해 재검증 방지</p>
<pre><code class="language-tsx">export function shouldRevalidate({ formAction }) {
  // &quot;/profile/update&quot;에서 발생한 액션이면 재검증 방지
  return formAction !== &quot;/profile/update&quot;;
}</code></pre>
<p><strong><code>ShouldRevalidateFunctionArgs</code> 주요 값</strong></p>
<table>
<thead>
<tr>
<th>속성</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>currentUrl</code></td>
<td>현재 요청된 URL</td>
</tr>
<tr>
<td><code>nextUrl</code></td>
<td>다음으로 이동할 URL</td>
</tr>
<tr>
<td><code>formMethod</code></td>
<td>폼 요청 시 사용된 HTTP 메서드 (<code>POST</code>, <code>PUT</code> 등)</td>
</tr>
<tr>
<td><code>formAction</code></td>
<td>폼이 전송된 경로</td>
</tr>
<tr>
<td><code>formData</code></td>
<td>폼에서 전송된 데이터</td>
</tr>
<tr>
<td><code>defaultShouldRevalidate</code></td>
<td>기본적으로 재검증해야 하는지 여부</td>
</tr>
</tbody></table>
<p>💡 <strong>언제 사용하면 좋을까?</strong></p>
<table>
<thead>
<tr>
<th>상황</th>
<th>재검증 필요 여부</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>🖊️ <strong>데이터 수정</strong></td>
<td>✅ 필요</td>
<td>UI 업데이트를 위해 새 데이터 필요</td>
</tr>
<tr>
<td>💬 <strong>댓글 작성 시 목록 변경 없음</strong></td>
<td>❌ 불필요</td>
<td>댓글 목록을 다시 불러올 필요 없음</td>
</tr>
<tr>
<td>🔄 <strong>필터 변경 시 새로운 데이터 필요</strong></td>
<td>✅ 필요</td>
<td>필터링에 맞는 데이터 다시 가져와야 함</td>
</tr>
<tr>
<td>✅ <strong>토글 버튼 클릭(상태값만 변경)</strong></td>
<td>❌ 불필요</td>
<td>서버 데이터가 변경되지 않음</td>
</tr>
</tbody></table>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Router v7 Framework - Routing]]></title>
            <link>https://velog.io/@feels_bot_man/React-Router-v7-Framework-Routing</link>
            <guid>https://velog.io/@feels_bot_man/React-Router-v7-Framework-Routing</guid>
            <pubDate>Wed, 19 Feb 2025 15:16:38 GMT</pubDate>
            <description><![CDATA[<h2 id="routing"><a href="https://reactrouter.com/start/framework/routing">Routing</a></h2>
<p>React Router는 다양한 라우팅 패턴을 지원하며, 주요 메서드는 다음과 같다:</p>
<ul>
<li><code>route()</code>    특정 경로에 대한 컴포넌트 연결</li>
<li><code>index()</code>    부모 경로의 기본 자식 라우트</li>
<li><code>layout()</code>    공통 레이아웃을 적용하는 라우트</li>
<li><code>prefix()</code>    여러 경로에 공통 접두사(prefix) 추가</li>
</ul>
<h3 id="configuring-routes">Configuring Routes</h3>
<p>React Router v7에서는 <code>app/routes.ts</code> 파일에서 경로(route)를 설정한다.
<code>route()</code>에는 두 파라미터가 있다. URL과 일치하는 <strong>URL 패턴</strong>과 동작을 정의하는 컴포넌트에 대한 <strong>파일 경로</strong>다.</p>
<pre><code class="language-ts">// app/routes.ts
import {
  type RouteConfig,
  route,
} from &quot;@react-router/dev/routes&quot;;

export default [
  route(&quot;some/path&quot;, &quot;./some/file.tsx&quot;),
  //    패턴 ^              ^ 파일경로
] satisfies RouteConfig;</code></pre>
<pre><code class="language-ts">// app/routes.ts
import {
  type RouteConfig,
  route,
  index,
  layout,
  prefix,
} from &quot;@react-router/dev/routes&quot;;

export default [
  // 기본 경로 (&#39;/&#39;)
  index(&quot;./home.tsx&quot;),

  // &#39;/about&#39; 경로
  route(&quot;about&quot;, &quot;./about.tsx&quot;),

  // &#39;/auth&#39; 관련 경로 (레이아웃 적용)
  layout(&quot;./auth/layout.tsx&quot;, [
    route(&quot;login&quot;, &quot;./auth/login.tsx&quot;),       // &#39;/auth/login&#39;
    route(&quot;register&quot;, &quot;./auth/register.tsx&quot;), // &#39;/auth/register&#39;
  ]),

  // &#39;/concerts&#39;로 시작하는 경로 (접두사 사용)
  ...prefix(&quot;concerts&quot;, [
    index(&quot;./concerts/home.tsx&quot;),          // &#39;/concerts&#39;
    route(&quot;:city&quot;, &quot;./concerts/city.tsx&quot;), // &#39;/concerts/:city&#39;
    route(&quot;trending&quot;, &quot;./concerts/trending.tsx&quot;), // &#39;/concerts/trending&#39;
  ]),
] satisfies RouteConfig;</code></pre>
<table>
<thead>
<tr>
<th>URL</th>
<th>렌더링 컴포넌트</th>
</tr>
</thead>
<tbody><tr>
<td><code>/</code></td>
<td><code>home.tsx</code></td>
</tr>
<tr>
<td><code>/about</code></td>
<td><code>about.tsx</code></td>
</tr>
<tr>
<td><code>/auth/login</code></td>
<td><code>auth/login.tsx</code> (레이아웃 포함)</td>
</tr>
<tr>
<td><code>/auth/register</code></td>
<td><code>auth/register.tsx</code> (레이아웃 포함)</td>
</tr>
<tr>
<td><code>/concerts</code></td>
<td><code>concerts/home.tsx</code></td>
</tr>
<tr>
<td><code>/concerts/seoul</code></td>
<td><code>concerts/city.tsx</code> (<code>params.city = &quot;seoul&quot;</code>)</td>
</tr>
<tr>
<td><code>/concerts/trending</code></td>
<td><code>concerts/trending.tsx</code></td>
</tr>
</tbody></table>
<p>📌 파일 기반 라우팅 (fs-routes)
<code>@react-router/fs-routes</code> 패키지를 사용하면 폴더 구조에 따라 자동으로 라우트를 구성할 수 있다.</p>
<pre><code class="language-ts">// app/routes.ts
import { type RouteConfig, route } from &quot;@react-router/dev/routes&quot;;
import { flatRoutes } from &quot;@react-router/fs-routes&quot;;

export default [
  route(&quot;/&quot;, &quot;./home.tsx&quot;), // 수동으로 정의한 기본 경로
  ...(await flatRoutes()), // 파일 시스템 기반 라우트 자동 생성
] satisfies RouteConfig;</code></pre>
<ul>
<li>flatRoutes() → 파일 시스템을 스캔해 폴더 구조대로 라우트를 생성</li>
<li>파일 시스템 라우팅과 수동 라우트 설정을 혼합할 수 있다.</li>
</ul>
<h3 id="route-modules">Route Modules</h3>
<p><code>routes.ts</code>에서 참조되는 파일은 각 경로의 동작을 정의한다.</p>
<pre><code class="language-ts">// app/routes.ts
route(&quot;teams/:teamId&quot;, &quot;./team.tsx&quot;),
//           route module ^^^^^^^^</code></pre>
<pre><code class="language-tsx">// app/team.tsx
// provides type safety/inference
import type { Route } from &quot;./+types/team&quot;;

// provides `loaderData` to the component
export async function loader({ params }: Route.LoaderArgs) {
  let team = await fetchTeam(params.teamId);
  return { name: team.name };
}

// renders after the loader is done
export default function Component({
  loaderData,
}: Route.ComponentProps) {
  return &lt;h1&gt;{loaderData.name}&lt;/h1&gt;;
}</code></pre>
<p>액션(actions), 헤더(headers), 에러 바운더리(error boundaries) 등 자세한 부분은 다음 포스트에서 다룬다.</p>
<h3 id="nested-routes">Nested Routes</h3>
<p>중첩 라우트(Nested Routes)는 부모 라우트 내부에 자식 라우트를 포함하는 방식이다.
이를 통해 관련된 여러 페이지를 논리적으로 그룹화하고, 공통 UI를 유지하면서 개별 페이지를 동적으로 교체할 수 있다.</p>
<pre><code class="language-ts">// app/routes.ts
import {
  type RouteConfig,
  route,
  index,
} from &quot;@react-router/dev/routes&quot;;

export default [
  // 부모 라우트
  route(&quot;dashboard&quot;, &quot;./dashboard.tsx&quot;, [
    // 자식 라우트
    index(&quot;./home.tsx&quot;), // &quot;/dashboard&quot;
    route(&quot;settings&quot;, &quot;./settings.tsx&quot;), // &quot;/dashboard/settings&quot;
  ]),
] satisfies RouteConfig;</code></pre>
<ul>
<li>dashboard.tsx가 부모 라우트이며, 그 내부에 home.tsx와 settings.tsx가 중첩됨</li>
<li>/dashboard를 방문하면 home.tsx가 렌더링됨</li>
<li>/dashboard/settings를 방문하면 settings.tsx가 렌더링됨</li>
</ul>
<pre><code class="language-tsx">// app/dashboard.tsx
import { Outlet } from &quot;react-router&quot;;

export default function Dashboard() {
  return (
    &lt;div&gt;
      &lt;h1&gt;Dashboard&lt;/h1&gt;
      {/* 자식 라우트(home.tsx 또는 settings.tsx)가 렌더링될 위치 */}
      &lt;Outlet /&gt;
    &lt;/div&gt;
  );
}</code></pre>
<ul>
<li><code>&lt;Outlet /&gt;</code>은 <code>dashboard.tsx</code> 내부에서 자식 라우트가 렌더링될 자리를 지정함</li>
<li><code>/dashboard</code>일 때는 <code>home.tsx</code>가 <code>&lt;Outlet /&gt;</code>에 렌더링됨</li>
<li><code>/dashboard/settings</code>일 때는 <code>settings.tsx</code>가 <code>&lt;Outlet /&gt;</code>에 렌더링됨</li>
</ul>
<h3 id="root-route">Root Route</h3>
<p><code>routes.ts</code>의 모든 경로는 <code>app/root.tsx</code> 모듈 내부에 중첩되어 있다.</p>
<h3 id="layout-routes">Layout Routes</h3>
<p><code>layout()</code>은 새로운 URL 세그먼트를 추가하지 않고, 자식 라우트를 감싸는 부모 역할을 한다.</p>
<pre><code class="language-ts">// app/routes.ts
import {
  type RouteConfig,
  route,
  layout,
  index,
  prefix,
} from &quot;@react-router/dev/routes&quot;;

export default [
  // 마케팅 페이지의 공통 레이아웃을 정의
  layout(&quot;./marketing/layout.tsx&quot;, [
    index(&quot;./marketing/home.tsx&quot;), // `/`
    route(&quot;contact&quot;, &quot;./marketing/contact.tsx&quot;), // `/contact`
  ]),

  // &quot;projects&quot;라는 경로를 가지는 하위 그룹을 생성
  ...prefix(&quot;projects&quot;, [
    index(&quot;./projects/home.tsx&quot;), // `/projects`
    layout(&quot;./projects/project-layout.tsx&quot;, [
      route(&quot;:pid&quot;, &quot;./projects/project.tsx&quot;), // `/projects/:pid`
      route(&quot;:pid/edit&quot;, &quot;./projects/edit-project.tsx&quot;), // `/projects/:pid/edit`
    ]),
  ]),
] satisfies RouteConfig;</code></pre>
<p>위의 설정을 기준으로 경로와 렌더링 결과를 정리하면:</p>
<table>
<thead>
<tr>
<th>URL</th>
<th>렌더링되는 파일</th>
</tr>
</thead>
<tbody><tr>
<td><code>/</code></td>
<td><code>marketing/layout.tsx</code> → <code>marketing/home.tsx</code></td>
</tr>
<tr>
<td><code>/contact</code></td>
<td><code>marketing/layout.tsx</code> → <code>marketing/contact.tsx</code></td>
</tr>
<tr>
<td><code>/projects</code></td>
<td><code>projects/project-layout.tsx</code> → <code>projects/home.tsx</code></td>
</tr>
<tr>
<td><code>/projects/:pid</code></td>
<td><code>projects/project-layout.tsx</code> → <code>projects/project.tsx</code></td>
</tr>
<tr>
<td><code>/projects/:pid/edit</code></td>
<td><code>projects/project-layout.tsx</code> → <code>projects/edit-project.tsx</code></td>
</tr>
</tbody></table>
<p>✔ <code>layout()</code>은 새로운 URL 세그먼트를 추가하지 않지만, 내부적으로 <strong>자식 라우트를 감싸는 부모 역할</strong>을 한다.
✔ <code>/projects/:pid</code> 경로로 접근하면, <code>project-layout.tsx</code>가 먼저 렌더링되고, 그 안의 <code>&lt;Outlet /&gt;</code>을 통해 <code>project.tsx</code>가 출력된다.</p>
<pre><code class="language-tsx"></code></pre>
<p>📌 layout()을 사용할 때 <Outlet />이 필요한 이유
layout()은 단독으로 화면을 렌더링하는 것이 아니라, 자식 라우트를 감싸는 부모 역할을 한다.
이를 위해 <Outlet />을 사용해야 한다.</p>
<pre><code class="language-tsx">// ./projects/project-layout.tsx
import { Outlet } from &quot;react-router&quot;;

export default function ProjectLayout() {
  return (
    &lt;div&gt;
      &lt;aside&gt;Example sidebar&lt;/aside&gt; {/* 모든 하위 페이지에서 공통으로 표시 */}
      &lt;main&gt;
        &lt;Outlet /&gt; {/* 여기에 자식 라우트가 렌더링됨 */}
      &lt;/main&gt;
    &lt;/div&gt;
  );
}</code></pre>
<ul>
<li><code>ProjectLayout</code>은 모든 projects 관련 페이지의 공통 레이아웃을 제공</li>
<li><code>&lt;Outlet /&gt;</code>을 사용하여, <code>projects/:pid</code>, <code>projects/:pid/edit</code> 등의 자식 라우트가 동적으로 렌더링됨</li>
<li><code>projects/:pid</code>로 접근하면, <code>&lt;Outlet /&gt;</code>에 <code>project.tsx</code>가 렌더링됨</li>
<li><code>projects/:pid/edit</code>로 접근하면, <code>&lt;Outlet /&gt;</code>에 <code>edit-project.tsx</code>가 렌더링됨</li>
</ul>
<p>📌 <code>layout()</code> vs <code>route()</code> 차이점**</p>
<table>
<thead>
<tr>
<th>구분</th>
<th><code>layout()</code></th>
<th><code>route()</code></th>
</tr>
</thead>
<tbody><tr>
<td>URL에 영향</td>
<td>❌ 없음</td>
<td>✅ 있음</td>
</tr>
<tr>
<td>공통 레이아웃 제공</td>
<td>✅ 가능</td>
<td>❌ 개별 페이지만 렌더링</td>
</tr>
<tr>
<td><code>&lt;Outlet /&gt;</code> 사용 여부</td>
<td>✅ 필요</td>
<td>❌ 불필요</td>
</tr>
<tr>
<td>용도</td>
<td>여러 개의 페이지를 감싸는 부모 컴포넌트</td>
<td>단일 페이지 정의</td>
</tr>
</tbody></table>
<p>✔ <code>layout()</code>을 사용하면 여러 페이지에서 <strong>공통 레이아웃을 쉽게 유지</strong>할 수 있음
✔ <code>route()</code>는 특정 경로에 해당하는 개별 페이지를 정의하는 용도</p>
<h3 id="index-routes">Index Routes</h3>
<p><code>index()</code>는 부모 URL이 호출되었을 때 기본으로 렌더링될 컴포넌트를 지정한다. 
자식 경로를 가질 수 없다. (즉, index() 내부에는 또 다른 route()를 추가할 수 없음)</p>
<pre><code class="language-ts">// app/routes.ts
import {
  type RouteConfig,
  route,
  index,
} from &quot;@react-router/dev/routes&quot;;

export default [
  // renders into the root.tsx Outlet at /
  index(&quot;./home.tsx&quot;),
  route(&quot;dashboard&quot;, &quot;./dashboard.tsx&quot;, [
    // renders into the dashboard.tsx Outlet at /dashboard
    index(&quot;./dashboard-home.tsx&quot;),
    route(&quot;settings&quot;, &quot;./dashboard-settings.tsx&quot;),
  ]),
] satisfies RouteConfig;</code></pre>
<h3 id="route-prefixes">Route Prefixes</h3>
<p><code>prefix</code>를 사용하면 공통 경로(prefix)를 여러 개의 하위 경로에 적용할 수 있다.
즉, 부모 레이아웃 없이 특정 그룹의 경로에 동일한 접두사를 추가하는 기능이다.</p>
<p>다음 경로에 대하여
✅ /projects
✅ /projects/:pid
✅ /projects/:pid/edit</p>
<p>부모 레이아웃 사용</p>
<pre><code class="language-tsx">layout(&quot;./projects/layout.tsx&quot;, [
  index(&quot;./projects/home.tsx&quot;),
  route(&quot;:pid&quot;, &quot;./projects/project.tsx&quot;),
  route(&quot;:pid/edit&quot;, &quot;./projects/edit-project.tsx&quot;),
]);</code></pre>
<ul>
<li>모든 하위 경로가 <code>layout.tsx</code>를 거쳐야 한다. <code>layout.tsx</code>가 필요하지 않다면 불필요한 중첩을 만들게 된다.</li>
</ul>
<p>prefix를 사용한 개선된 방식</p>
<pre><code class="language-tsx">export default [
  ...prefix(&quot;projects&quot;, [
    index(&quot;./projects/home.tsx&quot;),
    layout(&quot;./projects/project-layout.tsx&quot;, [
      route(&quot;:pid&quot;, &quot;./projects/project.tsx&quot;),
      route(&quot;:pid/edit&quot;, &quot;./projects/edit-project.tsx&quot;),
    ]),
  ]),
];</code></pre>
<ul>
<li><code>projects/</code>라는 공통 접두사가 자동 적용된다.</li>
<li><code>/projects/</code>는 레이아웃 없이 직접 렌더링된다.</li>
<li><code>/projects/:pid</code>, <code>/projects/:pid/edit</code>는 레이아웃을 적용한다.</li>
</ul>
<h3 id="dynamic-segments">Dynamic Segments</h3>
<p>동적 세그먼트(Dynamic Segments)는 경로의 특정 부분을 변수처럼 사용하는 기능이다.
경로에서 :paramName 형태로 작성하면 URL에서 해당 값을 추출하여 params 객체로 전달할 수 있다.</p>
<pre><code class="language-ts">// app/routes.ts
route(&quot;teams/:teamId&quot;, &quot;./team.tsx&quot;),</code></pre>
<ul>
<li>✅ /teams/123</li>
<li>✅ /teams/abc</li>
</ul>
<pre><code class="language-tsx">// app/team.tsx
import type { Route } from &quot;./+types/team&quot;;

export async function loader({ params }: Route.LoaderArgs) {
  //                           ^? { teamId: string }
}

export default function Component({
  params,
}: Route.ComponentProps) {
  params.teamId;
  //        ^ string
}</code></pre>
<p>하나의 경로에서 여러 개의 동적 세그먼트를 사용할 수도 있다.</p>
<pre><code class="language-ts">// app/routes.ts
route(&quot;c/:categoryId/p/:productId&quot;, &quot;./product.tsx&quot;),</code></pre>
<ul>
<li>✅ /c/electronics/p/5678</li>
<li>✅ /c/books/p/1234<pre><code class="language-tsx">// app/product.tsx
import type { Route } from &quot;./+types/product&quot;;
</code></pre>
</li>
</ul>
<p>async function loader({ params }: LoaderArgs) {
  //                    ^? { categoryId: string; productId: string }
}</p>
<pre><code>
❗ 주의할 점
각 동적 세그먼트는 고유해야 한다.
동일한 이름의 세그먼트를 사용하면 나중에 정의된 값이 기존 값을 덮어쓴다.
```tsx
route(&quot;:id/:id&quot;, &quot;./error.tsx&quot;); // 잘못된 예제</code></pre><h3 id="optional-segments">Optional Segments</h3>
<p>세그먼트 끝에 ?를 추가하여 경로 세그먼트를 선택 사항으로 만들 수 있다.</p>
<p>경로 매개변수(:param 형태)를 선택적으로 허용할 수 있다.</p>
<pre><code class="language-ts">// app/routes.ts
route(&quot;:lang?/categories&quot;, &quot;./categories.tsx&quot;),</code></pre>
<ul>
<li>✅ /categories</li>
<li>✅ /en/categories</li>
<li>✅ /ko/categories</li>
</ul>
<p>동적인 매개변수뿐만 아니라 일반적인 경로 세그먼트도 선택적으로 만들 수 있다.</p>
<pre><code class="language-ts">// app/routes.ts
route(&quot;users/:userId/edit?&quot;, &quot;./user.tsx&quot;);</code></pre>
<ul>
<li>✅ /users/123</li>
<li>✅ /users/123/edit</li>
</ul>
<h3 id="splats">Splats</h3>
<p>스플랫(Splats)은 catchall 또는 star segments라고도 하며, <code>/*</code> 패턴을 사용하여 경로의 나머지 부분을 모두 포함하는 라우팅 방식이다.
즉, 특정 경로 이후의 모든 하위 경로를 하나의 변수로 받아올 수 있다.</p>
<pre><code class="language-ts">// app/routes.ts
route(&quot;files/*&quot;, &quot;./files.tsx&quot;),</code></pre>
<ul>
<li><code>/files/</code>로 시작하는 모든 URL을 files.tsx에서 처리한다.</li>
</ul>
<pre><code class="language-tsx">// app/files.tsx
export async function loader({ params }: Route.LoaderArgs) {
  console.log(params[&quot;*&quot;]); // &quot;docs/readme.md&quot; (예시)
}</code></pre>
<ul>
<li>params[&quot;*&quot;]을 사용하면 files/ 이후의 모든 문자열을 가져올 수 있다.</li>
</ul>
<p>Splats를 변수명으로 할당하기</p>
<pre><code class="language-tsx">const { &quot;*&quot;: splat } = params;
console.log(splat); // &quot;docs/readme.md&quot;</code></pre>
<ul>
<li>구조 분해 할당을 이용해 params[&quot;*&quot;]를 splat이라는 이름으로 바꿀 수 있다.</li>
</ul>
<h3 id="component-routes">Component Routes</h3>
<p>Component Routes는 URL에 따라 특정 컴포넌트를 렌더링하는 기능을 제공한다.</p>
<pre><code class="language-tsx">import { Routes, Route } from &quot;react-router&quot;;

function Wizard() {
  return (
    &lt;div&gt;
      &lt;h1&gt;Some Wizard with Steps&lt;/h1&gt;
      &lt;Routes&gt;
        &lt;Route index element={&lt;StepOne /&gt;} /&gt;
        &lt;Route path=&quot;step-2&quot; element={&lt;StepTwo /&gt;} /&gt;
        &lt;Route path=&quot;step-3&quot; element={&lt;StepThree /&gt;} /&gt;
      &lt;/Routes&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>🔹 Wizard 컴포넌트 내부에서 <code>&lt;Routes&gt;</code>를 사용 → 이 컴포넌트는 자체적으로 라우팅을 관리한다.
🔹 <code>index</code> → 기본적으로 렌더링될 <code>StepOne</code> 컴포넌트
🔹 <code>step-2</code>, <code>step-3</code> → URL이 <code>/step-2</code>, <code>/step-3</code>일 때 각각 <code>StepTwo</code>, <code>StepThree</code>를 렌더링</p>
<p>다만, Component Routes 방식은 기존의 route module 방식과 달리 몇 가지 제약이 있다.</p>
<p>❌지원되지 않는 기능:</p>
<ul>
<li>데이터 로딩 (Loaders) → URL 이동 시 서버에서 데이터를 미리 가져오는 기능이 없음</li>
<li>액션 (Actions) → form을 통한 데이터 변경 및 자동 UI 갱신 기능이 없음</li>
<li>코드 스플리팅 (Code Splitting) → 필요한 페이지의 코드만 로드하는 기능이 없음</li>
<li>SEO 설정 → 페이지별 메타태그 관리 기능이 없음</li>
</ul>
<p>✅ 주로 사용하는 경우:</p>
<ul>
<li>다단계 마법사(Wizard) UI처럼 특정 컴포넌트 내에서 단계별 이동이 필요한 경우</li>
<li>앱 내부에서 별도의 작은 라우팅을 만들고 싶을 때</li>
<li>데이터 로딩과 관련 없는 단순한 UI 라우팅이 필요할 때</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Router v7 소개]]></title>
            <link>https://velog.io/@feels_bot_man/React-Router-v7-%EC%86%8C%EA%B0%9C</link>
            <guid>https://velog.io/@feels_bot_man/React-Router-v7-%EC%86%8C%EA%B0%9C</guid>
            <pubDate>Wed, 19 Feb 2025 13:47:32 GMT</pubDate>
            <description><![CDATA[<h2 id="react-router-v7">React Router v7</h2>
<p>지금까지의 React Router는 단순한 라우팅 라이브러리였지만, 이제는 Next.js처럼 자체적인 데이터 관리와 렌더링 방식까지 제공하는 프레임워크로 발전하고 있다.
Remix 팀은 Remix와 React Router의 기능이 매우 유사해졌다는 점을 인식하고, 두 프로젝트를 통합하기로 결정했다. React Router v7은 Remix의 모든 주요 기능을 포함하게 되었다. </p>
<h3 id="react-router를-라이브러리로-사용하는-경우">React Router를 라이브러리로 사용하는 경우</h3>
<p>기존 React Router v6처럼 <strong>클라이언트 측 라우팅(SPA)</strong>을 위한 라이브러리로 사용한다.</p>
<p>특징</p>
<ul>
<li>URL을 컴포넌트와 매칭하는 역할만 수행</li>
<li>URL 데이터 제공 및 페이지 간 이동 지원</li>
<li>싱글 페이지 앱(SPA)에 적합: 자체적인 프론트엔드 인프라를 가진 앱에서 쉽게 적용 가능</li>
<li>오프라인 및 동기(sync) 아키텍처에 유리: 서버 상태 변화 없이 즉각적인 로컬 반응 가능</li>
<li>서버 렌더링(SSR), SEO, 코드 스플리팅 같은 기능이 없음</li>
<li>v6에서 업그레이드할 때 큰 변경 없이 사용 가능</li>
</ul>
<h3 id="react-router를-프레임워크로-사용하는-경우">React Router를 프레임워크로 사용하는 경우</h3>
<p>React Router를 완전한 프레임워크처럼 사용할 수도 있다.</p>
<p>특징</p>
<ul>
<li>Vite 번들러 및 개발 서버와 통합</li>
<li>핫 모듈 교체(HMR) 지원</li>
<li>파일 기반 또는 설정 기반 라우팅</li>
<li>타입 안전한 데이터 로딩(loaders) 및 액션(actions) 지원</li>
<li>자동 페이지 데이터 리로드</li>
<li>서버 사이드 렌더링(SSR), SPA, 정적 렌더링 지원</li>
<li>SEO, 에러 경계, 자산(asset) 로딩 관리 가능</li>
</ul>
<h3 id="결론">결론</h3>
<table>
<thead>
<tr>
<th></th>
<th>라이브러리</th>
<th>프레임워크</th>
</tr>
</thead>
<tbody><tr>
<td><strong>라우팅 방식</strong></td>
<td><code>&lt;Routes&gt;</code>와 <code>&lt;Route&gt;</code>로 정의</td>
<td><code>routes.ts</code>에서 설정</td>
</tr>
<tr>
<td><strong>코드 스플리팅</strong></td>
<td>없음</td>
<td>자동 적용</td>
</tr>
<tr>
<td><strong>데이터 로딩</strong></td>
<td><code>useEffect + fetch</code> 사용</td>
<td><code>loader</code> 제공</td>
</tr>
<tr>
<td><strong>데이터 업데이트</strong></td>
<td>수동 처리 필요</td>
<td><code>action</code> 사용</td>
</tr>
<tr>
<td><strong>SEO 최적화</strong></td>
<td>없음</td>
<td>자동 처리 가능</td>
</tr>
<tr>
<td><strong>SSR 지원</strong></td>
<td>❌ (클라이언트 사이드 전용)</td>
<td>✅</td>
</tr>
<tr>
<td><strong>타입 안전성</strong></td>
<td>직접 관리해야 함</td>
<td>자동 적용</td>
</tr>
<tr>
<td><strong>핫 모듈 교체(HMR)</strong></td>
<td>지원 안 함</td>
<td>지원</td>
</tr>
</tbody></table>
<p>React Router를 단순히 라우팅을 위한 라이브러리로 사용할 수도 있고, 프레임워크처럼 완전한 데이터 관리 및 SSR 기능까지 포함한 풀스택 환경으로 사용할 수도 있다.
만약 기존 React Router v6처럼 간단한 클라이언트 측 라우팅을 원한다면 라이브러리 모드가 적합하다. 더 강력한 기능(SSR, 데이터 로딩, 타입 안전성 등)을 원한다면 프레임워크 모드를 활용할 수 있다.</p>
<p>향후 <a href="https://reactrouter.com/start/framework/installation">React Router: The Framework</a>의 공식문서를 정리하고자 한다.</p>
<p>참고 <a href="https://velog.io/@hazae23/Merging-Remix-and-React-Router">https://velog.io/@hazae23/Merging-Remix-and-React-Router</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[vite + React + typescript] 초기설정 tsconfig.app.json의 'tsBuildInfoFile', 'noUncheckedSideEffectImports' 에러 해결 ]]></title>
            <link>https://velog.io/@feels_bot_man/vite-React-typescript-%EC%B4%88%EA%B8%B0%EC%84%A4%EC%A0%95-tsconfig.app.json%EC%9D%98-tsBuildInfoFile-noUncheckedSideEffectImports-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@feels_bot_man/vite-React-typescript-%EC%B4%88%EA%B8%B0%EC%84%A4%EC%A0%95-tsconfig.app.json%EC%9D%98-tsBuildInfoFile-noUncheckedSideEffectImports-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Sat, 25 Jan 2025 03:02:36 GMT</pubDate>
            <description><![CDATA[<h3 id="문제점">문제점</h3>
<p>vscode를 사용할 때, vite + React + typescript의 초기 tsconfig.app.json에는 두 가지가 에러가 표시된다.</p>
<ol>
<li>Option &#39;tsBuildInfoFile&#39; cannot be specified without specifying option &#39;incremental&#39; or option &#39;composite&#39;.</li>
</ol>
<ol start="2">
<li>Unknown compiler option &#39;noUncheckedSideEffectImports&#39;.</li>
</ol>
<h3 id="해결">해결</h3>
<ol>
<li>첫 번째의 경우 &quot;incremental&quot;: true, 옵션을 추가하면 해결된다.<pre><code class="language-json">&quot;compilerOptions&quot;: {
 &quot;incremental&quot;: true,
 &quot;tsBuildInfoFile&quot;: &quot;./node_modules/.tmp/tsconfig.app.tsbuildinfo&quot;,</code></pre>
</li>
</ol>
<p>incremental 옵션: <a href="https://www.typescriptlang.org/tsconfig/#incremental">https://www.typescriptlang.org/tsconfig/#incremental</a></p>
<ol start="2">
<li>두 번째의 경우 vscode내장된 TypeScript의 버전이 5.6.3 미만이기 때문. </li>
</ol>
<ul>
<li><p>아무 <strong>타입스크립트 파일</strong>을 열어놓고<code>Ctrl+Shift+P</code>를 눌러서 TypeScript: Select TypeScript Version에 들어가서 버전을 5.6.3 이상을 선택하면 해결됨</p>
</li>
<li><p>만약 선택지에 최신 버전의 타입스크립트가 없는 경우, 선택지에 전역 설치된 TypeScript가 나오게 하는 방법:</p>
<ol>
<li><code>.vscode</code> 폴더를 프로젝트 최상단에 만들고</li>
<li>하위에 <code>settings.json</code> 파일 생성 (아래 예시는 윈도우의 경우)</li>
</ol>
</li>
</ul>
<pre><code class="language-json">{
  &quot;typescript.tsdk&quot;: &quot;C:\\Users\\&lt;유저명&gt;\\AppData\\Roaming\\npm\\node_modules\\typescript\\lib&quot;
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Immer와 불변성]]></title>
            <link>https://velog.io/@feels_bot_man/Immer%EC%99%80-%EB%B6%88%EB%B3%80%EC%84%B1</link>
            <guid>https://velog.io/@feels_bot_man/Immer%EC%99%80-%EB%B6%88%EB%B3%80%EC%84%B1</guid>
            <pubDate>Tue, 21 Jan 2025 09:17:10 GMT</pubDate>
            <description><![CDATA[<p>직접적으로 보이는 코드에서 Immer를 import하거나 사용하는 부분이 보이지 않더라도 프로젝트가 Redux Toolkit을 사용하고 있다면 Redux Toolkit은 내부적으로 Immer를 사용한다.</p>
<h2 id="immer">Immer?</h2>
<p>immer는 JavaScript에서 <strong>불변성(immutability)</strong>을 관리하기 쉽게 해주는 라이브러리다.  특히, 상태 관리 라이브러리에서 불변성을 유지하는 것이 중요한데, immer는 이를 간단하고 효율적으로 처리할 수 있게 도와준다.</p>
<h3 id="불변성immutability">불변성(immutability)?</h3>
<p>불변성은 데이터를 직접 변경하지 않고, 변경하려는 데이터를 복사하여 새로 만든 뒤 수정하는 방식을 의미한다.</p>
<p>Redux에서는 상태(state)가 <strong>불변(immutable)</strong>해야 한다. 왜냐하면 Redux는 상태 변경을 감지하기 위해 참조(reference) 비교를 사용하기 때문이다.</p>
<p>만약에 상태를 직접 수정할 경우</p>
<ol>
<li>state 객체가 직접 수정되면 기존 상태와 새로운 상태가 같은 참조를 가짐.</li>
<li>Redux는 상태가 변경되었다고 인지하지 못함.</li>
<li>React 컴포넌트가 리렌더링되지 않음.</li>
</ol>
<p>즉, 상태를 직접 수정하는 대신 기존 상태를 복사하고 변경된 새 상태를 반환해야 한다는 것이다.</p>
<p>JavaScript에서는 객체와 배열이 참조 타입(reference type)이기 때문에, 불변성을 유지하려면 보통 객체나 배열을 복사한 뒤 수정해야 한다. 변경된 경로만 새로운 객체가 되고, 변경되지 않은 부분은 기존 객체의 참조를 유지해서 불필요한 메모리 사용을 줄이려고 하기 때문이다.</p>
<pre><code class="language-javascript">const state = { count: 0 };

// 불변성을 유지하기 위해 새로운 객체를 생성
const newState = { ...state, count: state.count + 1 };
console.log(newState); // { count: 1 }
console.log(state);    // { count: 0 } (원본은 변경되지 않음)</code></pre>
<p>하지만 문제는, 복잡한 중첩 구조의 객체를 다룰 때 불변성을 유지하는 코드가 번잡하고 실수가 많아질 수 있다.😫</p>
<pre><code class="language-javascript">const state = {
    user: {
        profile: {
            name: &quot;John&quot;,
            age: 30
        }
    }
};

const newState = {
    ...state,
    user: {
        ...state.user,
        profile: {
            ...state.user.profile,
            age: 31
        }
    }
};
</code></pre>
<h3 id="immer의-장점">immer의 장점</h3>
<p>깊은 중첩 구조도 쉽게 업데이트 가능해진다.</p>
<pre><code class="language-javascript">const state = {
    user: {
        profile: {
            name: &quot;John&quot;,
            age: 30
        }
    }
};

const newState = produce(state, draft =&gt; {
    draft.user.profile.age = 31; // 간단하게 작성
});

console.log(newState.user.profile.age); // 31
console.log(state.user.profile.age);    // 30
</code></pre>
<p>상태를 &quot;수정하는 것처럼&quot; 작성하면 되므로 가독성이 높아진다.👍</p>
<h2 id="결론">결론</h2>
<ul>
<li>Redux 상태 관리에서 리듀서를 작성할 때.</li>
<li>React 컴포넌트에서 복잡한 상태를 업데이트할 때.</li>
<li>중첩된 데이터 구조를 간단히 수정하고 싶을 때.</li>
</ul>
<p>immer는 복잡한 불변성 코드를 간단하고 안전하게 작성할 수 있게 해주는 도구로, 상태 관리 작업을 훨씬 쉽게 만들어준다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[파이썬에도 JavaScript의 const와 같은 상수 선언 키워드가 있을까?]]></title>
            <link>https://velog.io/@feels_bot_man/%ED%8C%8C%EC%9D%B4%EC%8D%AC%EC%97%90%EB%8F%84-JavaScript%EC%9D%98-const%EC%99%80-%EA%B0%99%EC%9D%80-%EC%83%81%EC%88%98-%EC%84%A0%EC%96%B8-%ED%82%A4%EC%9B%8C%EB%93%9C%EA%B0%80-%EC%9E%88%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@feels_bot_man/%ED%8C%8C%EC%9D%B4%EC%8D%AC%EC%97%90%EB%8F%84-JavaScript%EC%9D%98-const%EC%99%80-%EA%B0%99%EC%9D%80-%EC%83%81%EC%88%98-%EC%84%A0%EC%96%B8-%ED%82%A4%EC%9B%8C%EB%93%9C%EA%B0%80-%EC%9E%88%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Sat, 04 Jan 2025 09:12:34 GMT</pubDate>
            <description><![CDATA[<p>파이썬에는 <strong>JavaScript의 <code>const</code>와 같은 상수 선언 키워드</strong>는 없다. 하지만, <strong>상수처럼 사용할 변수</strong>를 선언하는 관례가 있다.</p>
<hr>
<h3 id="파이썬에서-상수-선언-관례">파이썬에서 상수 선언 관례</h3>
<p>파이썬에서는 <strong>상수를 선언할 수 있는 특별한 문법이 없지만</strong>, <strong>변수 이름을 모두 대문자로</strong> 작성하여 <strong>상수처럼 사용</strong>하는 것이 일반적인 관례이다. 이는 개발자 간의 암묵적 규칙으로, 상수로 선언된 변수는 변경하지 말라는 의미를 전달한다.</p>
<h4 id="예시">예시:</h4>
<pre><code class="language-python">PI = 3.14159
MAX_CONNECTIONS = 100
API_KEY = &quot;your-api-key-here&quot;</code></pre>
<ul>
<li><code>PI</code>, <code>MAX_CONNECTIONS</code>, <code>API_KEY</code>는 상수로 사용된다고 간주된다.</li>
<li>하지만 <strong>문법적으로 값을 변경하는 것을 막을 수는 없다</strong>.<pre><code class="language-python">PI = 3.14  # 변경 가능 (경고나 오류 없음)</code></pre>
</li>
</ul>
<hr>
<h3 id="상수를-엄격히-보호하는-방법">상수를 엄격히 보호하는 방법</h3>
<p>상수를 변경하지 못하도록 강제하려면, 다음과 같은 방법을 사용할 수 있다:</p>
<h4 id="1-클래스를-활용한-상수-선언">1. <strong>클래스를 활용한 상수 선언</strong></h4>
<pre><code class="language-python">class Constants:
    PI = 3.14159
    MAX_CONNECTIONS = 100

# 상수처럼 사용
print(Constants.PI)</code></pre>
<p>이 경우, <code>Constants.PI</code>는 변경할 수는 있지만, 관례적으로 상수처럼 사용된다.</p>
<h4 id="2-frozen-데이터-구조-사용-eg-frozen-dataclass">2. <strong><code>frozen</code> 데이터 구조 사용 (e.g., <code>frozen dataclass</code>)</strong></h4>
<p>Python 3.8+에서는 <code>@dataclass(frozen=True)</code>를 사용하여 상수 같은 동작을 구현할 수 있다:</p>
<pre><code class="language-python">from dataclasses import dataclass

@dataclass(frozen=True)
class Constants:
    PI: float = 3.14159
    MAX_CONNECTIONS: int = 100

# 상수처럼 사용
constants = Constants()
print(constants.PI)

# constants.PI = 3.14  # 오류 발생 (상수 값을 변경하려고 시도)</code></pre>
<h4 id="3-typingfinal-사용">3. <strong><code>typing.Final</code> 사용</strong></h4>
<p>Python 3.8+에서는 <code>Final</code>을 통해 상수임을 표현할 수 있다:</p>
<pre><code class="language-python">from typing import Final

PI: Final = 3.14159
MAX_CONNECTIONS: Final = 100

# PI = 3.14  # MyPy를 사용하면 경고 발생 (런타임에서는 오류 없음)</code></pre>
<hr>
<h3 id="결론">결론</h3>
<p>파이썬에서 <strong>상수를 선언하는 공식 키워드</strong>는 없지만, <strong>대문자 변수명</strong>을 사용하거나 <strong><code>Final</code> 또는 <code>@dataclass(frozen=True)</code></strong> 같은 기능을 활용해 상수처럼 사용할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Sequelize Sync를 반복 실행하며 발생한 인덱스 누적 문제]]></title>
            <link>https://velog.io/@feels_bot_man/Sequelize-Sync%EB%A5%BC-%EB%B0%98%EB%B3%B5-%EC%8B%A4%ED%96%89%ED%95%98%EB%A9%B0-%EB%B0%9C%EC%83%9D%ED%95%9C-%EC%9D%B8%EB%8D%B1%EC%8A%A4-%EB%88%84%EC%A0%81-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@feels_bot_man/Sequelize-Sync%EB%A5%BC-%EB%B0%98%EB%B3%B5-%EC%8B%A4%ED%96%89%ED%95%98%EB%A9%B0-%EB%B0%9C%EC%83%9D%ED%95%9C-%EC%9D%B8%EB%8D%B1%EC%8A%A4-%EB%88%84%EC%A0%81-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Thu, 19 Dec 2024 02:55:48 GMT</pubDate>
            <description><![CDATA[<p>Sequelize를 사용하면서 개발 초기에 테이블의 스키마를 자동으로 동기화하기 위해 <code>sequelize.sync({ alter: true })</code>를 사용했다. 이 옵션은 기존 테이블을 수정하면서 새로운 필드나 변경 사항을 반영해 주는 유용한 기능이지만, 반복적으로 실행하면서 예상치 못한 문제가 발생했다.</p>
<hr>
<h4 id="문제-상황-인덱스-누적">문제 상황: 인덱스 누적</h4>
<p><code>alter: true</code> 옵션을 사용한 상태에서 여러 번 <code>sequelize.sync()</code>를 실행했더니, 데이터베이스 테이블에 설정된 인덱스가 점점 누적되었다. 결국 특정 테이블의 인덱스 개수가 <strong>64개를 초과</strong>하며 아래와 같은 오류가 발생했다:</p>
<pre><code>Error: Too many indexes on table &#39;your_table_name&#39;; the limit is 64</code></pre><p>해당 문제는 테이블에 이미 존재하는 인덱스와 새로운 인덱스가 계속 추가되는 방식으로 동작하기 때문이다. <code>sequelize.sync()</code>가 테이블 스키마를 업데이트하면서 인덱스를 중복 생성하는 구조적 한계를 드러낸 것이다.</p>
<hr>
<h4 id="원인-분석">원인 분석</h4>
<p><code>sequelize.sync({ alter: true })</code>는 다음과 같이 작동한다.</p>
<ol>
<li>데이터베이스의 기존 테이블 구조와 모델 정의를 비교</li>
<li>차이가 발견되면, 테이블 구조를 업데이트</li>
<li><strong>인덱스</strong>와 같은 제약 조건도 업데이트 대상에 포함</li>
<li>기존 인덱스를 삭제하지 않고 새롭게 추가하기 때문에 동일한 필드에 대해 중복된 인덱스가 생성될 수 있다.</li>
</ol>
<p>이 과정을 반복 실행하면 인덱스가 계속 누적되면서 데이터베이스의 인덱스 제한에 도달하게 된다.</p>
<hr>
<h4 id="해결-방법">해결 방법</h4>
<ol>
<li><p><strong><code>alter</code> 대신 <code>force</code> 옵션 사용</strong></p>
<ul>
<li><p><code>force: true</code> 옵션은 테이블을 완전히 삭제하고 다시 생성한다.</p>
</li>
<li><p>기존 데이터도 모두 삭제되므로 초기 개발 환경이나 데이터가 없어도 되는 상황에서만 사용 가능하다.</p>
<pre><code class="language-javascript">await sequelize.sync({ force: true });</code></pre>
<p>이 방법은 반복 실행 시에도 인덱스가 누적되지 않으며, 테이블 정의와 인덱스가 항상 모델과 일치하도록 보장한다.</p>
</li>
</ul>
</li>
<li><p><strong>인덱스 관리 로직 추가</strong></p>
<ul>
<li><p><code>sequelize.sync()</code>를 사용하지 않고, 데이터베이스 마이그레이션 도구(예: Sequelize CLI)를 활용해 명시적으로 인덱스를 관리한다.</p>
</li>
<li><p>기존 인덱스를 삭제하거나 필요한 경우에만 추가하도록 설계한다.</p>
<pre><code class="language-javascript">// 예: 마이그레이션 파일에서 인덱스 추가
await queryInterface.addIndex(&#39;your_table&#39;, [&#39;your_column&#39;], {
name: &#39;your_index_name&#39;,
unique: true,
});</code></pre>
</li>
</ul>
</li>
</ol>
<hr>
<h4 id="결론">결론</h4>
<p><code>sequelize.sync({ alter: true })</code>는 빠르고 간편한 옵션이지만, 반복 실행 시 예상치 못한 부작용을 초래할 수 있다. 특히 <strong>인덱스 누적 문제</strong>는 예기치 못한 영향을 미칠 수 있으므로 주의가 필요하다.</p>
<p>개발 환경에서는 <code>force: true</code> 옵션을 사용하거나, 프로덕션 환경에서는 명시적으로 마이그레이션 도구를 활용해 데이터베이스를 관리하는 것이 좋겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[result = result++;와 result = ++result;의 차이]]></title>
            <link>https://velog.io/@feels_bot_man/result-result%EC%99%80-result-result%EC%9D%98-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@feels_bot_man/result-result%EC%99%80-result-result%EC%9D%98-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Wed, 18 Dec 2024 02:25:58 GMT</pubDate>
            <description><![CDATA[<p><code>result = result++;</code>와 <code>result = ++result;</code>는 <strong>증감 연산자</strong>의 동작 방식 때문에 결과가 다르다. 이를 이해하려면 <strong>후위 연산자(<code>result++</code>)</strong>와 <strong>전위 연산자(<code>++result</code>)</strong>의 차이를 알아야 한다.</p>
<hr>
<h3 id="1-후위-연산자-result"><strong>1. 후위 연산자 (<code>result++</code>)</strong></h3>
<ul>
<li><strong>값을 먼저 반환한 후, 증가</strong>한다.</li>
<li>즉, <code>result++</code>는 현재 값을 반환하고 나서 <code>result</code>를 1 증가시킨다.</li>
</ul>
<h3 id="2-전위-연산자-result"><strong>2. 전위 연산자 (<code>++result</code>)</strong></h3>
<ul>
<li><strong>값을 먼저 증가한 후, 반환</strong>한다.</li>
<li>즉, <code>++result</code>는 <code>result</code>를 먼저 1 증가시키고 나서 증가된 값을 반환한다.</li>
</ul>
<hr>
<h3 id="코드-분석"><strong>코드 분석</strong></h3>
<h4 id="1-result--result"><strong>(1) result = result++;</strong></h4>
<ol>
<li><code>result++</code>는 <strong>후위 연산자</strong>이므로, 현재 값이 반환된 후 <code>result</code>가 1 증가한다.</li>
<li>하지만, <code>result =</code>로 인해 반환된 <strong>현재 값</strong>이 다시 <code>result</code>에 할당된다.</li>
<li>따라서 <strong>증가한 값은 덮어쓰여져 무시</strong>된다.<ul>
<li>결과: <code>result</code>의 값이 <strong>변하지 않는다</strong>.</li>
</ul>
</li>
</ol>
<p><strong>예시:</strong></p>
<pre><code class="language-javascript">let result = 5;
result = result++;
console.log(result); // 출력: 5</code></pre>
<h4 id="2-result--result"><strong>(2) result = ++result;</strong></h4>
<ol>
<li><code>++result</code>는 <strong>전위 연산자</strong>이므로, <code>result</code>를 먼저 1 증가시킨다.</li>
<li>증가된 값이 <code>result =</code>에 의해 다시 <code>result</code>에 할당된다.</li>
<li>결과적으로, <strong><code>result</code>는 정상적으로 1 증가</strong>한다.</li>
</ol>
<p><strong>예시:</strong></p>
<pre><code class="language-javascript">let result = 5;
result = ++result;
console.log(result); // 출력: 6</code></pre>
<hr>
<h3 id="정리"><strong>정리</strong></h3>
<table>
<thead>
<tr>
<th>표현식</th>
<th>설명</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td><code>result = result++;</code></td>
<td><code>result</code>의 현재 값이 반환되고, 증가된 값이 덮어써져 무시됨.</td>
<td>변하지 않음</td>
</tr>
<tr>
<td><code>result = ++result;</code></td>
<td><code>result</code>를 먼저 증가시킨 후, 증가된 값이 다시 <code>result</code>에 할당됨.</td>
<td>1 증가</td>
</tr>
</tbody></table>
<hr>
<h3 id="왜-이런-차이가-발생하는가"><strong>왜 이런 차이가 발생하는가?</strong></h3>
<p><code>result = result++;</code>는 <strong>값을 증가시키는 작업과 다시 할당하는 작업</strong>이 충돌하면서 발생한다.  </p>
<ul>
<li><code>result++</code>의 반환값(증가 전 값)이 <code>result</code>에 덮어써지므로 실제 증가한 값이 적용되지 않는다.<br><code>++result</code>는 증가 후 값을 반환하므로 이런 충돌이 발생하지 않는다.</li>
</ul>
<blockquote>
<p><strong>Tip:</strong> 코드의 명확성을 위해 <code>result++</code>와 <code>++result</code>를 대입문과 함께 사용하는 것은 지양하는 것이 좋다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 개발자 초보가 볼 만한 밀리의 서재 책들]]></title>
            <link>https://velog.io/@feels_bot_man/%EC%9B%B9-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%B4%88%EB%B3%B4%EA%B0%80-%EB%B3%BC-%EB%A7%8C%ED%95%9C-%EB%B0%80%EB%A6%AC%EC%9D%98-%EC%84%9C%EC%9E%AC-%EC%B1%85%EB%93%A4</link>
            <guid>https://velog.io/@feels_bot_man/%EC%9B%B9-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%B4%88%EB%B3%B4%EA%B0%80-%EB%B3%BC-%EB%A7%8C%ED%95%9C-%EB%B0%80%EB%A6%AC%EC%9D%98-%EC%84%9C%EC%9E%AC-%EC%B1%85%EB%93%A4</guid>
            <pubDate>Fri, 13 Dec 2024 07:12:12 GMT</pubDate>
            <description><![CDATA[<p>밀리의 서재에 있는, 읽어본 책들과 읽으려고 준비중인 책들을 모아보았습니다.</p>
<h2 id="ai">AI</h2>
<h3 id="요즘-ai-페어-프로그래밍">요즘 AI 페어 프로그래밍</h3>
<p>AI 시대에 개발자로 살아남는 29가지 LLM 프롬프트 엔지니어링 with 깃허브 코파일럿, 클로드, 챗GPT
<img src="https://velog.velcdn.com/images/feels_bot_man/post/2842653e-15ce-4b23-94de-7ddc87807834/image.jpg" alt=""></p>
<h2 id="코딩테스트">코딩테스트</h2>
<h3 id="코딩-테스트-합격자-되기---파이썬-편">코딩 테스트 합격자 되기 - 파이썬 편</h3>
<p>자료구조, 알고리즘, 빈출 100 문제로 대비하는 코테 풀 패키지
<img src="https://velog.velcdn.com/images/feels_bot_man/post/490516b6-f816-41a9-a0ca-5b0b8ecbeada/image.jpg" alt=""></p>
<h3 id="코딩-테스트-합격자-되기---자바스크립트-편">코딩 테스트 합격자 되기 - 자바스크립트 편</h3>
<p>프로그래머스 제공 100 문제로 완벽 대비
<img src="https://velog.velcdn.com/images/feels_bot_man/post/0477363c-15b6-46a7-965c-636f8589b79f/image.jpg" alt=""></p>
<h2 id="데이터베이스">데이터베이스</h2>
<h3 id="기초부터-차근차근-데이터베이스-배우기">기초부터 차근차근 데이터베이스 배우기</h3>
<p><img src="https://velog.velcdn.com/images/feels_bot_man/post/6892947b-f50d-4019-bc19-891b14c93d7d/image.jpg" alt=""></p>
<h3 id="비전공자도-쉽게-이해하는-데이터베이스-입문">비전공자도 쉽게 이해하는 데이터베이스 입문</h3>
<p><img src="https://velog.velcdn.com/images/feels_bot_man/post/087a6177-e2db-4dc8-a5c1-db15e0f2057d/image.jpg" alt=""></p>
<h2 id="보안">보안</h2>
<h3 id="입문자를-위한-컴퓨터-보안">입문자를 위한 컴퓨터 보안</h3>
<p>개념부터 실습까지 한 권으로 따라잡기
<img src="https://velog.velcdn.com/images/feels_bot_man/post/a53ea8a7-7a65-471b-9c8b-91ec3f3de153/image.jpg" alt=""></p>
<h3 id="포텐의-정보보안-카페">포텐의 정보보안 카페</h3>
<p>모든 사람을 위한 쉽고 톡톡 튀는 정보보안 콘서트
<img src="https://velog.velcdn.com/images/feels_bot_man/post/43a6d4c7-6ec4-4077-9d5a-ecd8404f5649/image.jpg" alt=""></p>
<h2 id="운영체제">운영체제</h2>
<h3 id="linux가-보이는-그림책">Linux가 보이는 그림책</h3>
<p>클라우드 시대의 새로운 OS에 대응
<img src="https://velog.velcdn.com/images/feels_bot_man/post/fe4277e4-7ed8-48ec-b587-016495a77258/image.jpg" alt=""></p>
<h2 id="nodejs">Node.js</h2>
<h3 id="nodejs-백엔드-개발자-되기">Node.js 백엔드 개발자 되기</h3>
<p>TypeScript + Node.js + Express + NestJS로 배우는 자바스크립트 백엔드 입문자를 위한 풀 패키지
<img src="https://velog.velcdn.com/images/feels_bot_man/post/ea2dea51-2e6d-4d92-856e-b095eb6c74be/image.jpg" alt=""></p>
<p>추천하는 책이 더 있다면 알려주세요. 감사합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Google의 TypeScript Style Guide > Naming]]></title>
            <link>https://velog.io/@feels_bot_man/Google%EC%9D%98-TypeScript-Style-Guide-Naming-21xhbgr0</link>
            <guid>https://velog.io/@feels_bot_man/Google%EC%9D%98-TypeScript-Style-Guide-Naming-21xhbgr0</guid>
            <pubDate>Fri, 06 Dec 2024 03:18:19 GMT</pubDate>
            <description><![CDATA[<h1 id="5-naming">5 <a href="https://google.github.io/styleguide/tsguide.html#naming">Naming</a></h1>
<h2 id="51-identifiers">5.1 Identifiers</h2>
<p><strong>식별자</strong>는 변수, 함수, 클래스, 인터페이스, 타입, 속성 등의 이름을 나타낸다.</p>
<p><strong>식별자에서 허용되는 문자</strong>:
영문 대소문자 (A-Z, a-z): 일반적인 알파벳 문자.
숫자 (0-9): 알파벳과 함께 사용 가능. 단, 숫자로 시작할 수는 없다.</p>
<p><strong>특정 상황에서만 밑줄 사용</strong>:
상수: ALL_CAPS 스타일의 상수 이름에 밑줄을 사용.
예: <code>MAX_LENGTH</code>, <code>DEFAULT_VALUE</code>.
구조화된 테스트 메서드 이름: 테스트 메서드에서 논리적 구조를 나타내기 위해 밑줄 사용 가능.
예: <code>test_addition_of_two_numbers</code></p>
<p><strong>달러 기호($)</strong>:
JavaScript 및 TypeScript에서 <code>$</code>는 변수 이름에 허용되지만, 일반적으로 특별한 목적으로만 사용한다.
예: AngularJS에서 <code>$scope</code>, jQuery에서 <code>$</code> 같은 경우.</p>
<p><strong>허용되지 않는 문자</strong>:
비ASCII 문자(예: 한글, 특수 기호 등)는 식별자로 사용하지 않는다.
예: <code>이름</code>, <code>#value</code>, <code>@count</code> 등은 허용되지 않음.</p>
<hr>
<h3 id="511-naming-style">5.1.1 Naming style</h3>
<ul>
<li>프로퍼티나 메서드의 맨 앞이나 맨 뒤에 밑줄(&#39;_&#39;)사용하지 않기</li>
<li>선택적 매개변수 앞에 <code>opt_</code> 접두사를 사용하지 않기</li>
<li>인터페이스를 특별히 표시하지 않기<ul>
<li>&#39;I&#39;또는 &#39;Interface&#39; 같은 접두사/접미사는 불필요하며, 코드의 명확성과 가독성을 해친다. 인터페이스를 도입할 때는 이름을 통해 그 인터페이스가 왜 존재하는지, 어떤 역할을 수행하는지 설명해야 한다.</li>
</ul>
</li>
<li><code>Observable</code> 변수 구분을 위한 <code>$</code> 접미사의 사용 여부는 팀의 판단에 따름</li>
</ul>
<p><strong>Before</strong></p>
<pre><code class="language-javascript">interface ITodoItem {
  title: string;
  completed: boolean;
}

interface TodoItemInterface {
  title: string;
  completed: boolean;
}</code></pre>
<ul>
<li><code>I</code>나 <code>Interface</code>와 같은 특별한 표시는 코드에 의미를 더하지 않는다.</li>
<li>이름만으로는 이 인터페이스가 어떤 용도인지 알기 어렵다.</li>
</ul>
<p><strong>After</strong></p>
<pre><code class="language-javascript">interface TodoItemStorage {
  title: string;
  completed: boolean;
}</code></pre>
<ul>
<li>의미 있는 이름: TodoItemStorage는 저장소 형식을 표현하기 위한 인터페이스임을 명확히 나타낸다.</li>
<li>클래스(TodoItem)와 역할을 구분하면서도 관계가 명확하다.</li>
</ul>
<hr>
<h3 id="512-descriptive-names">5.1.2 Descriptive names</h3>
<p>이름은 설명적이고 명확해야 한다. 모호하거나 익숙하지 않은 약어를 사용하지 말고, 단어 내에서 문자를 삭제하여 약어의 사용을 지양하자.</p>
<blockquote>
<p>예외: 10줄 이하 범위에 속하는 변수, 즉 내보내는 API에 속하지 않는 인수는 짧은(예: 문자 하나) 변수 이름을 사용할 수 있다.</p>
</blockquote>
<p><strong>명확하고 서술적인 이름 사용</strong>:
변수나 식별자는 코드만 봐도 무슨 역할을 하는지 쉽게 이해할 수 있는 이름을 사용해야 한다.
외부인이나 새로운 독자가 봐도 직관적으로 의미를 알 수 있도록 이름을 정해야 한다.</p>
<p><strong>모호하거나 생소한 약어 지양</strong>:
특정 프로젝트 내부에서만 통용되는 약어나 생소한 약어는 사용하지 말 것.
약어를 쓸 때는 일반적으로 알려진 약어만 사용한다.
예: <code>URL</code>, <code>DNS</code>, <code>Id</code> 등.</p>
<p><strong>단어 내 글자를 생략하지 말 것</strong>:
단어의 중간 글자를 삭제하거나 축약하여 의미를 모호하게 만들지 말 것.
예: <code>cstmrId</code> → <code>customerId</code>로 작성.</p>
<p><strong>짧은 범위에서는 간결한 이름 허용</strong>:
범위가 10줄 이하인 코드(로컬 변수, 함수 인자 등)에서는 단일 문자 또는 간결한 이름이 허용된다.
예: <code>i</code>, <code>n</code>, <code>x</code> 등.</p>
<p><strong>헝가리안 표기법 지양</strong>:
변수 이름에 타입 정보를 접두사로 붙이는 헝가리안 표기법은 사용하지 않는다.
예: <code>kSecondsPerDay</code> (잘못된 헝가리안 표기법).</p>
<p><strong>올바른 CamelCase</strong>:
CamelCase 표기법을 사용할 때, 약어는 일관되게 소문자로 처리.
예: <code>customerId</code> (올바름), <code>customerID</code> (잘못된 CamelCase).</p>
<p><strong>Before</strong></p>
<pre><code class="language-javascript">const n = 5; // 의미 불명확
const nErr = 0; // 애매한 약어
const cstmrId = &quot;12345&quot;; // 축약으로 가독성 저하
const wgcConnections = []; // 팀 내부에만 알려진 약어
const customerID = &quot;12345&quot;; // 잘못된 CamelCase</code></pre>
<p><strong>After</strong></p>
<pre><code class="language-javascript">const itemCount = 5; // 명확한 이름
const errorCount = 0; // 가독성이 좋은 이름
const customerId = &quot;12345&quot;; // 축약하지 않은 명확한 이름
const connections = []; // 약어를 제거하여 직관적으로 수정</code></pre>
<hr>
<h3 id="513-camel-case">5.1.3 Camel case</h3>
<p><strong>약어는 단어처럼 취급</strong>:
약어(Acronym)가 포함된 식별자 이름은 단어처럼 혼합 대소문자로 작성해야 한다.
BAD: <code>loadHTTPURL</code> (약자를 전체 대문자로 작성하면 읽기 어렵다.)
GOOD: <code>loadHttpUrl</code> (Http는 단어처럼 취급되어 첫 글자만 대문자로 표기) </p>
<p><strong>플랫폼 이름이 특별한 경우는 예외</strong>:
특정 플랫폼이나 API 이름이 약자를 대문자로 사용하는 경우, 해당 관례를 따라야 한다.
(<code>XMLHttpRequest</code>는 JavaScript의 API 이름에서 사용되는 표준.)</p>
<hr>
<h3 id="514-dollar-sign">5.1.4 Dollar sign</h3>
<p><code>$</code>는 일반적인 경우 식별자 이름에 사용하지 않는 것이 좋다.
다만, 서드파티 프레임워크의 명명 규칙에 따라 사용하거나, <code>Observable</code> 값을 구분하기 위해 명확한 목적으로 사용할 수 있다.
<code>$</code>를 사용할 경우, 프로젝트 내에서 일관성을 유지하는 것이 중요하다.</p>
<hr>
<h2 id="52-rules-by-identifier-type">5.2 Rules by identifier type</h2>
<p><strong>식별자의 종류에 따라 일관된 네이밍 스타일을 사용해야 한다</strong>:</p>
<table>
<thead>
<tr>
<th><strong>스타일</strong></th>
<th><strong>대상 카테고리</strong></th>
<th><strong>설명</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>PascalCase</strong></td>
<td>클래스, 인터페이스, 타입, 열거형(enum), 데코레이터, 타입 매개변수, TSX/JSX 요소 타입 매개변수</td>
<td>첫 단어와 이후 모든 단어의 첫 글자가 대문자. 주로 <strong>구조적이고 정의적인 객체</strong>에 사용됨.</td>
</tr>
<tr>
<td><strong>camelCase</strong></td>
<td>변수, 매개변수, 함수, 메서드, 속성, 모듈 별칭</td>
<td>첫 단어는 소문자, 이후 단어는 첫 글자만 대문자. 일반적으로 <strong>작은 스코프의 요소</strong> 또는 동작과 관련된 것들에 사용됨.</td>
</tr>
<tr>
<td><strong>CONSTANT_CASE</strong></td>
<td>전역 상수 값, 열거형(enum) 값</td>
<td>모든 문자가 대문자이며, 단어는 밑줄(<code>_</code>)로 구분. 불변 값을 명확히 구분하기 위해 사용됨.</td>
</tr>
<tr>
<td><strong>#ident</strong></td>
<td>private 식별자</td>
<td>TypeScript에서는 지원하지 않음. <code>#</code> 접두사는 JavaScript의 private class 필드에 사용되며 TypeScript에서는 일반적으로 <code>private</code> 키워드를 사용.</td>
</tr>
</tbody></table>
<hr>
<h3 id="521-type-parameters">5.2.1 Type parameters</h3>
<p>타입 매개변수는 제네릭(generic)을 사용할 때 선언되며, 함수, 클래스, 인터페이스, 타입 별칭 등에 유연한 타입 지정을 제공한다. 단일 대문자 또는 PascalCase를 사용할 수 있다.</p>
<pre><code class="language-typescript">function getKey&lt;K, V&gt;(key: K, value: V): K {
  return key;
}</code></pre>
<pre><code class="language-typescript">class ApiResponse&lt;DataType&gt; {
  constructor(public data: DataType, public success: boolean) {}
}
const userResponse = new ApiResponse&lt;{ id: number; name: string }&gt;({
  id: 1,
  name: &quot;Alice&quot;,
}, true);</code></pre>
<hr>
<h3 id="522-test-names">5.2.2 Test names</h3>
<p>테스트의 목적과 동작을 명확하게 나타내고, 테스트가 어떤 상황에서 어떤 결과를 예상하는지를 쉽게 이해할 수 있도록 _ (밑줄) 구분자를 사용하여 테스트 메서드를 설명적이고 읽기 쉽게 작성한다.</p>
<ul>
<li>구조는 일반적으로 <code>testX_whenY_doesZ()</code> 형태를 따른다.<ul>
<li><code>X</code>: 테스트하려는 기능 또는 행동.</li>
<li><code>Y</code>: 조건이나 상황.</li>
<li><code>Z</code>: 예상되는 결과 또는 행동</li>
</ul>
</li>
</ul>
<pre><code class="language-typescript">function testLogin_whenCredentialsAreValid_doesRedirectToDashboard() {
  // 로그인 기능 테스트
}</code></pre>
<ul>
<li><code>testLogin</code>: 로그인 기능을 테스트.</li>
<li><code>whenCredentialsAreValid</code>: 사용자 자격 증명이 유효한 경우에 대해 테스트.</li>
<li><code>doesRedirectToDashboard</code>: 유효한 자격 증명으로 로그인할 때 대시보드로 리디렉션되는지 확인.</li>
</ul>
<pre><code class="language-typescript">function testCalculateTotalPrice_whenItemIsAdded_doesIncreaseTotalPrice() {
  // 상품 추가 시 총 가격이 증가하는지 테스트
}</code></pre>
<ul>
<li><code>testCalculateTotalPrice</code>: 총 가격 계산 기능을 테스트.</li>
<li><code>whenItemIsAdded</code>: 아이템이 장바구니에 추가될 때.</li>
<li><code>doesIncreaseTotalPrice</code>: 아이템을 추가하면 총 가격이 증가하는지 확인.</li>
</ul>
<p>구체적이고 설명적이어야 한다. 지나치게 짧거나 모호한 이름을 피하고, 테스트가 무엇을, 언제, 어떻게 확인하는지 명확하게 나타내야 한다.
너무 긴 이름은 가독성을 떨어뜨릴 수 있으므로 적당히 사용한다.</p>
<hr>
<h3 id="523-_-prefixsuffix">5.2.3 _ prefix/suffix</h3>
<p><strong>밑줄을 접두사나 접미사로 사용하지 않기</strong>:
식별자는 가능하면 의미가 명확해야 하며, 밑줄을 접두사나 접미사로 사용할 경우 어떤 의미가 있는지 또는 그 식별자가 특별한 목적을 가지고 있는지 명확히 전달되지 않는다.</p>
<p><strong>단독으로 밑줄 사용 금지</strong>:
식별자가 의미를 가지지 않게 된다. 변수나 매개변수는 그 목적이 분명해야 한다.
(파이썬에서는 매개변수가 사용되지 않음을 나타내는 경우 단독으로 밑줄을 사용함)</p>
<blockquote>
<p>구조 분해 할당을 사용할 때, 배열이나 튜플에서 불필요한 요소를 무시하려면 단순히 중간에 ,를 삽입하여 해당 요소를 무시할 수 있다.</p>
</blockquote>
<pre><code class="language-typescript">const [a, , b] = [1, 5, 10];  // a &lt;- 1, b &lt;- 10, 중간 요소 5는 무시</code></pre>
<hr>
<h3 id="524-imports">5.2.4 Imports</h3>
<p><strong>모듈 네임스페이스 (camelCase)</strong>:
모듈을 네임스페이스로 가져올 때는 camelCase를 사용한다.</p>
<pre><code class="language-typescript">import * as fooBar from &#39;./foo_bar&#39;;  // 올바른 네임스페이스 네이밍</code></pre>
<p><strong>파일 이름 (snake_case)</strong>:
파일 이름은 snake_case를 사용해야 하며, 이는 소문자와 밑줄(_)로 단어를 구분하는 스타일이다. 예를 들어 foo_bar.ts와 같이 작성된다.</p>
<blockquote>
<p>(작성자 첨언) 그러나 모듈과 파일 이름은 camelCase나 kebab-case를 사용하기도 한다.</p>
</blockquote>
<hr>
<h3 id="525-constants">5.2.5 Constants</h3>
<p><strong>CONSTANT_CASE의 사용</strong>:
상수(Constant) 값에 CONSTANT_CASE를 사용하여 변하지 않아야 하는 값들을 명확하게 표시하고, 이를 통해 코드에서 해당 값을 수정하지 않도록 유도한다.</p>
<p><strong>static readonly와 CONSTANT_CASE</strong>:
클래스에서 <code>static readonly</code> 속성도 상수로 다루어질 수 있다. <code>readonly</code> 키워드를 사용하면 해당 속성의 값을 변경할 수 없다는 점에서 불변성을 보장할 수 있다.</p>
<pre><code class="language-javascript">class Foo {
  private static readonly MY_SPECIAL_NUMBER = 5;

  bar() {
    return 2 * Foo.MY_SPECIAL_NUMBER;
  }
}</code></pre>
<p><strong>Global Scope와 CONSTANT_CASE</strong>:
CONSTANT_CASE는 모듈 수준에서만 사용된다. 모듈의 최상위에서 정의된 상수값에 대해서만 CONSTANT_CASE를 사용하는 것이 권장된다. 지역 변수나 함수 내부에서 선언된 값은 camelCase를 사용해야 한다.</p>
<hr>
<h3 id="526-aliases">5.2.6 Aliases</h3>
<p>로컬 별칭은 기존 심볼의 네이밍 규칙과 형식을 따르며, 그 목적에 맞는 적절한 선언 방법을 사용해야 한다.</p>
<p><strong>기존 심볼의 네이밍 규칙을 따르기</strong>:
로컬에서 기존의 변수를 별칭으로 사용할 때, 별칭의 이름은 기존 심볼의 형식과 규칙을 따라야 한다. 예를 들어, 상수는 대문자 및 밑줄을 사용한 CONSTANT_CASE 형식을 따르고, 변수나 객체는 camelCase 형식을 사용해야 한다. 이렇게 하면 일관된 네이밍 규칙을 유지할 수 있다.</p>
<pre><code class="language-typescript">const { BrewStateEnum } = SomeType;  // 기존 심볼
const CAPACITY = 5;  // 기존 상수</code></pre>
<ul>
<li><code>BrewStateEnum</code>은 <code>SomeType</code>에서 추출된 객체의 속성이다. 이 값을 사용할 때 로컬에서 새로운 이름을 지정할 수 있다.</li>
<li><code>CAPACITY</code>는 값 5를 나타내는 상수이다. 이 값을 사용하려면 로컬에서 <code>CAPACITY</code>라는 이름을 그대로 사용해야 한다.</li>
</ul>
<p><strong>로컬 별칭 생성 방법</strong>:
로컬에서 별칭을 생성할 때, 변수와 클래스 필드에 대해 서로 다른 방식으로 선언해야 한다.</p>
<ul>
<li>변수의 로컬 별칭은 <code>const</code>를 사용하여 선언해야 한다.</li>
<li>클래스 필드의 로컬 별칭은 <code>readonly</code> 속성을 사용하여 선언해야 한다.<pre><code class="language-typescript">class Teapot {
readonly BrewStateEnum = BrewStateEnum;  // 클래스 필드에서 BrewStateEnum을 읽기 전용으로 설정
readonly CAPACITY = CAPACITY;  // 클래스 필드에서 CAPACITY를 읽기 전용으로 설정
}</code></pre>
</li>
<li><code>BrewStateEnum</code>과 <code>CAPACITY</code>는 각각 기존 값의 별칭이다. 클래스 필드에 대해 <code>readonly</code>를 사용하면 불변 값임을 명확하게 나타낼 수 있다.</li>
<li>이 별칭들은 클래스의 속성으로 사용되며, 외부에서 해당 값을 변경할 수 없도록 보장한다.</li>
</ul>
<p><strong>템플릿에서의 별칭</strong>:
프레임워크나 템플릿을 사용할 때, 별칭을 생성하여 템플릿에 노출하려는 경우에도 이 규칙이 적용된다. 별칭을 만들 때 접근 제어자(예: <code>public</code>, <code>private</code>)를 올바르게 지정하는 것이 중요하다. 프레임워크에서는 특정 데이터나 속성을 템플릿에서 직접 사용할 수 있도록 노출해야 할 때가 많은데, 이때도 접근 제어자를 적절히 설정하여 외부에서 접근할 수 있는지, 또는 내부에서만 접근할 수 있는지를 정의해야 한다.</p>
<pre><code class="language-typescript">class Teapot {
  public readonly BrewStateEnum = BrewStateEnum;  // 템플릿에서 사용될 수 있도록 공개
  private readonly CAPACITY = CAPACITY;  // 내부에서만 사용될 수 있도록 제한
}</code></pre>
<ul>
<li><code>public</code> 접근 제어자는 템플릿에서 이 값을 사용할 수 있도록 함.</li>
<li><code>private</code> 접근 제어자는 템플릿에서 사용되지 않도록 하여, 외부 접근을 제한.</li>
</ul>
<hr>
<h4 id="참고자료">참고자료</h4>
<p><a href="https://google.github.io/styleguide/tsguide.html">Google TypeScript Style Guide</a></p>
<p><a href="https://github.com/google/gts">GitHub - google/gts: ☂️ TypeScript style guide, formatter, and linter. </a></p>
<p><a href="https://www.youtube.com/watch?v=lhQhsIrRXoc">Typescript Google Code Style Part 1</a><br><a href="https://www.youtube.com/watch?v=idzUmsnPM-g">Typescript Google Code Style Part 2</a> 
<a href="https://www.youtube.com/watch?v=yNWzUsGdD54">Typescript Google Code Style Part 3</a></p>
<p><a href="https://ts.dev/style/">ts.dev - TypeScript style guide</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Google의 TypeScript Style Guide > Language features > 4.8 Primitive literals]]></title>
            <link>https://velog.io/@feels_bot_man/Google%EC%9D%98-TypeScript-Style-Guide-Language-features-4.8-Primitive-literals</link>
            <guid>https://velog.io/@feels_bot_man/Google%EC%9D%98-TypeScript-Style-Guide-Language-features-4.8-Primitive-literals</guid>
            <pubDate>Fri, 06 Dec 2024 03:04:30 GMT</pubDate>
            <description><![CDATA[<h1 id="4-language-features">4 Language features</h1>
<h2 id="48-primitive-literals">4.8 Primitive literals</h2>
<h3 id="481-string-literals">4.8.1 String literals</h3>
<p><strong>Single Quotes 사용</strong>:  (작성자 생각: JSON과 구별하기 위함인가?🤷)
일반적인 문자열 리터럴에서는 싱글 쿼트(<code>&#39;</code>)를 사용하는 것이 권장된다. 더블 쿼트(<code>&quot;</code>)도 사용할 수 있지만, 싱글 쿼트를 사용하면 일반적인 코드 스타일과 일관성을 유지할 수 있다.
<strong>Before</strong></p>
<pre><code class="language-javascript">const greeting = &quot;Hello, world!&quot;;</code></pre>
<p><strong>After</strong></p>
<pre><code class="language-javascript">const greeting = &#39;Hello, world!&#39;;</code></pre>
<p>문자열에 싱글 쿼트가 포함되어 있는 경우, 이스케이프(<code>\&#39;</code>) 대신 템플릿 리터럴을 사용하는 것이 가독성을 높인다.
<strong>Before</strong></p>
<pre><code class="language-javascript">const message = &#39;It\&#39;s a beautiful day!&#39;;</code></pre>
<p><strong>After</strong></p>
<pre><code class="language-javascript">const message = `It&#39;s a beautiful day!`;</code></pre>
<p><strong>템플릿 문자열 사용</strong>:
템플릿 문자열은 백틱(`)으로 감싸고, 문자열 내에서 <strong>동적 데이터 삽입</strong>과 <strong>여러 줄 문자열</strong> 작성을 간단히 처리할 수 있다.
복잡한 문자열 연결 작업은 템플릿 문자열로 대체하는 것이 더 명확하고 가독성이 좋다.</p>
<p><strong>Before</strong></p>
<pre><code class="language-javascript">const message = &#39;Hello, &#39; + name + &#39;! You have &#39; + count + &#39; messages.&#39;;</code></pre>
<p><strong>After</strong></p>
<pre><code class="language-javascript">const message = `Hello, ${name}! You have ${count} messages.`;</code></pre>
<p><strong>줄 바꿈(Line Continuation) 금지</strong>:
문자열을 <strong>백슬래시(<code>\</code>)를 사용해 줄 바꿈</strong>하면 코드의 가독성이 떨어지고, 공백과 관련된 에러가 발생할 가능성이 있다. 예를 들어, 백슬래시 뒤에 공백이 있으면 에러를 초래한다.
줄 바꿈이 필요한 경우, <strong>문자열 연결(<code>+</code>)</strong>을 사용하거나 <strong>템플릿 문자열</strong>을 활용한다.</p>
<p><strong>Before</strong></p>
<pre><code class="language-javascript">const longString = &#39;This is a very long string that spans multiple lines. \
It can cause tricky errors if there is any trailing whitespace.&#39;;</code></pre>
<p><strong>After</strong></p>
<pre><code class="language-javascript">// 문자열 연결
const longString = &#39;This is a very long string &#39; +
    &#39;that spans multiple lines without issues.&#39;;

// 템플릿 리터럴 사용
const longString = `This is a very long string 
that spans multiple lines cleanly.`;

// 템플릿 문자열로 여러 줄 처리
function arithmetic(a, b) {
  return `Here is a table of arithmetic operations:
${a} + ${b} = ${a + b}
${a} - ${b} = ${a - b}
${a} * ${b} = ${a * b}
${a} / ${b} = ${a / b}`;
}</code></pre>
<hr>
<h3 id="482-number-literals">4.8.2 Number literals</h3>
<p><strong>숫자 리터럴 표기법</strong>:
JavaScript에서는 숫자를 다양한 진법(Decimal, Hexadecimal, Octal, Binary)으로 표현할 수 있다. 숫자 리터럴의 <strong>접두사</strong>는 소문자로 통일해야 한다.</p>
<table>
<thead>
<tr>
<th>진법</th>
<th>표기법 예시</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Decimal</strong></td>
<td><code>42</code></td>
<td>10진수 (기본)</td>
</tr>
<tr>
<td><strong>Hex</strong></td>
<td><code>0x2a</code></td>
<td>16진수, 접두사 <code>0x</code></td>
</tr>
<tr>
<td><strong>Octal</strong></td>
<td><code>0o52</code></td>
<td>8진수, 접두사 <code>0o</code></td>
</tr>
<tr>
<td><strong>Binary</strong></td>
<td><code>0b101010</code></td>
<td>2진수, 접두사 <code>0b</code></td>
</tr>
</tbody></table>
<pre><code class="language-javascript">const decimal = 42;     // 10진수
const hex = 0x2a;       // 16진수
const octal = 0o52;     // 8진수
const binary = 0b101010; // 2진수</code></pre>
<p><strong>선행 0 금지</strong>:
숫자 리터럴에 선행 0(leading zero)를 사용하는 경우 혼란을 초래할 수 있다. 예전에는 선행 0이 있는 숫자가 8진수로 해석되었기 때문이다(ECMAScript 5 이전).
ECMAScript 6 이상에서는 선행 0을 사용하는 숫자 리터럴은 <strong>문법 오류</strong>로 간주된다. 따라서 선행 0은 반드시 <code>0x</code>, <code>0o</code>, <code>0b</code>와 같이 올바른 접두사가 뒤따라야 한다.</p>
<p><strong>Before</strong></p>
<pre><code class="language-javascript">// 예전 JavaScript에서 선행 0은 8진수로 해석되었다.
const octal = 052; // 42 (8진수)</code></pre>
<p><strong>After</strong></p>
<pre><code class="language-javascript">// 올바른 8진수 표현
const octal = 0o52;</code></pre>
<hr>
<h3 id="483-type-coercion">4.8.3 Type coercion</h3>
<p><strong>암시적 강제 변환(Implicit coercion) 지양하기</strong>:
암시적 강제 변환은 코드의 의도를 불분명하게 만들고, 예상치 못한 오류를 초래할 수 있다.
<strong>Before</strong></p>
<pre><code class="language-typescript">if (!!foo) {...}  // BAD:불필요한 암시적 강제 변환
while (!!foo) {...}</code></pre>
<p><strong>After</strong></p>
<pre><code class="language-typescript">if (foo) {...}  // GOOD: 간결하게 작성
while (foo) {...}</code></pre>
<p><strong>문자열 명시적 변환에 <code>String()</code> 또는 <code>Boolean()</code> 함수를 사용하기</strong>:</p>
<pre><code class="language-typescript">const bool = Boolean(false); // false
const str = String(123);     // &quot;123&quot;
const bool2 = !!str;         // true
const str2 = `result: ${bool2}`; // &quot;result: true&quot;</code></pre>
<ul>
<li>변환이 명확하고 코드 리뷰 시 쉽게 파악할 수 있음</li>
<li>암시적 강제 변환보다 의도를 명확히 표현</li>
<li><code>!!</code> 연산자를 사용하여 값을 명시적으로 boolean으로 변환</li>
</ul>
<p><strong>열거형 값을 불리언으로 변환하지 않기</strong>:
열거형(Enum)은 기본적으로 숫자 값과 매핑되며, 첫 번째 값은 <code>0</code>으로 할당된다.<br><code>Boolean(enumValue)</code>나 <code>!!enumValue</code>를 사용하면:</p>
<ul>
<li>첫 번째 값(<code>0</code>)은 <strong>falsy</strong>로 평가</li>
<li>나머지 값은 <strong>truthy</strong>로 평가
이런 동작은 예상치 못한 오류를 유발할 수 있다.</li>
</ul>
<p><strong>Before</strong></p>
<pre><code class="language-typescript">enum SupportLevel {
  NONE,
  BASIC,
  ADVANCED,
}

const level: SupportLevel = SupportLevel.NONE;
let enabled = Boolean(level); // 잘못된 방식: `NONE`이 `false`로 평가됨</code></pre>
<p><strong>After</strong></p>
<pre><code class="language-typescript">enum SupportLevel {
  NONE,
  BASIC,
  ADVANCED,
}

const level: SupportLevel = SupportLevel.NONE;
let enabled = level !== SupportLevel.NONE; // 명시적 비교</code></pre>
<p><strong>숫자형 변환에 Number() 사용하기</strong>:
숫자 값을 파싱할 때는 <code>Number()</code>를 사용하고, 결과가 유효한지 명시적으로 확인해야 한다:</p>
<ul>
<li><strong><code>isNaN</code></strong>: 숫자가 NaN인지 확인</li>
<li><strong><code>isFinite</code></strong>: 숫자가 유한한 값인지 확인</li>
</ul>
<p><strong>Before</strong></p>
<pre><code class="language-typescript">const x = +y; // 가독성이 낮고, 실수하기 쉬움
const n = parseInt(someString, 10); // 에러 발생 가능
const f = parseFloat(someString); // 에러 발생 가능</code></pre>
<ul>
<li>문자열에 추가적인 문자가 포함될 경우 오류가 발생하지 않고 잘못된 값을 반환함.</li>
</ul>
<p><strong>After</strong></p>
<pre><code class="language-typescript">let f = Number(someString); // 숫자로 변환
if (!isFinite(f)) throw new Error(&#39;Invalid number&#39;); // 유효성 확인
if (isNaN(f)) handleError(); // NaN인지 확인
f = Math.floor(f);           // 정수로 변환</code></pre>
<blockquote>
<p><code>Number(&#39;&#39;)</code>, <code>Number(&#39; &#39;)</code>, <code>Number(&#39;\t&#39;)</code>는 <code>NaN</code> 대신 <code>0</code>을 반환한다.
<code>Number(&#39;Infinity&#39;)</code>, <code>Number(&#39;-Infinity&#39;)</code>는 <code>Infinity</code>와 <code>-Infinity</code>를 반환한다. 
지수 표기법, 예를들어 <code>Number(&#39;1e+309&#39;)</code>, <code>Number(&#39;-1e+309&#39;)</code>은 무한대로 오버플로될 수 있다. 이러한 경우 특별한 처리가 필요할 수 있다. </p>
</blockquote>
<hr>
<h2 id="49-control-structures">4.9 Control structures</h2>
<h3 id="491-control-flow-statements-and-blocks">4.9.1 Control flow statements and blocks</h3>
<p>모든 제어 흐름문(if, else, for, while, 등)은 항상 중괄호로 코드 블록을 감싸야 한다.
단, if 문이 한 줄로 끝나는 경우에는 중괄호 생략이 허용된다.</p>
<p><strong>Before</strong></p>
<pre><code class="language-typescript">if (x)
  doSomething(); // 중괄호 없이 여러 줄 코드 작성 시 가독성 문제 발생</code></pre>
<p><strong>After</strong></p>
<pre><code class="language-typescript">if (x) {
  doSomething();
}

for (let i = 0; i &lt; 10; i++) {
  console.log(i);
}

if (x) x.doSomething(); // 한 줄 코드인 경우에만 중괄호 생략 가능</code></pre>
<p>제어문 내부에서 변수 할당은 지양해야 한다. 
이는 할당(=)과 비교(==)를 혼동할 가능성을 줄이기 위함이다.
단, 의도적으로 할당하는 경우 이중 괄호를 사용해 의도를 명확히 표시한다.</p>
<p><strong>Before</strong></p>
<pre><code class="language-typescript">if (x = someFunction()) {
  // 할당이 조건문 내부에 있어 혼동 가능
}</code></pre>
<p><strong>After</strong></p>
<pre><code class="language-typescript">x = someFunction();
if (x) {
  // x를 사용
}</code></pre>
<pre><code class="language-typescript">while ((x = someFunction())) {
  // 할당이 의도적임을 알 수 있음
}</code></pre>
<p>배열을 반복 처리할 때는 다음과 같은 방식을 우선적으로 사용한다:</p>
<ul>
<li><code>for ... of</code>: 배열의 값을 직접 반복</li>
<li><code>Array.prototype.forEach</code>: 반복 동작을 콜백 함수로 처리</li>
<li>전통적인 <code>for</code> 루프: 인덱스가 필요한 경우에만 사용<pre><code class="language-typescript">for (const value of someArr) {
console.log(value);
}
</code></pre>
</li>
</ul>
<p>for (let i = 0; i &lt; someArr.length; i++) {
  const value = someArr[i];  // 인덱스가 필요한 경우
  console.log(i, value);
}</p>
<p>for (const [i, value] of someArr.entries()) {
  console.log(i, value);
}</p>
<pre><code>&gt; `for ... in`은 배열이 아닌 객체 순회용이며, 배열의 인덱스를 문자열로 반환하기 때문에 비직관적이므로 배열에 사용하지 말자.

객체를 반복 처리할 때는 `for ... in`보다 Object.keys, Object.values, Object.entries를 권장한다.
```typescript
for (const key of Object.keys(obj)) {
  console.log(key);
}

for (const value of Object.values(obj)) {
  console.log(value);
}

for (const [key, value] of Object.entries(obj)) {
  console.log(key, value);
}

for (const key in obj) {
  if (!obj.hasOwnProperty(key)) continue;
  doWork(key, obj[key]);
}</code></pre><hr>
<h3 id="492-grouping-parentheses">4.9.2 Grouping parentheses</h3>
<p>코드를 작성할 때 불필요한 괄호 사용을 피하되, 읽기 쉽고 오해의 소지가 없도록 작성하라.</p>
<p>선택적인 그룹화 괄호는 작성자와 검토자가 그룹화 괄호 없이는 코드가 오해받을 가능성이 전혀 없고, 코드의 읽기가 더 쉬워지지 않을 것에 동의하는 경우에만 생략한다. (모든 독자가 연산자 우선순위 표 전체를 암기하고 있다고 가정하는 것은 합리적이지 않다.)</p>
<p><code>delete</code>, <code>typeof</code>, <code>void</code>, <code>return</code>, <code>throw</code>, <code>case</code>, <code>in</code>, <code>of</code>, <code>yield</code> 다음에 나오는 전체 표현식 주위에 불필요한 괄호를 사용하지 말자.
<strong>Before</strong></p>
<pre><code class="language-typescript">return (a + b); 
throw (new Error(&#39;Error message&#39;));</code></pre>
<p><strong>After</strong></p>
<pre><code class="language-typescript">return a + b; 
throw new Error(&#39;Error message&#39;);</code></pre>
<hr>
<h3 id="493-exception-handling">4.9.3 Exception handling</h3>
<p>예외는 언어의 중요한 부분이므로 예외적인 경우가 발생할 때마다 사용해야 한다.
사용자 정의 예외는 함수에서 추가적인 오류 정보를 전달하는 좋은 방법을 제공한다.</p>
<p><strong><code>new</code>를 사용한 인스턴스화 하기</strong>:
<code>throw Error()</code>보다는 <code>throw new Error()</code>를 사용. 이는 다른 객체 인스턴스화 방식과 일관성을 유지한다.
<strong>Before</strong></p>
<pre><code class="language-typescript">throw Error(&#39;Foo is not a valid bar.&#39;);</code></pre>
<p><strong>After</strong></p>
<pre><code class="language-typescript">throw new Error(&#39;Foo is not a valid bar.&#39;);</code></pre>
<p><strong><code>Error</code>만 <code>throw</code> 하기</strong>:
일반적으로 JavaScript에서는 <code>throw</code> 키워드를 사용해 예외를 발생시킬 때 <code>Error</code> 객체를 던진다.
임시 오류 처리 방식(오류 컨테이너 참조 유형 전달이나 오류 속성이 있는 객체 반환 등)보다 예외를 <code>throw</code>하는 것을 권장한다.
<strong>Before</strong></p>
<pre><code class="language-typescript">throw &#39;oh noes!&#39;;

new Promise((resolve, reject) =&gt; void reject(&#39;oh noes!&#39;));
Promise.reject();
Promise.reject(&#39;oh noes!&#39;);</code></pre>
<p><strong>After</strong></p>
<pre><code class="language-typescript">throw new Error(&#39;oh noes!&#39;);

class MyError extends Error {}
throw new MyError(&#39;my oh noes!&#39;);

new Promise((resolve) =&gt; resolve());
new Promise((resolve, reject) =&gt; void reject(new Error(&#39;oh noes!&#39;)));
Promise.reject(new Error(&#39;oh noes!&#39;));</code></pre>
<ul>
<li><code>Error</code> 객체만이 의미 있는 스택 추적 정보를 제공한다.</li>
<li>임의의 값을 던지면 디버깅이 어려워진다.</li>
</ul>
<p>오류를 잡을 때 코드는 발생한 모든 오류가 <code>Error</code> 인스턴스라고 가정해야 한다.</p>
<pre><code class="language-typescript">function assertIsError(e: unknown): asserts e is Error {
  if (!(e instanceof Error)) throw new Error(&quot;e is not an Error&quot;);
}

try {
  doSomething();
} catch (e: unknown) {
  // All thrown errors must be Error subtypes. Do not handle
  // other possible values unless you know they are thrown.
  assertIsError(e);
  displayError(e.message);
  // or rethrow:
  throw e;
}</code></pre>
<p><strong>과도하게 방어적인 코드를 작성하지 말자</strong>:
대부분의 코드에서 <code>throw</code>는 <code>Error</code> 객체를 던지기 때문에, 항상 모든 경우의 타입(string, number 등)을 방어적으로 처리하는 코드는 불필요한 반복적 방어가 된다.
문제를 일으킬 가능성이 적은 상황에서 &quot;모든 경우&quot;를 처리하려는 과도한 방어는 코드 복잡성을 높이고 유지보수를 어렵게 만든다.</p>
<blockquote>
<p>만약 사용하는 API가 <code>Error</code>가 아닌 다른 타입(예: 문자열)을 던지는 비정상적인 동작을 하는 경우에 한하여 방어적 코드를 작성하고 주석을 추가하여 문제의 출처를 명확히 설명하자.</p>
</blockquote>
<p><strong>빈 catch 블록 피하기</strong>:
<strong>Before</strong></p>
<pre><code class="language-typescript">try {
  shouldFail();
} catch (e) {
  // 아무것도 하지 않음
}</code></pre>
<p><strong>After</strong></p>
<pre><code class="language-typescript">try {
  // 작업 수행
} catch (e: unknown) {
  // 왜 아무것도 하지 않는지 주석으로 설명
  // 예: 특정 예외는 무시해도 되는 상황
}</code></pre>
<hr>
<h3 id="494-switch-statements">4.9.4 Switch statements</h3>
<p><strong>모든 <code>switch</code> 문에 <code>default</code> 그룹을 포함하기</strong>:
<code>switch</code> 문에는 항상 <code>default</code> 그룹을 포함해야 하며, 이 그룹은 항상 마지막에 위치한다.
빈 그룹도 괜찮다. 코드가 없는 경우라도 주석으로 의도를 명시하라.</p>
<p><strong>모든 비어 있지 않은 <code>case</code> 그룹은 명시적으로 종료하기</strong>:
<code>case</code> 블록이 비어 있지 않다면, 반드시 명시적으로 종료해야 한다. 종료 방법은 <code>break</code>, <code>return</code>, 또는 <code>throw</code>를 사용하는 것이다.
명시적으로 종료하지 않으면 다음 <code>case</code> 블록으로 실행이 넘어가는 fall-through로 의도치 않은 동작을 유발할 수 있다.
물론 여러 조건을 같은 방식으로 처리하고 싶을 때 의도적인 묶음 처리는 사용해도 된다.</p>
<pre><code class="language-typescript">switch (x) {
  case 1:
    doSomething();
    break; // 명시적으로 종료
  case 2: // fall-through! =&gt; 다음 case로 실행이 넘어감
  case 3:
    doSomethingElse();
    break;
  default:
    // nothing to do.
}</code></pre>
<hr>
<h3 id="495-equality-checks">4.9.5 Equality checks</h3>
<p><strong>삼중 등호(<code>===</code>, <code>!==</code>) 사용하기</strong>:
JavaScript의 동등 비교 연산자(<code>==</code>, <code>!=</code>)에서 발생하는 타입 강제 변환(type coercion)에 따른 예측 불가능한 동작을 방지하기 위해 항상 삼중 등호(<code>===</code>, <code>!==</code>)를 사용할 것을 권장한다.
<code>==</code>는 두 값이 다른 타입일 경우, 비교를 시도하기 위해 한쪽 값을 다른 타입으로 변환한다.
이러한 변환은 예상치 못한 결과를 초래할 수 있다.
또한, 타입 강제 변환을 수행하는 <code>==</code>는 JavaScript 엔진이 더 많은 작업을 처리해야 하므로 느리다.
삼중 등호는 타입을 체크한 뒤 바로 값을 비교하므로 성능이 더 효율적이다.</p>
<p>예외: <code>null</code> 비교에서만 <code>==</code>와 <code>!=</code> 허용
단, <code>null</code>과 <code>undefined</code>를 동시에 비교해야 할 때는 예외적으로 <code>==</code>와 <code>!=</code>를 사용할 수 있다.</p>
<pre><code class="language-typescript">if (foo === null || foo === undefined) {
  console.log(&#39;foo is null or undefined&#39;);
}

if (foo == null) {
  console.log(&#39;foo is null or undefined&#39;); // true
}</code></pre>
<hr>
<h3 id="496-type-and-non-nullability-assertions">4.9.6 Type and non-nullability assertions</h3>
<p>TypeScript의 타입 단언(type assertions)과 null 불가능성 단언(non-nullability assertions)을 신중하게 사용할 것을 권장한다.</p>
<p><strong>타입 단언 시, <code>as</code> 문법을 사용하라</strong>:
<code>&lt;Foo&gt;</code>와 같은 앵글 브래킷 문법은 혼란을 줄 수 있으므로 지양한다.
<code>as</code>를 사용하면 멤버 접근 시 명확성을 높이기 위해 괄호가 추가되기 때문이다.</p>
<p><strong>Before</strong></p>
<pre><code class="language-typescript">const x = (&lt;Foo&gt;z).length;
const y = &lt;Foo&gt;z.length; // 오해의 소지가 있음</code></pre>
<p><strong>After</strong></p>
<pre><code class="language-typescript">const x = (z as Foo).length; // 괄호가 명확성을 보장</code></pre>
<p><strong>타입 단언과 null 불가능성 단언은 반드시 필요한 경우에만 사용하라</strong>:
타입 단언 (<code>x as SomeType</code>)과 null 불가능성 단언 (<code>y!</code>)은 컴파일러에게 특정 값이 어떤 타입임을 강제로 알리는 기능이다.
하지만 런타임에서는 실제로 타입이나 null 체크를 수행하지 않는다. 따라서:</p>
<ul>
<li>잘못된 단언은 런타임 에러로 이어질 수 있다.</li>
<li>단언은 개발자의 책임을 가중시키며 코드 안정성을 떨어뜨릴 수 있다.</li>
</ul>
<p><strong>Before</strong></p>
<pre><code class="language-typescript">const x: unknown = &quot;hello&quot;;
console.log((x as number).toFixed(2)); // 런타임 에러 발생!</code></pre>
<p><strong>After</strong>
타입 체크나 null 체크를 명시적으로 작성하라.</p>
<pre><code class="language-typescript">if (typeof x === &quot;number&quot;) {
  console.log(x.toFixed(2)); // 안전한 실행
}</code></pre>
<p>타입 단언이나 null 불가능성 단언을 반드시 사용해야 할 경우, 주석을 통해 해당 단언이 안전하다고 판단한 이유를 기록한다. (명백한 경우에는 주석이 생략 가능하다.)</p>
<pre><code class="language-typescript">// x는 Foo 타입임이 보장된다 (어떤 이유 때문인지 명시).
(x as Foo).foo();

// y는 null이 될 수 없음을 알고 있다 (해당 상황 설명).
y!.bar();</code></pre>
<p><strong>이중 단언문 (Double Assertions)</strong>
TypeScript는 일반적으로 &quot;더 구체적이거나 더 일반적인 타입&quot;으로의 단언만 허용한다.
안전하다고 확신하는 경우, 이중 단언문을 사용할 수 있다.
중간 타입으로 <code>any</code>나 <code>{}</code> 대신 <code>unknown</code>을 권장한다.
<strong>Before</strong></p>
<pre><code class="language-typescript">(x as any as Foo).fooMethod();</code></pre>
<p><strong>After</strong></p>
<pre><code class="language-typescript">(x as unknown as Foo).fooMethod();</code></pre>
<p><strong>객체 리터럴에는 타입 어노테이션을 사용하여 에러를 사전에 방지하라</strong>:
객체 리터럴에서는 타입 단언 (<code>as Foo</code>) 대신 타입 어노테이션 (<code>: Foo</code>) 사용을 권장한다.
타입 어노테이션을 사용하면 인터페이스 변경 시 잘못된 필드가 자동으로 감지된다.
<strong>Before</strong></p>
<pre><code class="language-typescript">interface Foo {
  bar: number;
  baz?: string;
}

const foo = {
  bar: 123,
  bam: &#39;abc&#39;, // 오류가 발생하지 않음!
} as Foo;</code></pre>
<p><strong>After</strong></p>
<pre><code class="language-typescript">interface Foo {
  bar: number;
  baz?: string;
}

const foo: Foo = {
  bar: 123,
  bam: &#39;abc&#39;, // bam이 Foo에 정의되지 않았다고 에러 발생
};</code></pre>
<hr>
<h3 id="497-keep-try-blocks-focused">4.9.7 Keep try blocks focused</h3>
<p><strong><code>try</code> 블록에 포함된 코드는 최소화하는 것이 좋다</strong>:
어떤 코드가 예외를 던질 가능성이 있는지 명확하게 드러나도록 하기 위함이다.</p>
<p><strong>Before</strong></p>
<pre><code class="language-typescript">try {
  const result = methodThatMayThrow(); // 예외를 던질 가능성 있음
  use(result);                         // 예외를 던지지 않음
} catch (error: unknown) {
  // ...
}</code></pre>
<ul>
<li><code>use(result)</code>는 예외를 던지지 않지만 <code>try</code> 블록 안에 포함되어 있어 독자에게 혼란을 줄 수 있다.</li>
<li><em>After*</em><pre><code class="language-typescript">let result;
try {
 result = methodThatMayThrow(); // 예외를 던질 가능성이 있는 코드만 포함
} catch (error: unknown) {
 // ...
}
use(result); // 예외를 던지지 않는 코드는 `try` 블록 밖으로 이동</code></pre>
</li>
<li>이제 독자가 <code>methodThatMayThrow()</code>만 예외를 던질 가능성이 있음을 명확히 알 수 있다.<pre><code class="language-typescript">try {
use(methodThatMayThrow()); // 코드가 간단하므로 별도로 나눌 필요 없음
} catch (error: unknown) {
// ...
}</code></pre>
</li>
</ul>
<p><strong>루프 안에 <code>try</code> 블록을 반복적으로 선언하면 성능 문제가 발생할 수 있다</strong>:
이 경우, try 블록의 범위를 루프 전체로 확장하는 것이 낫다.
<strong>Before</strong></p>
<pre><code class="language-typescript">for (const item of items) {
  try {
    processItem(item); // 매번 `try` 블록이 재생성됨
  } catch (error: unknown) {
    // ...
  }
}</code></pre>
<p><strong>After</strong></p>
<pre><code class="language-typescript">try {
  for (const item of items) {
    processItem(item); // `try` 블록이 한 번만 생성됨
  }
} catch (error: unknown) {
  // ...
}</code></pre>
<hr>
<h2 id="410-decorators">4.10 Decorators</h2>
<p>데코레이터는 클래스, 메서드, 필드, 또는 파라미터에 메타데이터를 추가하거나, 동작을 수정할 수 있도록 도와주는 특수한 문법이다.
데코레이터는 @ 기호로 시작하며, 함수 형태로 정의된다.</p>
<pre><code class="language-typescript">@Component({
  selector: &#39;my-component&#39;,
  template: &#39;&lt;p&gt;My Component&lt;/p&gt;&#39;,
})
class MyComponent {}</code></pre>
<p><strong>새로운 데코레이터를 정의하지 말고, 프레임워크 제공 데코레이터만 사용하라</strong>:
데코레이터는 TypeScript에서 실험적(Experimental)으로 도입되었다.
하지만 JavaScript 표준화 기구인 TC39의 최종 표준안과는 다르게 동작하는 부분이 있다.
실험적 기능이기 때문에 이미 발견된 몇 가지 버그들이 수정되지 않을 가능성이 있다.
이런 이유로 새로운 데코레이터를 정의하거나 사용하는 것이 위험할 수 있다.
프레임워크에서 제공하는 공식 데코레이터만 사용해야 한다.</p>
<p><strong>데코레이터는 빈 줄 없이 바로 적용 대상 앞에 위치시켜 가독성과 유지보수성을 높여라</strong>:
<strong>Before</strong></p>
<pre><code class="language-typescript">/** JSDoc comments */
@Component({...}) 

class MyComponent {}  // 빈 줄이 있어서 잘못된 예</code></pre>
<p><strong>After</strong></p>
<pre><code class="language-typescript">/** JSDoc comments */
@Component({...})
class MyComponent {}  // 빈 줄 없이 바로 연결</code></pre>
<pre><code class="language-typescript">class MyComp {
  @Input() myField: string;  // 같은 줄
  @Input()
  myOtherField: string;  // 다음 줄로 감싸기
}</code></pre>
<ul>
<li>필드에 데코레이터를 붙일 때는 같은 줄에 작성하거나 다음 줄로 감싸는 방식 모두 허용된다.</li>
</ul>
<hr>
<h2 id="411-disallowed-features">4.11 Disallowed features</h2>
<h3 id="4111-wrapper-objects-for-primitive-types">4.11.1 Wrapper objects for primitive types</h3>
<p>래퍼 클래스(wrapper class)는 JavaScript의 원시 타입(String, Boolean, Number)을 객체로 감싸는 기능을 제공한다.
예를 들어, String, Boolean, Number는 각각 원시 타입을 객체처럼 다룰 수 있게 해 준다.</p>
<pre><code class="language-typescript">const s = new String(&#39;hello&#39;);  // 문자열 &#39;hello&#39;를 String 객체로 감쌈
const b = new Boolean(false);  // 불리언 false를 Boolean 객체로 감쌈
const n = new Number(5);       // 숫자 5를 Number 객체로 감쌈</code></pre>
<p><strong>원시 타입을 감싸는 래퍼 클래스를 직접 인스턴스화하지 말자</strong>:
래퍼 클래스를 인스턴스화하면 원시 값이 아니라 객체가 생성된다.
이로 인해 의도와 다른 동작을 초래할 수 있다.
원시 타입을 객체로 감싸는 것은 메모리와 성능 측면에서 불필요한 오버헤드를 발생시킨다.
원시 타입 자체는 가볍고 빠르게 동작하도록 설계되었다.</p>
<p><strong>Before</strong></p>
<pre><code class="language-typescript">const b = new Boolean(false);
console.log(b);  // Boolean {false}
console.log(b == true);  // true
console.log(b ? &#39;true&#39; : &#39;false&#39;);  // &#39;true&#39;</code></pre>
<p>new Boolean(false)는 불리언 객체를 반환하며, 객체는 항상 true로 평가된다.
즉, 실제 값이 false임에도 true로 평가되는 문제가 발생한다.</p>
<pre><code class="language-typescript">const s1 = &#39;hello&#39;;
const s2 = new String(&#39;hello&#39;);

console.log(s1 === s2);  // false (문자열 vs 객체)
console.log(s1 == s2);   // true (느슨한 비교로 동등하다고 평가)</code></pre>
<p><strong>After</strong></p>
<pre><code class="language-typescript">const s = &#39;hello&#39;;  // 원시 문자열
const b = false;    // 원시 불리언
const n = 5;        // 원시 숫자</code></pre>
<pre><code class="language-typescript">const s = String(&#39;hello&#39;);  // 문자열로 변환
const b = Boolean(0);       // 불리언으로 변환
const n = Number(&#39;42&#39;);     // 숫자로 변환</code></pre>
<ul>
<li>타입 강제 변환은 래퍼 클래스 함수 호출로 처리해야 한다.</li>
<li>이는 +, &#39;&#39;와 같은 비권장 방식보다 명시적이고 안전하다.</li>
</ul>
<hr>
<h3 id="4112-automatic-semicolon-insertion">4.11.2 Automatic Semicolon Insertion</h3>
<p>모든 문장을 세미콜론으로 명시적으로 끝내야 한다. 
이렇게 하면 잘못된 세미콜론 삽입으로 인한 버그를 방지하고 ASI 지원이 제한된 도구(예: clang-format)와의 호환성을 보장한다.</p>
<hr>
<h3 id="4113-const-enums">4.11.3 Const enums</h3>
<p><strong>const enum을 사용해서는 안 된다</strong>:
const enum은 컴파일 결과에서 사라지므로, JavaScript로 작성된 코드나 다른 모듈에서 이 열거형을 참조할 수 없다. 만약 const enum을 사용한 TypeScript 코드가 JavaScript 모듈과 함께 사용될 경우, 예상치 못한 오류가 발생할 수 있다.
const enum은 값이 인라인되기 때문에 디버깅 과정에서 열거형 이름을 볼 수 없다. 이는 디버깅 정보를 잃게 만들고, 유지보수를 어렵게 한다. 또한 열거형의 값이 여러 곳에 인라인으로 삽입되면, 열거형 값이 변경될 때 컴파일된 JavaScript의 모든 참조를 다시 컴파일해야 한다.
const 열거형은 모듈의 JavaScript 사용자에게 열거형을 보이지 않게 하는 최적화와 관련된 별도의 언어 기능이며, JavaScript 표준이 아니다. 따라서 다른 환경에서 호환되지 않을 가능성이 있다.</p>
<p><strong>enum</strong>:</p>
<pre><code class="language-typescript">enum Color {
  Red,
  Green,
  Blue,
}

console.log(Color.Red);  // 출력: 0</code></pre>
<p>컴파일 후 JavaScript:</p>
<pre><code class="language-JavaScript">var Color;
(function (Color) {
  Color[Color[&quot;Red&quot;] = 0] = &quot;Red&quot;;
  Color[Color[&quot;Green&quot;] = 1] = &quot;Green&quot;;
  Color[Color[&quot;Blue&quot;] = 2] = &quot;Blue&quot;;
})(Color || (Color = {}));

console.log(Color.Red);  // 출력: 0</code></pre>
<p><strong>const enum</strong>:</p>
<pre><code class="language-typescript">const enum Color {
  Red,
  Green,
  Blue,
}

console.log(Color.Red);  // 출력: 0</code></pre>
<p>컴파일 후 JavaScript:</p>
<pre><code class="language-JavaScript">console.log(0);  // `Color.Red`가 상수로 대체됨</code></pre>
<hr>
<h3 id="4114-debugger-statements">4.11.4 Debugger statements</h3>
<p><code>debugger</code>문은 프로덕션 코드에 포함되어서는 안 된다.
debugger는 JavaScript에서 디버깅을 위해 사용되는 명령어로, 코드 실행을 중단하고 디버깅 세션을 시작할 수 있도록 돕는다.
이 명령어는 주로 브라우저의 개발자 도구나 Node.js의 디버깅 모드에서 실행될 때 유용하게 사용된다.</p>
<pre><code class="language-javascript">function debugMe() {
  debugger;  // 디버거가 활성화된 상태에서 실행 시, 코드가 중단된다.
  console.log(&quot;This will not run unless debugger is removed.&quot;);
}</code></pre>
<p><code>debugger</code>는 코드 실행을 중단시키므로, 사용자가 웹 애플리케이션을 사용할 때 예기치 않게 페이지가 멈추거나 동작이 중단될 수 있다.
프로덕션 환경에서는 디버깅을 위한 코드 실행 중단이 사용자 경험을 방해하거나 서비스의 가용성에 악영향을 미칠 수 있다.</p>
<p><code>debugger</code> 문이 코드에 남아 있으면, 프로덕션 환경에서도 디버깅을 할 수 있는 상태로 남게 된다. 이는 보안 위험을 초래할 수 있다.
공격자는 브라우저의 개발자 도구를 통해 디버거를 활성화하고 애플리케이션의 상태를 추적하거나 악용할 수 있다.
특히, 민감한 데이터를 처리하는 애플리케이션에서는 이러한 디버깅 코드가 보안 취약점을 유발할 수 있다.</p>
<blockquote>
<p>배포 후 디버깅은 실제 코드에 debugger를 삽입하는 대신, 로그 파일이나 에러 추적 시스템을 활용하여 디버깅을 수행하는 것이 좋다.</p>
</blockquote>
<hr>
<h3 id="4115-with">4.11.5 with</h3>
<p><strong><code>with</code> 키워드 사용 금지</strong>: 
코드를 이해하기 어렵게 만들고 ES5 이후로 strict 모드에서 금지되었다.</p>
<hr>
<h3 id="4116-dynamic-code-evaluation">4.11.6 Dynamic code evaluation</h3>
<p><code>eval</code>이나 <code>Function(...string)</code> 생성자를 사용 금지(코드 로더 제외).
이러한 기능은 잠재적으로 위험하며 엄격한 콘텐츠 보안 정책을 사용하는 환경에서는 제대로 작동하지 않는다.</p>
<pre><code class="language-javascript">const userInput = &quot;alert(&#39;Hacked!&#39;)&quot;;
eval(userInput);  // 위험: 사용자가 악의적인 코드를 삽입할 수 있음

const userInput = &quot;alert(&#39;Malicious code&#39;)&quot;;
const fn = new Function(userInput);
fn();  // 위험: 악의적인 코드 실행</code></pre>
<hr>
<h3 id="4117-non-standard-features">4.11.7 Non-standard features</h3>
<p><strong>표준화된 ECMAScript 및 Web Platform 기능만 사용하자</strong>:
비표준 ECMAScript나 웹 플랫폼 기능으로 간주되는 것들은 다음과 같다.</p>
<ul>
<li>구식 기능 또는 더 이상 지원되지 않는 기능</li>
<li>표준화되지 않은 새로운 ECMAScript 기능</li>
<li>현재 TC39 작업 초안이나 제안 중인 기능</li>
<li>제안되었지만 아직 완성되지 않은 웹 표준</li>
<li>외부 트랜스파일러 확장 기능</li>
<li>특정 JavaScript 런타임을 타겟팅하는 API 사용</li>
</ul>
<hr>
<h3 id="4118-modifying-builtin-objects">4.11.8 Modifying builtin objects</h3>
<p><strong>내장 타입을 수정하지 마라</strong>:
JavaScript에는 내장된 타입들, 예를 들어 <code>Array</code>, <code>Object</code>, <code>String</code>, <code>Number</code>, <code>Boolean</code>, <code>Function</code> 등이 있다. </p>
<p>내장 타입을 수정하면, 기존 코드나 외부 라이브러리가 의도한 대로 동작하지 않을 수 있다. 예를 들어, <code>Array</code>에 새로운 메서드를 추가하면, 다른 코드에서 기존 메서드들과 충돌하거나 예기치 않은 결과를 초래할 수 있다.
<code>Array.prototype</code>에 <code>newMethod</code>를 추가한다고 가정했을 때, 다른 라이브러리가 같은 이름의 메서드를 추가하거나, 예상치 못한 방식으로 동작할 수 있다.</p>
<p>표준 JavaScript 동작을 변경하는 것은 코드의 호환성 문제를 유발할 수 있다. 예를 들어, 새로운 자바스크립트 버전이나 다른 환경에서는 다르게 동작할 가능성이 있으며, 이는 예상치 못한 버그를 초래할 수 있다.</p>
<p>내장 타입을 수정하면 디버깅을 어렵게 만들 수 있다. 수정된 메서드는 JavaScript 엔진의 표준 동작을 변경하므로, 문제가 발생할 경우 이를 추적하기가 힘들어진다.</p>
<p><strong>글로벌 객체에 심볼을 추가하지 마라</strong>:
글로벌 객체는 JavaScript 실행 환경에서 전역적으로 접근 가능한 객체를 의미한다. 브라우저에서는 <code>window</code>, Node.js에서는 <code>global</code> 객체가 해당된다. 이 규칙은 글로벌 객체에 심볼이나 속성을 추가하지 말고, 추가가 필요하다면 꼭 필요한 경우에만 하라는 것이다.</p>
<p>글로벌 객체는 애플리케이션 내 모든 코드에서 접근 가능하다. 여기에 심볼이나 속성을 추가하면, 전역 변수의 이름이 충돌할 가능성이 높아지며, 이는 코드의 예기치 못한 동작을 초래할 수 있다. 특히, 외부 라이브러리나 다른 개발자와 협업할 때 문제가 될 수 있다.</p>
<p>글로벌 객체에 불필요한 심볼이나 속성이 많이 추가되면, 코드의 복잡성이 증가하고, 성능에도 영향을 미칠 수 있다. 이는 특히 대규모 애플리케이션에서 문제가 될 수 있다.</p>
<p>글로벌 객체에 의도치 않은 속성이 추가되면, 디버깅할 때 혼란을 초래할 수 있다. 어떤 코드에서 해당 심볼을 수정하거나 참조할 수 있기 때문에, 문제가 발생했을 때 추적이 어렵다.</p>
<p>글로벌 객체에 심볼을 추가할 때는 반드시 그 이유를 명확히 하고, 외부에서 이 심볼에 접근하거나 수정할 가능성을 최소화해야 한다.</p>
<hr>
<h4 id="참고자료">참고자료</h4>
<p><a href="https://google.github.io/styleguide/tsguide.html">Google TypeScript Style Guide</a></p>
<p><a href="https://github.com/google/gts">GitHub - google/gts: ☂️ TypeScript style guide, formatter, and linter. </a></p>
<p><a href="https://www.youtube.com/watch?v=lhQhsIrRXoc">Typescript Google Code Style Part 1</a><br><a href="https://www.youtube.com/watch?v=idzUmsnPM-g">Typescript Google Code Style Part 2</a> 
<a href="https://www.youtube.com/watch?v=yNWzUsGdD54">Typescript Google Code Style Part 3</a></p>
<p><a href="https://ts.dev/style/">ts.dev - TypeScript style guide</a></p>
]]></description>
        </item>
    </channel>
</rss>