<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>낙서장이자 오답 노트이자 컨닝 페이퍼</title>
    <link>https://nang-in.tistory.com/</link>
    <description>1 일 1 포스팅 가보자~~</description>
    <language>ko</language>
    <pubDate>Thu, 23 Jul 2026 10:51:33 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>NangIn</managingEditor>
    <image>
      <title>낙서장이자 오답 노트이자 컨닝 페이퍼</title>
      <url>https://tistory1.daumcdn.net/tistory/6250659/attach/7d3775cffb48453281114b71fcb3303d</url>
      <link>https://nang-in.tistory.com</link>
    </image>
    <item>
      <title>패스트캠퍼스 환급챌린지 35일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기</title>
      <link>https://nang-in.tistory.com/158</link>
      <description>&lt;p style=&quot;background-color: #e7f8ff; color: #000000; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;강의 내용 정리&amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Database Seeding&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;개요: DB 시딩이란?&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;DB 시딩(Seeding)&lt;/b&gt;: 개발 환경에서 테스트나 시연을 위해 샘플 데이터를 자동으로 삽입하는 작업&lt;/li&gt;
&lt;li&gt;프로덕션에서는 잘 쓰지 않지만, &lt;b&gt;개발/테스트 환경에서는 필수&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;폴더 구조 및 시딩 준비&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Postman에 시드 폴더 생성&lt;/li&gt;
&lt;li&gt;Authorization &amp;rarr; Bearer Token (환경 변수로 access_token 사용)&lt;/li&gt;
&lt;li&gt;시딩 순서:
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;로그인&lt;/li&gt;
&lt;li&gt;다이렉터 생성&lt;/li&gt;
&lt;li&gt;장르 생성&lt;/li&gt;
&lt;li&gt;무비 생성&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;랜덤 데이터 생성 (Pre/Post 스크립트 활용)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;랜덤 다이렉터 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;랜덤 풀네임, 랜덤 국가 &amp;rarr; name, country 값 자동 생성&lt;/li&gt;
&lt;li&gt;Postman Test 탭에서 응답 받은 id를 environment variable로 저장:&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const body = pm.response.json();
let directors = pm.environment.get('director_ids') || '';
directors = directors ? `${directors},${body.id}` : `${body.id}`;
pm.environment.set('director_ids', directors);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;랜덤 장르 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;랜덤 명사, 형용사를 조합하여 장르 이름 생성&lt;/li&gt;
&lt;li&gt;중복 방지 로직 추가:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미 존재하면 400 에러 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;위와 같은 방식으로 장르 ID 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;무비 시딩을 위한 관계 랜덤 연결&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;무비 생성 시 필요한 정보
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;title: 랜덤 명사 + 형용사 + 숫자&lt;/li&gt;
&lt;li&gt;description: 랜덤 문장&lt;/li&gt;
&lt;li&gt;directorId: 시딩된 다이렉터 중 하나 랜덤 선택&lt;/li&gt;
&lt;li&gt;genreIds: 장르 중 3개 랜덤 중복 없이 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Pre-request Script 예시
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;const _ = require('lodash');

// 다이렉터 ID 랜덤 선택
const directorIds = pm.environment.get(&quot;director_ids&quot;).split(&quot;,&quot;);
const randomDirectorId = _.sample(directorIds);
pm.environment.set(&quot;director&quot;, randomDirectorId);

// 장르 ID들 중 3개 랜덤 중복 없이 선택
const genreIds = pm.environment.get(&quot;genre_ids&quot;).split(&quot;,&quot;);
let picked = [];
let failCount = 0;
while (picked.length &amp;lt; 3 &amp;amp;&amp;amp; failCount &amp;lt; 20) {
  const g = _.sample(genreIds);
  if (!picked.includes(g)) {
    picked.push(g);
  } else {
    failCount++;
  }
}
pm.environment.set(&quot;genres&quot;, picked);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Postman Runner로 대량 생성&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Postman Collection Runner 활용법
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Runner에서 시드 폴더 선택 &amp;rarr; 실행 순서 지정&lt;/li&gt;
&lt;li&gt;반복 횟수(iteration) 설정 (ex. 감독 10명, 장르 50개, 영화 200개)&lt;/li&gt;
&lt;li&gt;중복 에러 발생 가능하지만 일부 실패는 무방&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;테이블 리셋 후 재시딩&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB 테이블 드랍 &amp;rarr; 서버 재시작 &amp;rarr; Postman으로 다시 시딩&lt;/li&gt;
&lt;li&gt;유저 등록 &amp;rarr; 롤 변경 (어드민) &amp;rarr; 로그인 &amp;rarr; 다시 시딩 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;학습 후기&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의를 통해 Postman을 활용한 Database Seeding 자동화 방법을 배우면서, 백엔드 개발에서 데이터 준비가 얼마나 중요한지 다시금 느꼈습니다. 실제 서비스에서는 프로덕션 데이터를 직접 다루지만, 개발과 테스트 환경에서는 시딩을 통해 일관된 테스트 환경을 유지하는 것이 매우 중요합니다. 단순히 데이터를 넣는 것에 그치는 게 아니라, 관계형 데이터를 유의미하게 구성하고, 이를 반복적으로 쉽게 재사용할 수 있도록 자동화하는 흐름을 체득할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 인상 깊었던 점은 Postman 스크립트를 활용한 랜덤 데이터 생성과 변수 저장 방식이었습니다. Pre-request Script에서는 lodash와 같은 유틸을 활용해 랜덤한 감독 ID, 장르 ID 등을 추출하고, 이를 Postman 환경변수에 저장하는 구조가 굉장히 유연하고 강력했습니다. 단순한 API 테스트 도구로만 알고 있었던 Postman이 실제로는 데이터 생성용 워크플로우 자동화 도구로 활용될 수 있다는 점에서 눈이 열렸습니다. 테스트 탭에서 응답값을 파싱하고 환경변수에 저장해 다음 요청에 활용하는 방식은 서비스 개발 초기에 데이터를 대량으로 준비해야 할 때 큰 도움이 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Postman Runner를 활용해 반복 횟수를 조정하면서 대량 데이터를 생성하는 기능도 유용했습니다. 단순히 다이렉터, 장르, 무비를 순서대로 생성하는 것뿐만 아니라, 각 무비가 관계를 맺고 있는 다이렉터와 장르를 랜덤하게 연결하는 과정이 포함되어 있어서, 실무에서 자주 마주하게 될 관계형 데이터 시딩 시나리오를 미리 체험해 볼 수 있었습니다. 특히 반복 중 일부 실패는 무시해도 되는 전략 덕분에 오류에 유연하게 대응할 수 있었고, 이는 테스트 효율성을 높이는 데 중요한 요소라고 느꼈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, 시딩 작업을 진행하며 데이터 초기화 및 재시딩 흐름까지 자연스럽게 익힐 수 있었습니다. NestJS 서버 재시작과 함께 데이터베이스를 초기화하고 다시 시딩하는 일련의 작업이 반복되면서, 백엔드 초기화 &amp;rarr; 유저 등록 &amp;rarr; 권한 설정 &amp;rarr; 시딩 자동화라는 전반적인 백엔드 개발 워크플로우를 체득하게 되었습니다. 실무에서는 종종 staging 환경에서 테스트 데이터를 넣어야 하는 상황이 있는데, 이 방법을 숙지해두면 반복 작업 없이 빠르게 처리할 수 있을 것이라 확신합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 학습을 통해 Postman은 단순한 API 테스트 도구가 아니라, 테스트 환경 구축과 데이터 워크플로우 자동화까지 지원하는 매우 유용한 도구임을 몸소 체감했습니다.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;학습 인증샷&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;314&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CyXFw/btsNCoZwtPX/u07IZlRy3m6xCyjAC4LYG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CyXFw/btsNCoZwtPX/u07IZlRy3m6xCyjAC4LYG1/img.png&quot; data-alt=&quot;수강 인증 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CyXFw/btsNCoZwtPX/u07IZlRy3m6xCyjAC4LYG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCyXFw%2FbtsNCoZwtPX%2Fu07IZlRy3m6xCyjAC4LYG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;314&quot; height=&quot;124&quot; data-origin-width=&quot;314&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;수강 인증 사진&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bV3QYK/btsNAG1H1lM/87xVAyouJX8izBCmZxJLG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bV3QYK/btsNAG1H1lM/87xVAyouJX8izBCmZxJLG0/img.png&quot; data-alt=&quot;학습 인증샷&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bV3QYK/btsNAG1H1lM/87xVAyouJX8izBCmZxJLG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbV3QYK%2FbtsNAG1H1lM%2F87xVAyouJX8izBCmZxJLG0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1032&quot; height=&quot;466&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;466&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;학습 인증샷&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;210&quot; data-origin-height=&quot;117&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IHWjR/btsNBwjnYWn/oiSjOpap5hVhaKLm1GDksK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IHWjR/btsNBwjnYWn/oiSjOpap5hVhaKLm1GDksK/img.png&quot; data-alt=&quot;공부 시작 시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IHWjR/btsNBwjnYWn/oiSjOpap5hVhaKLm1GDksK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIHWjR%2FbtsNBwjnYWn%2FoiSjOpap5hVhaKLm1GDksK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;210&quot; height=&quot;117&quot; data-origin-width=&quot;210&quot; data-origin-height=&quot;117&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공부 시작 시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;207&quot; data-origin-height=&quot;96&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/X6pHt/btsNCfnT4aF/x4auFVCjmnhj6kkbz7CLb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/X6pHt/btsNCfnT4aF/x4auFVCjmnhj6kkbz7CLb1/img.png&quot; data-alt=&quot;공부 종료 시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/X6pHt/btsNCfnT4aF/x4auFVCjmnhj6kkbz7CLb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FX6pHt%2FbtsNCfnT4aF%2Fx4auFVCjmnhj6kkbz7CLb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;207&quot; height=&quot;96&quot; data-origin-width=&quot;207&quot; data-origin-height=&quot;96&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공부 종료 시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333;&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;a href=&quot;https://abit.ly/lisbva&quot;&gt;https://abit.ly/lisbva&lt;/a&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;</description>
      <category>습관형성</category>
      <category>오공완</category>
      <category>직장인자기계발</category>
      <category>패스트캠퍼스</category>
      <category>패스트캠퍼스후기</category>
      <category>환급챌린지</category>
      <author>NangIn</author>
      <guid isPermaLink="true">https://nang-in.tistory.com/158</guid>
      <comments>https://nang-in.tistory.com/158#entry158comment</comments>
      <pubDate>Mon, 5 May 2025 14:05:57 +0900</pubDate>
    </item>
    <item>
      <title>패스트캠퍼스 환급챌린지 34일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기</title>
      <link>https://nang-in.tistory.com/159</link>
      <description>&lt;p style=&quot;background-color: #e7f8ff; color: #000000; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;강의 내용 정리&amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;환경변수와 스크립트 작성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제 상황: 매번 토큰 복사 &amp;rarr; 붙여넣기 너무 번거로움&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 API가 인증 필요 &amp;rarr; 매번 로그인 &amp;rarr; 토큰 복사 &amp;rarr; Authorization 헤더에 붙여야 함&lt;/li&gt;
&lt;li&gt;실무에서는 &lt;b&gt;자동화/재사용&lt;/b&gt;이 필요함&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해결 1: 환경변수 (Environment Variable) 활용&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;환경 만들기&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;좌측 하단 Environments &amp;rarr; + 클릭 &amp;rarr; 환경 이름 입력
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: FastCampus NestJS&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;변수 추가:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Key: host, Initial: (비워둬도 됨), Current: http://localhost:3000&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;저장 후 우측 상단에서 해당 환경 활성화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;변수 사용법&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;URL 앞부분을 {{host}} 로 치환&lt;/li&gt;
&lt;li&gt;{{host}}/movies&lt;/li&gt;
&lt;li&gt;마우스 올려 보면 적용된 값 확인 가능&lt;/li&gt;
&lt;li&gt;추후 배포 시 환경만 바꾸면 되므로 매우 유용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해결 2: 로그인 시 토큰 자동 저장&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;스크립트 작성&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그인 API &amp;rarr; Tests 탭에 아래 스크립트 입력&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;const body = pm.response.json();
pm.environment.set(&quot;access_token&quot;, body.accessToken);
pm.environment.set(&quot;refresh_token&quot;, body.refreshToken);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;응답에서 토큰을 추출해 &lt;b&gt;환경 변수로 저장&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;토큰 확인&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Environments &amp;rarr; 해당 환경 &amp;rarr; 변수 자동 생성됨
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;access_token&lt;/li&gt;
&lt;li&gt;refresh_token&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해결 3: Authorization 자동 주입&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Authorization에 환경 변수 사용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;헤더 &amp;gt; Authorization &amp;gt; Bearer {{access_token}}&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;반복 방지: Inherit auth from parent 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각각의 요청/폴더/컬렉션에서:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Authorization 설정 &amp;rarr; Inherit auth from parent&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최상위 컬렉션&lt;/b&gt;에서만 직접 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Bearer Token 방식 선택&lt;/li&gt;
&lt;li&gt;Token 값: {{access_token}}&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;토큰 자동 갱신 흐름
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;로그인 실행 &amp;rarr; access_token 갱신됨&lt;/li&gt;
&lt;li&gt;모든 요청은 {{access_token}} 사용 &amp;rarr; 자동 적용&lt;/li&gt;
&lt;li&gt;다시는 복붙 필요 없음&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;학습 후기&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의를 통해 Postman을 활용한 API 테스트 자동화 및 인증 토큰 관리의 중요성과 구체적인 방법들을 배울 수 있었습니다. NestJS 기반의 백엔드 서버를 개발하면서 모든 API가 인증을 필요로 하다 보니, 로그인 &amp;rarr; 토큰 복사 &amp;rarr; 헤더에 붙여넣는 반복 작업이 점점 부담으로 다가왔습니다. 하지만 환경 변수와 스크립트를 적절히 활용하니, 이러한 번거로움이 한 번에 해결되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 유용했던 점은 환경변수 설정입니다. {{host}}와 같은 변수로 API의 base URL을 추상화해두니, 추후 서버 환경이 변경되더라도 환경값만 바꾸면 전체 테스트 스크립트를 수정할 필요가 없어졌습니다. 실제로 로컬에서 테스트할 때는 localhost를 사용하고, 추후 배포 시에는 프로덕션 URL만 바꾸면 되니 협업이나 배포 자동화에도 유리할 것이라 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 로그인 API의 응답에서 access_token과 refresh_token을 자동으로 저장하는 방식은 매우 강력했습니다. Postman의 Tests 탭에 간단한 JavaScript를 삽입함으로써 로그인 요청 이후의 모든 요청이 자동으로 최신 토큰을 반영하게 되었고, 헤더 설정 또한 Bearer {{access_token}}으로 통일할 수 있었습니다. 이런 자동화 덕분에 테스트 효율이 크게 향상되었고, 테스트 중에 토큰이 만료되더라도 단지 로그인 요청 하나만 재전송하면 전체 환경이 다시 유효해진다는 점이 실무에서도 매우 유용하게 쓰일 것이라 느꼈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 컬렉션 상속을 통한 Authorization 설정은 Postman을 깊이 있게 활용하는 첫 경험이었습니다. 각각의 요청에서 Authorization을 따로 설정할 필요 없이, 최상위 컬렉션에서 한 번만 설정하고 모든 하위 요청은 상속받게 하는 방식은 코드의 DRY(Don&amp;rsquo;t Repeat Yourself) 원칙과도 일맥상통한다고 느꼈습니다. 실수도 줄고, 협업자와의 공유 시에도 구조적으로 깔끔하게 유지할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습은 단순한 토큰 처리 자동화 수준을 넘어, API 테스트 자체의 생산성과 안정성을 끌어올릴 수 있다는 인사이트를 주었습니다. 백엔드 개발자로서 단순히 API를 만드는 것에 그치지 않고, 테스트 자동화와 관리 효율성까지 고려하는 것이 진짜 실무 능력이라는 점을 절실히 느꼈습니다. 앞으로 다른 프로젝트에서도 이 방식을 그대로 적용할 생각입니다.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;학습 인증샷&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;291&quot; data-origin-height=&quot;159&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwhQUL/btsNJ0STKQw/NhkBHtH2Q8CJnL3K8A8WDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwhQUL/btsNJ0STKQw/NhkBHtH2Q8CJnL3K8A8WDk/img.png&quot; data-alt=&quot;수강 인증 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwhQUL/btsNJ0STKQw/NhkBHtH2Q8CJnL3K8A8WDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwhQUL%2FbtsNJ0STKQw%2FNhkBHtH2Q8CJnL3K8A8WDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;291&quot; height=&quot;159&quot; data-origin-width=&quot;291&quot; data-origin-height=&quot;159&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;수강 인증 사진&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1039&quot; data-origin-height=&quot;341&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daTHyK/btsNJVxf5Lm/iSl30orX4tBzp22JYtm3Z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daTHyK/btsNJVxf5Lm/iSl30orX4tBzp22JYtm3Z1/img.png&quot; data-alt=&quot;학습 인증샷&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daTHyK/btsNJVxf5Lm/iSl30orX4tBzp22JYtm3Z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdaTHyK%2FbtsNJVxf5Lm%2FiSl30orX4tBzp22JYtm3Z1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1039&quot; height=&quot;341&quot; data-origin-width=&quot;1039&quot; data-origin-height=&quot;341&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;학습 인증샷&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;152&quot; data-origin-height=&quot;94&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQLmei/btsNK4AfzTG/KKuDlhHpL6kYyVba93KkRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQLmei/btsNK4AfzTG/KKuDlhHpL6kYyVba93KkRK/img.png&quot; data-alt=&quot;공부 시작 시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQLmei/btsNK4AfzTG/KKuDlhHpL6kYyVba93KkRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQLmei%2FbtsNK4AfzTG%2FKKuDlhHpL6kYyVba93KkRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;152&quot; height=&quot;94&quot; data-origin-width=&quot;152&quot; data-origin-height=&quot;94&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공부 시작 시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;179&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u2sgs/btsNJ5GImlp/ywXGw3I9EOA39axUNHXAJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u2sgs/btsNJ5GImlp/ywXGw3I9EOA39axUNHXAJK/img.png&quot; data-alt=&quot;공부 종료 시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u2sgs/btsNJ5GImlp/ywXGw3I9EOA39axUNHXAJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu2sgs%2FbtsNJ5GImlp%2FywXGw3I9EOA39axUNHXAJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;179&quot; height=&quot;86&quot; data-origin-width=&quot;179&quot; data-origin-height=&quot;86&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공부 종료 시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333;&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;a href=&quot;https://abit.ly/lisbva&quot;&gt;https://abit.ly/lisbva&lt;/a&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;</description>
      <category>NestJS</category>
      <category>습관형성</category>
      <category>오공완</category>
      <category>직장인자기계발</category>
      <category>패스트캠퍼스</category>
      <category>패스트캠퍼스후기</category>
      <category>환급챌린지</category>
      <author>NangIn</author>
      <guid isPermaLink="true">https://nang-in.tistory.com/159</guid>
      <comments>https://nang-in.tistory.com/159#entry159comment</comments>
      <pubDate>Sun, 4 May 2025 14:50:09 +0900</pubDate>
    </item>
    <item>
      <title>패스트캠퍼스 환급챌린지 33일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기</title>
      <link>https://nang-in.tistory.com/157</link>
      <description>&lt;p style=&quot;background-color: #e7f8ff; color: #000000; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;강의 내용 정리&amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Role Based Access Control 이론&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;RBAC이란?&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;RBAC&lt;/b&gt; = &lt;b&gt;Role-Based Access Control&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;번역: &lt;b&gt;역할 기반 인가 시스템&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;역할(Role)에 따라 리소스에 대한 **권한(Permission)**을 부여&lt;/li&gt;
&lt;li&gt;특정 리소스의 CRUD(Create, Read, Update, Delete) 권한을 &lt;b&gt;역할에 따라 제어&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;왜 필요한가?&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자의 등급/역할에 따라 접근 가능한 기능을 나눌 수 있음&lt;/li&gt;
&lt;li&gt;예:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;어드민(Admin)&lt;/b&gt;: 모든 기능 접근&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유저(User)&lt;/b&gt;: 본인 리소스만 접근 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;게스트(Guest)&lt;/b&gt;: 열람만 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;구현 난이도&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RBAC는 매우 복잡하게 설계할 수 있지만, 단순하게 시작하는 것이 좋음&lt;/li&gt;
&lt;li&gt;복잡하게 설계할수록:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기능 설계 난이도 급증&lt;/li&gt;
&lt;li&gt;기획자/개발자 간 커뮤니케이션 비용 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;권한 설계는 유지보수가 매우 어려워지므로 &lt;b&gt;작게 시작하고 점진적으로 확장&lt;/b&gt;할 것&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;기본 구현 아이디어&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;역할 분류 예시
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;enum Role {
  ADMIN = 'ADMIN',
  USER = 'USER',
  GUEST = 'GUEST',
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;커스텀 데코레이터로 역할 지정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Roles(Role.ADMIN)
@Roles(Role.USER)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;가드 내부에서 처리 로직
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Roles() 데코레이터에서 역할 정보 추출&lt;/li&gt;
&lt;li&gt;요청의 req.user.role 과 비교&lt;/li&gt;
&lt;li&gt;권한 없으면 ForbiddenException 던짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;역할 지정이 없을 경우 처리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;if (!roles) {
  return true; // 권한 검사하지 않음
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;프로젝트마다 디폴트 정책은 다르게 설정 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;RBAC 구현하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;커스텀 데코레이터 생성&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일&lt;/b&gt;: auth/decorators/rbac.decorator.ts
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;export const Rbac = createParamDecorator&amp;lt;Role&amp;gt;((role) =&amp;gt; role);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;@Rbac(Role.ADMIN) 과 같이 사용될 예정&lt;/li&gt;
&lt;li&gt;역할(Roles)을 데코레이터로 엔드포인트에 명시함&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;RbacGuard 생성&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일&lt;/b&gt;: auth/guards/rbac.guard.ts&lt;/li&gt;
&lt;li&gt;CanActivate 가드 인터페이스 구현&lt;/li&gt;
&lt;li&gt;&lt;b&gt;핵심 로직&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// 1. Reflector를 통해 @Rbac 데코레이터 메타데이터 추출
const requiredRole = this.reflector.get&amp;lt;Role&amp;gt;(RBAC_KEY, context.getHandler());

if (!requiredRole) return true; // 데코레이터 없으면 제한 없음

// 2. Request 객체에서 유저의 role 추출
const req = context.switchToHttp().getRequest();
const user = req.user;

if (!user) return false; // 로그인 안 되어 있으면 차단

// 3. 권한 체크 (숫자가 작을수록 높은 권한)
return user.role &amp;lt;= requiredRole;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;역할 Enum 정의&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;예시&lt;/b&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;export enum Role {
  ADMIN = 0,
  PAID_USER = 1,
  USER = 2,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;숫자가 &lt;b&gt;작을수록 높은 권한&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;0 = 최고 권한&lt;/li&gt;
&lt;li&gt;2 = 일반 유저&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;app.module.ts에 가드 전역 등록
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;providers: [
  {
    provide: APP_GUARD,
    useClass: AuthGuard, // 인증 먼저
  },
  {
    provide: APP_GUARD,
    useClass: RbacGuard, // 인가(RBAC)는 두 번째
  },
];
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;가드 순서 중요: 인증(토큰 확인) &amp;rarr; 인가(권한 확인)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;엔드포인트에 권한 적용&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: 일반 유저는 영화 조회 가능, 관리자만 생성/수정/삭제 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Get()
@Public()
getMovies() {}

@Post()
@Rbac(Role.ADMIN)
createMovie() {}

@Patch(':id')
@Rbac(Role.ADMIN)
updateMovie() {}

@Delete(':id')
@Rbac(Role.ADMIN)
deleteMovie() {}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;학습 후기&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 RBAC 강의를 통해 NestJS에서 역할 기반 인가를 실제로 어떻게 구현할 수 있는지 깊이 있게 이해할 수 있었습니다. 인증만으로는 보안이 완성되지 않으며, 인가가 더해져야 비로소 시스템 전반의 접근 제어가 가능해진다는 점을 실습을 통해 체감했습니다. 특히 JWT에 role 값을 포함하고, 이 값을 기준으로 엔드포인트별 접근 권한을 제한하는 방식은 NestJS의 데코레이터 및 가드 시스템과 매우 잘 어울리는 설계라고 느꼈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RBAC 구현 시 가장 인상 깊었던 부분은 @Rbac()이라는 커스텀 데코레이터를 만들어 각 라우트 핸들러에 요구되는 역할을 명시하고, 이를 Reflector로 추출하여 가드 내부에서 req.user.role과 비교하는 구조였습니다. NestJS의 메타프로그래밍 개념이 실무와 직결되는 순간이었고, 역할이 없을 경우 인가를 건너뛰는 예외 처리 로직까지 반영하면서 유연하고 확장 가능한 인가 시스템을 직접 설계할 수 있다는 자신감을 얻었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 권한을 숫자 기반으로 정의한 점도 현실적이고 실용적인 선택이라고 느꼈습니다. 단순한 조건문 비교로 충분한 유연성을 확보할 수 있었고, 나중에 기획 변경이나 새로운 역할 추가 시에도 비교적 손쉽게 대응할 수 있을 것이라 판단됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전역 가드 등록 순서 또한 중요한 학습 포인트였습니다. 인증이 먼저 실행되고, 그 다음에 인가가 실행되는 순서로 구성하여 인증이 먼저 통과된 사용자만이 인가 로직을 거칠 수 있게 함으로써, 책임과 관심사의 분리가 잘 이루어진 구조였습니다. 이로 인해 가드 코드가 더 명확해지고, 유지보수 또한 쉬워진다는 장점이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, 퍼블릭 라우트에 잘못된 토큰이 전달될 경우 에러가 발생하지 않도록 Bearer Token 미들웨어를 개선한 방식 역시 실무적인 고민이 반영된 내용이었습니다. 토큰이 없거나 잘못되어도 미들웨어는 조용히 넘어가고, 인가 여부는 가드에서 판단하는 구조는 시스템의 견고함을 높이는 설계라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 강의를 통해 RBAC의 필요성과 구조, 그리고 NestJS에서의 구현 방법을 확실히 체득할 수 있었으며, 실제 프로젝트에 도입할 수 있을 만큼 구체적인 실습 경험을 얻을 수 있었습니다. 앞으로 역할 기반의 더 복잡한 권한 시스템(RBAC + ABAC 등)으로도 확장할 수 있다는 점에서 큰 가능성을 느꼈습니다.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;학습 인증샷&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;289&quot; data-origin-height=&quot;308&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQsosa/btsNJ1KZvla/3IDZ1FpLkB3kIAFX1FYPi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQsosa/btsNJ1KZvla/3IDZ1FpLkB3kIAFX1FYPi1/img.png&quot; data-alt=&quot;수강 인증 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQsosa/btsNJ1KZvla/3IDZ1FpLkB3kIAFX1FYPi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQsosa%2FbtsNJ1KZvla%2F3IDZ1FpLkB3kIAFX1FYPi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;289&quot; height=&quot;308&quot; data-origin-width=&quot;289&quot; data-origin-height=&quot;308&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;수강 인증 사진&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1223&quot; data-origin-height=&quot;545&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AdEDa/btsNLQ2ngOG/AqPFYCQcj1iw6kTQuGUpcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AdEDa/btsNLQ2ngOG/AqPFYCQcj1iw6kTQuGUpcK/img.png&quot; data-alt=&quot;학습 인증샷&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AdEDa/btsNLQ2ngOG/AqPFYCQcj1iw6kTQuGUpcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAdEDa%2FbtsNLQ2ngOG%2FAqPFYCQcj1iw6kTQuGUpcK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1223&quot; height=&quot;545&quot; data-origin-width=&quot;1223&quot; data-origin-height=&quot;545&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;학습 인증샷&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;162&quot; data-origin-height=&quot;87&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJvbG8/btsNLzsYnb8/VSTYK5KFy8zHRx9wYW4Yqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJvbG8/btsNLzsYnb8/VSTYK5KFy8zHRx9wYW4Yqk/img.png&quot; data-alt=&quot;공부 시작 시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJvbG8/btsNLzsYnb8/VSTYK5KFy8zHRx9wYW4Yqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJvbG8%2FbtsNLzsYnb8%2FVSTYK5KFy8zHRx9wYW4Yqk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;162&quot; height=&quot;87&quot; data-origin-width=&quot;162&quot; data-origin-height=&quot;87&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공부 시작 시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;166&quot; data-origin-height=&quot;89&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/stf66/btsNJFnYYvf/prkm59uGKbUP6Pxsjj5nuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/stf66/btsNJFnYYvf/prkm59uGKbUP6Pxsjj5nuk/img.png&quot; data-alt=&quot;공부 종료 시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/stf66/btsNJFnYYvf/prkm59uGKbUP6Pxsjj5nuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fstf66%2FbtsNJFnYYvf%2Fprkm59uGKbUP6Pxsjj5nuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;166&quot; height=&quot;89&quot; data-origin-width=&quot;166&quot; data-origin-height=&quot;89&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공부 종료 시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333;&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;a href=&quot;https://abit.ly/lisbva&quot;&gt;https://abit.ly/lisbva&lt;/a&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;</description>
      <category>NestJS</category>
      <category>습관형성</category>
      <category>오공완</category>
      <category>직장인자기계발</category>
      <category>패스트캠퍼스</category>
      <category>패스트캠퍼스후기</category>
      <category>환급챌린지</category>
      <author>NangIn</author>
      <guid isPermaLink="true">https://nang-in.tistory.com/157</guid>
      <comments>https://nang-in.tistory.com/157#entry157comment</comments>
      <pubDate>Sat, 3 May 2025 11:30:41 +0900</pubDate>
    </item>
    <item>
      <title>패스트캠퍼스 환급챌린지 32일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기</title>
      <link>https://nang-in.tistory.com/156</link>
      <description>&lt;p style=&quot;background-color: #e7f8ff; color: #000000; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;강의 내용 정리&amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Guard 이론&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;왜 가드를 배우는가?&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본기를 익힌 후 진도를 빠르게 나가기 위함.&lt;/li&gt;
&lt;li&gt;파트1에서 많은 내용을 소화해야 하므로 깊이 있고 빠르게 커버할 필요 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;NestJS의 주요 기능 분류 및 비교&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;기능명&lt;/td&gt;
&lt;td&gt;주 목적&lt;/td&gt;
&lt;td&gt;실행 시점&lt;/td&gt;
&lt;td&gt;사용 포인트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;미들웨어&lt;/td&gt;
&lt;td&gt;요청 전 비파괴적 작업&lt;/td&gt;
&lt;td&gt;가장 먼저 실행&lt;/td&gt;
&lt;td&gt;logging, 공통 처리 등&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;파이프&lt;/td&gt;
&lt;td&gt;유효성 검사 및 변환&lt;/td&gt;
&lt;td&gt;컨트롤러 진입 전&lt;/td&gt;
&lt;td&gt;DTO, transform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가드&lt;/td&gt;
&lt;td&gt;인가(authorization) 처리&lt;/td&gt;
&lt;td&gt;컨트롤러 진입 직전&lt;/td&gt;
&lt;td&gt;권한 체크&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인터셉터&lt;/td&gt;
&lt;td&gt;요청/응답 가로채기&lt;/td&gt;
&lt;td&gt;컨트롤러 전/후&lt;/td&gt;
&lt;td&gt;응답 변경, 로깅 등&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기능은 유사한 일을 할 수 있지만, 각 기능의 문맥(Context)에 맞게 사용하는 것이 협업과 유지보수에 유리함.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;가드(Guard)의 정의 및 역할&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;가드는 요청을 보호하는 역할&lt;/b&gt;을 한다.&lt;/li&gt;
&lt;li&gt;요청을 라우트 핸들러에 넘기기 전, &lt;b&gt;요청자가 자격이 있는지(인가) 판단&lt;/b&gt;.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이를 Authorization이라고 함 (인증: Authentication과 구분)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;요청을 통과시키거나 차단할 수 있음 (true/false 반환)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;미들웨어 vs 가드&lt;/b&gt; &amp;nbsp;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;항목&lt;/td&gt;
&lt;td&gt;미들웨어&lt;/td&gt;
&lt;td&gt;가드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실행 위치&lt;/td&gt;
&lt;td&gt;가장 앞단&lt;/td&gt;
&lt;td&gt;핸들러 직전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용 가능 정보&lt;/td&gt;
&lt;td&gt;request, response만 있음&lt;/td&gt;
&lt;td&gt;executionContext 제공 (컨텍스트 풍부)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;적절한 용도&lt;/td&gt;
&lt;td&gt;로깅, 인증 토큰 파싱 등&lt;/td&gt;
&lt;td&gt;권한 검증, 접근 제어 등&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;가드 구현 방법&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;implements CanActivate 인터페이스 구현&lt;/li&gt;
&lt;li&gt;canActivate(context: ExecutionContext): boolean 메서드 정의&lt;/li&gt;
&lt;li&gt;반환 값
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;true: 통과 &amp;rarr; 요청 핸들러 실행&lt;/li&gt;
&lt;li&gt;false: 차단 &amp;rarr; ForbiddenException (HTTP 403)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;가드 적용 방식&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;로컬 적용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;클래스 전체에 적용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@UseGuards(RolesGuard)
@Controller('cats')
export class CatsController {}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특정 메서드에만 적용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@UseGuards(RolesGuard)
@Get()
findAll() {}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;글로벌 적용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;간단한 글로벌 적용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DI(의존성 주입)가 필요한 경우&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;providers: [
  {
    provide: APP_GUARD,
    useClass: RolesGuard,
  },
]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Auth Guard 생성하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;목표&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증되지 않은 사용자가 특정 라우트(예: 영화 생성, 수정, 삭제 등)에 접근하지 못하도록 &lt;b&gt;가드(Guard)&lt;/b&gt; 를 구현한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;기본 설계 및 구조&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;src/us/guards/authentication.guard.ts 생성&lt;/li&gt;
&lt;li&gt;인증용 가드를 간단하게 구현&lt;/li&gt;
&lt;li&gt;이미 &lt;b&gt;미들웨어에서 JWT 검증&lt;/b&gt;이 완료되었고, 그 결과가 request.user에 담겨 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Guard 구현 단계&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;클래스 정의 및 데코레이터 추가&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;@Injectable()
export class UsGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean { ... }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ExecutionContext로부터 Request 추출&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;const request = context.switchToHttp().getRequest();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검증 조건&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;if (!request.user || request.user.tokenType !== 'access') {
  return false; // &amp;rarr; 403 Forbidden
}
return true; // &amp;rarr; 라우트 통과
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;로컬 적용&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 라우트(예: 영화 생성)에만 적용하고 싶은 경우:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@UseGuards(UsGuard)
@Post()
createMovie() {}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;NestJS 내장 PassportGuard가 아닌, &lt;b&gt;직접 만든 Guard를 import&lt;/b&gt; 해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;기능 테스트 (Postman)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;토큰 없이 POST 요청 시 &amp;rarr; 403 Forbidden&lt;/li&gt;
&lt;li&gt;올바른 Access Token 포함 시 &amp;rarr; 정상적으로 요청 통과&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;글로벌 적용 방법&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;반복 적용 피하기 위해 &lt;b&gt;AppModule에 글로벌 가드 등록&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;providers: [
  {
    provide: APP_GUARD,
    useClass: UsGuard,
  }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이렇게 하면 모든 라우트가 기본적으로 &lt;b&gt;private route&lt;/b&gt;가 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;글로벌 가드 적용 후 주의사항&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그인(auth/login), 회원가입(auth/register) 등 &lt;b&gt;퍼블릭 라우트도 막히게 됨&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;의도치 않게 외부 공개되어야 할 API까지 막히는 문제가 발생할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Public Custom Decorator 생성하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제 상황&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AuthGuard를 &lt;b&gt;전역(Global Guard)&lt;/b&gt; 으로 적용하면서, 로그인(auth/login)이나 회원가입(auth/register) 등 &lt;b&gt;퍼블릭 라우트까지 차단&lt;/b&gt;되는 문제가 발생함.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;목표&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로 모든 라우트를 &lt;b&gt;프라이빗(private)&lt;/b&gt; 으로 설정하고,&lt;/li&gt;
&lt;li&gt;특정 라우트만 &lt;b&gt;퍼블릭(public)&lt;/b&gt; 으로 &lt;b&gt;제외 처리&lt;/b&gt;할 수 있도록 &lt;b&gt;커스텀 데코레이터 + 가드 개선&lt;/b&gt;을 통해 해결.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;커스텀 데코레이터 만들기&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;us/decorators/public.decorator.ts 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () =&amp;gt; SetMetadata(IS_PUBLIC_KEY, true);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;SetMetadata()를 통해 'isPublic'이라는 메타데이터를 true로 설정&lt;/li&gt;
&lt;li&gt;@Public() 데코레이터를 통해 퍼블릭 라우트를 마킹할 수 있게 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;라우트에 데코레이터 적용&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Public()
@Post('login')
login() { ... }

@Public()
@Post('register')
register() { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 데코레이터를 붙이면, &lt;b&gt;가드 통과&lt;/b&gt;를 자동으로 허용할 예정&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;가드 수정 (us/guards/auth.guard.ts)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Reflector 주입
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;constructor(private reflector: Reflector) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;퍼블릭 여부 확인 후 처리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;const isPublic = this.reflector.get&amp;lt;boolean&amp;gt;(IS_PUBLIC_KEY, context.getHandler());
if (isPublic) {
  return true; // 퍼블릭이면 모든 로직 바이패스
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;context.getHandler()를 통해 현재 라우트 핸들러의 메타데이터 접근&lt;/li&gt;
&lt;li&gt;isPublic === true이면 인증 검사를 하지 않고 곧바로 통과시킴&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Bearer Token Middleware 토큰 검증 개선하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제 상황: 퍼블릭 라우트에서 &quot;잘못된 토큰&quot;이 들어올 경우&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Public() 데코레이터가 붙은 퍼블릭 라우트에 요청 시:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;토큰이 아예 없는 경우&lt;/b&gt; &amp;rarr; 정상 동작&lt;/li&gt;
&lt;li&gt;&lt;b&gt;잘못된 토큰이 들어온 경우&lt;/b&gt; &amp;rarr; 403 Forbidden 에러 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;왜 문제가 되나?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;퍼블릭 라우트는 &lt;b&gt;누구나 접근 가능해야&lt;/b&gt; 함&lt;/li&gt;
&lt;li&gt;&quot;잘못된 토큰&quot;이 있다고 해서 &lt;b&gt;에러가 발생하면 안 됨&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;프론트엔드는 보통 &lt;b&gt;모든 요청에 액세스 토큰을 자동으로 포함&lt;/b&gt;하므로,&lt;/li&gt;
&lt;li&gt;퍼블릭 라우트에도 토큰이 포함되는 경우가 많음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;원인 분석&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 에러는 &lt;b&gt;미들웨어&lt;/b&gt;(BearerMiddleware)에서 토큰 검증 실패 시 에러를 던지기 때문&lt;/li&gt;
&lt;li&gt;그런데 이미 &lt;b&gt;가드(AuthGuard)&lt;/b&gt; 에서 인증 여부 판단을 하고 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해결 전략: 토큰 검증 실패 시 &quot;조용히 무시&quot;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 로직 (문제 있음)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;try {
  const payload = verify(token, secret);
  req.user = payload;
} catch (e) {
  throw new UnauthorizedException();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&amp;rarr; &lt;b&gt;토큰이 잘못되면 에러 던짐 &amp;rarr; 퍼블릭 라우트도 실패&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;변경 후 로직
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;monkey&quot;&gt;&lt;code&gt;try {
  const payload = verify(token, secret);
  req.user = payload;
} catch (e) {
  // 에러 무시하고 next() 호출
}
next();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;검증에 실패해도 &lt;b&gt;req.user = undefined&lt;/b&gt;인 채로 다음 단계로 넘어감&lt;/li&gt;
&lt;li&gt;이후 인증 여부는 &lt;b&gt;AuthGuard 내부에서 판단&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;학습 후기&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의를 통해 NestJS에서 가드가 어떤 위치에서 어떤 역할을 수행하는지 명확히 이해할 수 있었습니다. 처음에는 미들웨어와 가드가 유사하게 느껴졌지만, 미들웨어는 요청을 변형하거나 로그를 출력하는 등 비파괴적인 전처리에 가깝고, 가드는 컨트롤러 직전에서 접근을 허용할지 말지를 결정하는 보안 필터라는 점에서 확실히 역할이 구분된다는 점이 흥미로웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 AuthGuard를 구현하면서, ExecutionContext에서 request 객체를 가져와 req.user를 기반으로 인증 여부를 판단하는 구조는 NestJS의 IoC 철학에 잘 부합한다고 느꼈습니다. 특히 기존에 Bearer Token을 미들웨어에서 파싱하여 req.user에 주입하는 구조와 연결되어, 미들웨어와 가드가 각자 역할을 나누고 있지만 유기적으로 협력하는 구조가 인상 깊었습니다. 인증은 가드가 담당하고, 미들웨어는 정보를 제공하는 조력자로만 남겨두는 구조가 가장 깔끔하고 확장 가능한 아키텍처라고 느꼈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 글로벌 가드 설정을 통해 모든 라우트를 기본적으로 보호하면서, @Public() 데코레이터를 통해 퍼블릭 라우트를 명시적으로 열어두는 방식은 실무에서 자주 겪는 인증/인가 요구사항을 매우 우아하게 해결해준다고 생각합니다. 특히 Reflector를 이용해 메타데이터 기반으로 가드의 동작을 제어하는 방식은, 이후 역할 기반 권한 시스템이나 세분화된 인증 정책으로 확장할 때에도 매우 유용하게 작동할 것이라 판단됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 실습 중 가장 인상 깊었던 부분은, 퍼블릭 라우트에 잘못된 토큰이 포함된 경우에도 요청이 거부되는 문제를 해결한 방식이었습니다. 처음엔 이 현상이 이상하다고 느껴졌지만, 문제는 가드가 아닌 Bearer 토큰 미들웨어에서 에러를 던진 것이 원인이었습니다. 이 부분을 try-catch로 조용히 무시하고 넘어가며, 인증 여부의 최종 판단은 가드에서 하도록 설계한 점은 책임의 분리를 잘 보여주는 사례였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 이번 강의를 통해 단순히 Guard의 사용법만 익힌 것이 아니라, NestJS 아키텍처의 철학과 역할 분리에 대한 깊은 이해를 얻게 되었습니다. 인증, 인가, 역할 권한 등의 보안 관련 요구사항을 구조적으로 어떻게 풀어야 할지에 대한 감을 잡을 수 있었고, 실제 팀 프로젝트나 실무에서도 매우 유용하게 활용할 수 있을 것이라 확신하게 되었습니다.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;학습 인증샷&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;290&quot; data-origin-height=&quot;438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVr0d8/btsNLtM7dpo/ZxRk9xVFyKWK7bnFmvf1i0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVr0d8/btsNLtM7dpo/ZxRk9xVFyKWK7bnFmvf1i0/img.png&quot; data-alt=&quot;수강 인증 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVr0d8/btsNLtM7dpo/ZxRk9xVFyKWK7bnFmvf1i0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVr0d8%2FbtsNLtM7dpo%2FZxRk9xVFyKWK7bnFmvf1i0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;290&quot; height=&quot;438&quot; data-origin-width=&quot;290&quot; data-origin-height=&quot;438&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;수강 인증 사진&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;499&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QFj7t/btsNJ4ug8Nm/9KIkDziwmhypgTQAZazzuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QFj7t/btsNJ4ug8Nm/9KIkDziwmhypgTQAZazzuk/img.png&quot; data-alt=&quot;학습 인증샷&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QFj7t/btsNJ4ug8Nm/9KIkDziwmhypgTQAZazzuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQFj7t%2FbtsNJ4ug8Nm%2F9KIkDziwmhypgTQAZazzuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1196&quot; height=&quot;499&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;499&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;학습 인증샷&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;188&quot; data-origin-height=&quot;94&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k2Sho/btsNMaGj6rh/ej5AjxRKl0t9534a6f1jhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k2Sho/btsNMaGj6rh/ej5AjxRKl0t9534a6f1jhk/img.png&quot; data-alt=&quot;공부 시작 시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k2Sho/btsNMaGj6rh/ej5AjxRKl0t9534a6f1jhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk2Sho%2FbtsNMaGj6rh%2Fej5AjxRKl0t9534a6f1jhk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;188&quot; height=&quot;94&quot; data-origin-width=&quot;188&quot; data-origin-height=&quot;94&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공부 시작 시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;146&quot; data-origin-height=&quot;79&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cv6NBd/btsNJ2DbVW9/lQ57RySrqAwYFP1rKHz8UK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cv6NBd/btsNJ2DbVW9/lQ57RySrqAwYFP1rKHz8UK/img.png&quot; data-alt=&quot;공부 종료 시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cv6NBd/btsNJ2DbVW9/lQ57RySrqAwYFP1rKHz8UK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcv6NBd%2FbtsNJ2DbVW9%2FlQ57RySrqAwYFP1rKHz8UK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;146&quot; height=&quot;79&quot; data-origin-width=&quot;146&quot; data-origin-height=&quot;79&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공부 종료 시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333;&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;a href=&quot;https://abit.ly/lisbva&quot;&gt;https://abit.ly/lisbva&lt;/a&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;
&lt;script&gt;
document.querySelector(&quot;.info_post&quot;).innerHTML = `&lt;span class=&quot;info_post&quot;&gt;NangIn
        &lt;span class=&quot;txt_bar&quot;&gt;&lt;/span&gt;2025. 5. 2. 10:07
    &lt;span class=&quot;my_edit&quot; aria-expanded=&quot;false&quot; aria-haspopup=&quot;true&quot;&gt;
        &lt;button class=&quot;btn_etc&quot;&gt;
            &lt;svg width=&quot;20px&quot; height=&quot;20px&quot; viewBox=&quot;0 0 21 21&quot; version=&quot;1.1&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; class=&quot;ico_more&quot;&gt;
                &lt;title&gt;더보기&lt;/title&gt;
                &lt;g id=&quot;s07_magazine&quot; transform=&quot;translate(-467.000000, -226.000000)&quot;&gt;
                    &lt;g id=&quot;more-copy&quot; transform=&quot;translate(467.400000, 226.400000)&quot;&gt;
                        &lt;path d=&quot;M10.1,0 C15.678076,0 20.2,4.52192403 20.2,10.1 C20.2,15.678076 15.678076,20.2 10.1,20.2 C4.52192403,20.2 0,15.678076 0,10.1 C0,4.52192403 4.52192403,0 10.1,0 Z M10.1,1.2 C5.18466573,1.2 1.2,5.18466573 1.2,10.1 C1.2,15.0153343 5.18466573,19 10.1,19 C15.0153343,19 19,15.0153343 19,10.1 C19,5.18466573 15.0153343,1.2 10.1,1.2 Z M10.1,13.3 C10.7075132,13.3 11.2,13.7924868 11.2,14.4 C11.2,15.0075132 10.7075132,15.5 10.1,15.5 C9.49248678,15.5 9,15.0075132 9,14.4 C9,13.7924868 9.49248678,13.3 10.1,13.3 Z M10.1,9 C10.7075132,9 11.2,9.49248678 11.2,10.1 C11.2,10.7075132 10.7075132,11.2 10.1,11.2 C9.49248678,11.2 9,10.7075132 9,10.1 C9,9.49248678 9.49248678,9 10.1,9 Z M10.1,4.7 C10.7075132,4.7 11.2,5.19248678 11.2,5.8 C11.2,6.40751322 10.7075132,6.9 10.1,6.9 C9.49248678,6.9 9,6.40751322 9,5.8 C9,5.19248678 9.49248678,4.7 10.1,4.7 Z&quot; id=&quot;Combined-Shape&quot;&gt;&lt;/path&gt;
                    &lt;/g&gt;
                &lt;/g&gt;
            &lt;/svg&gt;
        &lt;/button&gt;
        &lt;span class=&quot;layer_edit&quot; role=&quot;menu&quot;&gt;
            &lt;a href=&quot;https://nang-in.tistory.com/manage/post/155?returnURL=ENTRY&quot; role=&quot;menuitem&quot; class=&quot;btn_g&quot; data-tiara-action-name=&quot;상단 옵션 버튼_클릭&quot; data-tiara-layer=&quot;Edit&quot;&gt;수정&lt;/a&gt;
             &lt;a class=&quot;btn_g&quot; data-tiara-action-name=&quot;상단 옵션 버튼_클릭&quot; data-tiara-layer=&quot;Setting&quot; href=&quot;#&quot; role=&quot;menuitem&quot; onclick=&quot;changeVisibility(155, 20); return false;&quot;&gt;공개로 변경&lt;/a&gt;
            &lt;a href=&quot;#&quot; onclick=&quot;deleteEntry(155); return false;&quot; role=&quot;menuitem&quot; class=&quot;btn_g&quot; data-tiara-action-name=&quot;상단 옵션 버튼_클릭&quot; data-tiara-layer=&quot;Del&quot;&gt;삭제&lt;/a&gt;
            &lt;span class=&quot;ico_postbtn ico_arrb&quot;&gt;&lt;/span&gt;
        &lt;/span&gt;
    &lt;/span&gt;

      &lt;/span&gt;`;
&lt;/script&gt;
&lt;/p&gt;</description>
      <category>NestJS</category>
      <category>습관형성</category>
      <category>오공완</category>
      <category>직장인자기계발</category>
      <category>패스트캠퍼스</category>
      <category>패스트캠퍼스후기</category>
      <category>환급챌린지</category>
      <author>NangIn</author>
      <guid isPermaLink="true">https://nang-in.tistory.com/156</guid>
      <comments>https://nang-in.tistory.com/156#entry156comment</comments>
      <pubDate>Sat, 3 May 2025 10:07:30 +0900</pubDate>
    </item>
    <item>
      <title>패스트캠퍼스 환급챌린지 31일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기</title>
      <link>https://nang-in.tistory.com/155</link>
      <description>&lt;p style=&quot;background-color: #e7f8ff; color: #000000; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;강의 내용 정리&amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Middleware 이론&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;NestJS 요청 처리 순서&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;미들웨어 &amp;rarr; 가드 &amp;rarr; 인터셉터 &amp;rarr; 파이프 &amp;rarr; 컨트롤러(핸들러) &amp;rarr; 응답 &amp;rarr; 인터셉터 &amp;rarr; 예외 필터&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;미들웨어란?&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;라우트 핸들러&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;실행 전에 동작&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Request, Response, Next 객체에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;접근 및 조작 가능&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;NestJS 공식 문서에도 다음과 같은 역할로 정의됨:&lt;/li&gt;
&lt;li&gt;&lt;b&gt;미들웨어의 주요 역할&lt;/b&gt;기능 설명
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;자유로운 로직 실행&lt;/td&gt;
&lt;td&gt;로그 출력, 요청 전처리 등&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;req/res 객체 변경&lt;/td&gt;
&lt;td&gt;예: req.user 값 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;요청-응답 사이클 중단&lt;/td&gt;
&lt;td&gt;인증 실패 시 직접 응답 종료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;다음 단계로 전달&lt;/td&gt;
&lt;td&gt;next() 호출 시 다음 미들웨어 혹은 컨트롤러로 진행&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;미들웨어 만들기&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;monkey&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log(`${req.method} ${req.url}`);
    next();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NestMiddleware를 구현하고 use() 메서드 필수 정의&lt;/li&gt;
&lt;li&gt;next() 호출 전까지 원하는 로직 실행 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;미들웨어 적용 방법&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;모듈 단위 적용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AppModule 또는 특정 모듈에서 적용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;dart&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('*'); // 전체 라우트 적용
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;apply() 안에 여러 미들웨어 넣을 수도 있음&lt;/li&gt;
&lt;li&gt;forRoutes()에는 다음과 같은 방식 사용 가능:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;.forRoutes('cats')                      // 특정 경로 전체
.forRoutes({ path: 'cats', method: RequestMethod.GET })  // 특정 메서드만
.forRoutes('*')                         // 전체 라우트

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;제외 라우트 설정&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;consumer
  .apply(LoggerMiddleware)
  .exclude({ path: 'cats', method: RequestMethod.POST }) // 제외 조건
  .forRoutes('*');
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;exclude()를 먼저 선언하고 forRoutes()로 나머지 적용&lt;/li&gt;
&lt;li&gt;너무 복잡하게 exclude 로직을 설계하지 않도록 주의&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;글로벌 미들웨어로 등록&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;main.ts에서 전체 앱에 전역 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;php&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;app.use(LoggerMiddleware);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;미들웨어 다중 적용&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;consumer
  .apply(LoggerMiddleware, AuthMiddleware, CacheMiddleware)
  .forRoutes('*');
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;미들웨어는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;등록 순서대로 실행&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;순서 중요할 경우 주의 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Bearer Token Middleware 구현하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;목표&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;매번 컨트롤러/가드에서 Bearer 토큰을 파싱하지 않고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;전역 미들웨어로 처리&lt;/b&gt;하여 중복 제거 및 편의성 향상&lt;/li&gt;
&lt;li&gt;Request 객체에 payload를 삽입해 이후 로직에서 req.user로 간편하게 접근&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제 인식&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;parseBearerToken()을 매번 불러서 토큰을 파싱하는 게 번거로움&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해결 방안:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;미들웨어를 통해 자동으로 처리되도록 만들자&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;미들웨어 구조 설계 및 생성&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;middleware/bearer-token.middleware.ts 생성&lt;/li&gt;
&lt;li&gt;NestMiddleware 인터페이스 구현 및 use() 함수 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;groovy&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;export class BearerTokenMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    // ...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;토큰 파싱 및 예외 처리 로직&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;토큰 유무 확인
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Authorization 헤더가 없으면 &amp;rarr; 인증 의도 없음 &amp;rarr; next()로 패스&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Bearer 토큰 형식 확인 및 파싱
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;형식이 &quot;Bearer &quot;인지 확인&lt;/li&gt;
&lt;li&gt;아니면 BadRequestException 던짐&lt;/li&gt;
&lt;li&gt;올바르면 토큰 문자열만 추출&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;디코드 후 타입 확인 (access / refresh)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;jwtService.decode()로 타입 확인&lt;/li&gt;
&lt;li&gt;타입이 access나 refresh가 아니면 &amp;rarr; UnauthorizedException&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Secret 결정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;토큰 타입에 따라 accessTokenSecret 또는 refreshTokenSecret을 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;검증 및 payload 삽입&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;jwtService.verify()로 실제 검증 수행&lt;/li&gt;
&lt;li&gt;검증된 payload를 req.user에 삽입&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;의존성 주입&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JwtService, ConfigService를 생성자에서 주입&lt;/li&gt;
&lt;li&gt;ConfigModule은 글로벌 등록되어 따로 exports 불필요&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;AppModule에 미들웨어 등록&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;configure()에서 consumer.apply().forRoutes()를 통해 전역 등록&lt;/li&gt;
&lt;li&gt;단, 로그인/회원가입은 Basic Auth 사용 &amp;rarr; exclude()로 예외 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;consumer
  .apply(BearerTokenMiddleware)
  .exclude(
    { path: 'auth/login', method: RequestMethod.POST },
    { path: 'auth/register', method: RequestMethod.POST }
  )
  .forRoutes('*');
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;미들웨어 적용 후 검증&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨트롤러에서 @Req() req: Request를 통해 req.user에 접근&lt;/li&gt;
&lt;li&gt;토큰이 포함되지 않으면 undefined&lt;/li&gt;
&lt;li&gt;Bearer 토큰 포함 &amp;rarr; payload 정보가 정상 출력됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;학습 후기&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;0&quot; data-end=&quot;185&quot;&gt;이번 강의를 통해 NestJS의 미들웨어 구조와 실질적인 활용 방식에 대해 명확하게 이해할 수 있었습니다. 이전까지는 미들웨어를 단순히 로깅이나 요청 전처리 용도로만 생각했으나, 이번에 Bearer Token을 파싱하고 검증하는 과정을 미들웨어로 추출함으로써 인증 흐름 전반을 효율화할 수 있다는 사실을 직접 체감할 수 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;0&quot; data-end=&quot;185&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;187&quot; data-end=&quot;468&quot;&gt;가장 인상 깊었던 부분은 req.user에 payload를 삽입하는 방식이었습니다. 일반적으로는 가드나 컨트롤러 내부에서 JWT 검증 로직을 넣고 결과를 기반으로 인증 여부를 판단했지만, 이번 방식은 인증과 관련된 공통 로직을 미들웨어로 분리하여 코드 중복을 제거하고 유지보수성을 높이는 데 효과적이었습니다. 특히 parseBearerToken 같은 유틸성 함수가 여기저기서 반복적으로 호출되는 상황에서, 해당 로직을 미들웨어로 대체한 구조는 실무적인 관점에서 매우 유용한 패턴이라고 느꼈습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;187&quot; data-end=&quot;468&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;470&quot; data-end=&quot;664&quot;&gt;또한 NestJS의 요청 처리 순서를 다시 확인하고, 미들웨어가 가드보다 앞서 실행된다는 점을 이해함으로써 인증 흐름 전체를 설계하는 데에 훨씬 더 명확한 기준을 갖게 되었습니다. 예외 처리와 next() 호출 타이밍, 그리고 Authorization 헤더가 없는 경우에도 유연하게 넘어가는 구조는 사용자 경험 측면에서도 중요하다고 생각합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;470&quot; data-end=&quot;664&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;666&quot; data-end=&quot;814&quot;&gt;특정 라우트를 exclude() 처리하여 미들웨어 적용 대상에서 제외하는 방법 역시 유연한 라우팅 구성을 가능하게 해주었고, 미들웨어의 실행 순서가 등록 순서에 따라 결정된다는 점은 로그, 캐시, 인증 등을 조합할 때 고려해야 할 중요한 포인트임을 실감했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;816&quot; data-end=&quot;989&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;816&quot; data-end=&quot;989&quot;&gt;결론적으로, 이번 강의를 통해 NestJS에서 미들웨어가 단순한 기능적 보조 수단을 넘어서 인증, 보안, 요청 흐름 제어 등 핵심 기능까지도 담당할 수 있는 유연한 구조임을 체감했습니다. 앞으로 프로젝트를 설계할 때는 이러한 구조적 이점을 적극 활용해, 깔끔하고 유지보수 가능한 인증 흐름을 설계하고자 합니다.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;학습 인증샷&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;291&quot; data-origin-height=&quot;327&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbJDVG/btsNLCb9f4a/qED70blftzVokCSpkOL4sk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbJDVG/btsNLCb9f4a/qED70blftzVokCSpkOL4sk/img.png&quot; data-alt=&quot;수강 인증 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbJDVG/btsNLCb9f4a/qED70blftzVokCSpkOL4sk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbJDVG%2FbtsNLCb9f4a%2FqED70blftzVokCSpkOL4sk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;291&quot; height=&quot;327&quot; data-origin-width=&quot;291&quot; data-origin-height=&quot;327&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;수강 인증 사진&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;531&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LAVb6/btsNLzsYj9K/2qHx1JKdWCvC9lq7UTePNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LAVb6/btsNLzsYj9K/2qHx1JKdWCvC9lq7UTePNk/img.png&quot; data-alt=&quot;학습 인증샷&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LAVb6/btsNLzsYj9K/2qHx1JKdWCvC9lq7UTePNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLAVb6%2FbtsNLzsYj9K%2F2qHx1JKdWCvC9lq7UTePNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1206&quot; height=&quot;531&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;531&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;학습 인증샷&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;158&quot; data-origin-height=&quot;92&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dO0czn/btsNJZ7vB1X/xNyjf322hdZ4s6457cim61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dO0czn/btsNJZ7vB1X/xNyjf322hdZ4s6457cim61/img.png&quot; data-alt=&quot;공부 시작 시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dO0czn/btsNJZ7vB1X/xNyjf322hdZ4s6457cim61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdO0czn%2FbtsNJZ7vB1X%2FxNyjf322hdZ4s6457cim61%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;158&quot; height=&quot;92&quot; data-origin-width=&quot;158&quot; data-origin-height=&quot;92&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공부 시작 시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;174&quot; data-origin-height=&quot;97&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ANzXn/btsNJ1YCJIN/jWvj17PpTPXI5eiQ7xajxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ANzXn/btsNJ1YCJIN/jWvj17PpTPXI5eiQ7xajxK/img.png&quot; data-alt=&quot;공부 종료 시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ANzXn/btsNJ1YCJIN/jWvj17PpTPXI5eiQ7xajxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FANzXn%2FbtsNJ1YCJIN%2FjWvj17PpTPXI5eiQ7xajxK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;174&quot; height=&quot;97&quot; data-origin-width=&quot;174&quot; data-origin-height=&quot;97&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공부 종료 시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333;&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;a href=&quot;https://abit.ly/lisbva&quot;&gt;https://abit.ly/lisbva&lt;/a&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;</description>
      <category>NestJS</category>
      <category>습관형성</category>
      <category>오공완</category>
      <category>직장인자기계발</category>
      <category>패스트캠퍼스</category>
      <category>패스트캠퍼스후기</category>
      <category>환급챌린지</category>
      <author>NangIn</author>
      <guid isPermaLink="true">https://nang-in.tistory.com/155</guid>
      <comments>https://nang-in.tistory.com/155#entry155comment</comments>
      <pubDate>Thu, 1 May 2025 22:41:34 +0900</pubDate>
    </item>
    <item>
      <title>패스트캠퍼스 환급챌린지 30일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기</title>
      <link>https://nang-in.tistory.com/154</link>
      <description>&lt;p style=&quot;background-color: #e7f8ff; color: #000000; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;강의 내용 정리&amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Access Token 재발급하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;목적&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Access Token&lt;/b&gt;은 유효기간이 짧음 (보안 목적)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Refresh Token&lt;/b&gt;을 통해 &lt;b&gt;Access Token 재발급&lt;/b&gt; 가능&lt;/li&gt;
&lt;li&gt;&amp;rarr; 로그인 없이 세션 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;엔드포인트 구성&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Post('token/access')
rotateAccessToken(@Headers('Authorization') token: string) {
  const payload = await this.authService.parseBearerToken(token, true);
  return {
    accessToken: await this.authService.issueToken(payload, false),
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Authorization: Bearer &amp;lt;refresh_token&amp;gt; 형식으로 요청&lt;/li&gt;
&lt;li&gt;refresh_token 유효성 검증 후, 새로운 access_token 발급&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;토큰 파싱 함수 분리&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;parseBearerToken()&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;async parseBearerToken(rawToken: string, isRefresh: boolean): Promise&amp;lt;{ id: number; role: Role; }&amp;gt; {
  const [type, token] = rawToken.split(' ');
  if (type.toLowerCase() !== 'bearer') throw new BadRequestException('포맷이 잘못됐습니다.');

  const payload = await this.jwtService.verifyAsync(token, {
    secret: this.configService.get(isRefresh ? 'REFRESH_TOKEN_SECRET' : 'ACCESS_TOKEN_SECRET'),
  });

  // 토큰 타입 체크
  if (isRefresh &amp;amp;&amp;amp; payload.type !== 'refresh') {
    throw new BadRequestException('Refresh Token을 입력해주세요.');
  }
  if (!isRefresh &amp;amp;&amp;amp; payload.type !== 'access') {
    throw new BadRequestException('Access Token을 입력해주세요.');
  }

  return { id: payload.sub, role: payload.role };
}
​&lt;/code&gt;&lt;/pre&gt;
&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Bearer 포맷 확인&lt;/li&gt;
&lt;li&gt;verifyAsync()로 서명/만료 검증&lt;/li&gt;
&lt;li&gt;&lt;b&gt;payload의 type이 'refresh'인지 확인 필수&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;토큰 발급 함수 개선&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;issueToken() 수정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;async issueToken(payload: { id: number; role: Role }, isRefresh: boolean): Promise&amp;lt;string&amp;gt; {
  return this.jwtService.signAsync(
    {
      sub: payload.id,
      role: payload.role,
      type: isRefresh ? 'refresh' : 'access',
    },
    {
      secret: this.configService.get(
        isRefresh ? 'REFRESH_TOKEN_SECRET' : 'ACCESS_TOKEN_SECRET',
      ),
      expiresIn: isRefresh ? '24h' : 300,
    },
  );
}
​&lt;/code&gt;&lt;/pre&gt;
&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;payload.sub, payload.role, type 명시&lt;/li&gt;
&lt;li&gt;expiresIn은 refresh &amp;rarr; 24h, access &amp;rarr; 5분(300초)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;환경변수 정비&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;왜 환경 변수를 상수로 분리해야 하는가?&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;.env의 키들을 &lt;b&gt;하드코딩된 문자열로 직접 반복 사용&lt;/b&gt;하고 있었음&lt;/li&gt;
&lt;li&gt;이는 오타 발생 시 &lt;b&gt;런타임 에러&lt;/b&gt;로 이어질 수 있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;변수화&lt;/b&gt;해 관리하면 오타를 줄이고 유지보수가 쉬움&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;공통 상수 관리 폴더 및 파일 생성&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구조:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;src/common/const/env.const.ts&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;코드:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;export const EnvVariableKeys = {
  DB_TYPE: 'DB_TYPE',
  DB_HOST: 'DB_HOST',
  DB_PORT: 'DB_PORT',
  DB_USERNAME: 'DB_USERNAME',
  DB_PASSWORD: 'DB_PASSWORD',
  DB_DATABASE: 'DB_DATABASE',
  HASH_ROUNDS: 'HASH_ROUNDS',
  ACCESS_TOKEN_SECRET: 'ACCESS_TOKEN_SECRET',
  REFRESH_TOKEN_SECRET: 'REFRESH_TOKEN_SECRET',
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;사용처 리팩토링&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;app.module.ts
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;ConfigModule.forRoot({
  validationSchema: Joi.object({
    [EnvVariableKeys.DB_TYPE]: Joi.string().required(),
    [EnvVariableKeys.DB_HOST]: Joi.string().required(),
    ...
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;auth.service.ts
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;this.configService.get(EnvVariableKeys.ACCESS_TOKEN_SECRET)
this.configService.get(EnvVariableKeys.REFRESH_TOKEN_SECRET)
this.configService.get(EnvVariableKeys.HASH_ROUNDS)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;정상 작동 여부 테스트&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 API (/auth/login, /auth/token/access) 테스트 &amp;rarr; &lt;b&gt;정상 작동&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;실수로 access token을 refresh token 엔드포인트에 넣으면 &amp;rarr; &lt;b&gt;에러 응답&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;refresh token 만료 시 에러 처리 개선&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 문제:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만료된 refresh token 입력 시, 내부 에러만 발생 &amp;rarr; 사용자에게 &lt;b&gt;불명확한 응답&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;개선 방법:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;try {
  const payload = await this.parseBearerToken(token, true);
  ...
} catch (err) {
  throw new UnauthorizedException('토큰이 만료됐습니다.');
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;try-catch로 감싸고, 명시적인 메시지를 가진 예외를 던짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;학습 후기&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;341&quot; data-start=&quot;99&quot; data-ke-size=&quot;size16&quot;&gt;이번 강의를 통해 Access Token과 Refresh Token을 함께 사용하는 인증 구조의 실용성을 체감할 수 있었습니다. 기존에는 토큰이 만료되면 무조건 로그인을 다시 해야 한다고 막연히 생각했지만, Refresh Token을 통해 Access Token만 교체해 주는 구조는 실제 서비스에서 매우 합리적인 방식이라는 점을 알게 되었습니다. 사용자 경험을 해치지 않으면서도 보안을 유지할 수 있다는 점이 특히 인상 깊었습니다.&lt;/p&gt;
&lt;p data-end=&quot;341&quot; data-start=&quot;99&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;548&quot; data-start=&quot;343&quot; data-ke-size=&quot;size16&quot;&gt;특히 parseBearerToken 메서드로 Access와 Refresh 토큰을 구분하고, issueToken 메서드로 두 토큰을 모두 발급할 수 있게 설계한 구조는 단순하면서도 확장성이 뛰어나다고 느꼈습니다. 이 과정에서 토큰 타입을 payload.type으로 구분하는 아이디어는 향후 로그아웃이나 토큰 블랙리스트 기능을 구현할 때도 유용할 것 같습니다.&lt;/p&gt;
&lt;p data-end=&quot;548&quot; data-start=&quot;343&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;750&quot; data-start=&quot;550&quot; data-ke-size=&quot;size16&quot;&gt;추가로 .env 환경 변수 키를 상수화하여 관리하는 방식은 실무에서 반드시 필요한 설계 패턴이라는 인사이트를 얻었습니다. 문자열을 반복해서 사용하는 대신 상수 객체로 관리하니 오타를 방지할 수 있었고, ConfigService를 사용하는 모든 부분에서 일관된 접근 방식이 가능해졌습니다. 이는 코드의 안정성과 유지보수성을 크게 높여주는 작업이었습니다.&lt;/p&gt;
&lt;p data-end=&quot;750&quot; data-start=&quot;550&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;959&quot; data-start=&quot;752&quot; data-ke-size=&quot;size16&quot;&gt;또한, Refresh Token이 만료되었을 때 내부 에러 메시지로 끝나는 것이 아닌, UnauthorizedException을 통해 명확한 응답 메시지를 클라이언트에 전달하는 처리 방식은 사용자 친화적인 API 설계의 좋은 예시라고 느꼈습니다. 단순히 작동하는 코드보다, 클라이언트와의 소통을 고려한 예외 메시지 설계가 얼마나 중요한지를 다시금 느낄 수 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;959&quot; data-start=&quot;752&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1186&quot; data-start=&quot;961&quot; data-ke-size=&quot;size16&quot;&gt;이번 학습을 통해 인증 구조를 보다 체계적이고 견고하게 구성하는 방법을 익혔으며, 무엇보다도 실무 환경에서 반복적으로 마주치게 될 인증 문제들을 사전에 대비할 수 있는 탄탄한 설계 철학을 배울 수 있었습니다. Access Token 재발급 기능은 단순한 기능 이상으로 사용자 경험, 보안, 유지보수 측면에서 모두 중요한 역할을 한다는 점에서 앞으로도 적극적으로 활용하고 싶은 기술 중 하나가 되었습니다.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;학습 인증샷&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;298&quot; data-origin-height=&quot;158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bY8LNZ/btsNL2hmMbL/ygjs8SybxUWzaGviwMBOyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bY8LNZ/btsNL2hmMbL/ygjs8SybxUWzaGviwMBOyK/img.png&quot; data-alt=&quot;수강 인증 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bY8LNZ/btsNL2hmMbL/ygjs8SybxUWzaGviwMBOyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbY8LNZ%2FbtsNL2hmMbL%2Fygjs8SybxUWzaGviwMBOyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;298&quot; height=&quot;158&quot; data-origin-width=&quot;298&quot; data-origin-height=&quot;158&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;수강 인증 사진&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1209&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcz9Hj/btsNLpxkW0x/oqkpHhn1SjwjiDw8Lp6Dsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcz9Hj/btsNLpxkW0x/oqkpHhn1SjwjiDw8Lp6Dsk/img.png&quot; data-alt=&quot;학습 인증샷&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcz9Hj/btsNLpxkW0x/oqkpHhn1SjwjiDw8Lp6Dsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbcz9Hj%2FbtsNLpxkW0x%2FoqkpHhn1SjwjiDw8Lp6Dsk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1209&quot; height=&quot;592&quot; data-origin-width=&quot;1209&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;학습 인증샷&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;136&quot; data-origin-height=&quot;85&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dX14lX/btsNKa8U4YI/A63kCFJQKMkC6ISKpfTOHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dX14lX/btsNKa8U4YI/A63kCFJQKMkC6ISKpfTOHK/img.png&quot; data-alt=&quot;공부 시작 시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dX14lX/btsNKa8U4YI/A63kCFJQKMkC6ISKpfTOHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdX14lX%2FbtsNKa8U4YI%2FA63kCFJQKMkC6ISKpfTOHK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;136&quot; height=&quot;85&quot; data-origin-width=&quot;136&quot; data-origin-height=&quot;85&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공부 시작 시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;171&quot; data-origin-height=&quot;91&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MXpKR/btsNJVquJP5/Md9AoEbCJwOU53GpdSvBH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MXpKR/btsNJVquJP5/Md9AoEbCJwOU53GpdSvBH0/img.png&quot; data-alt=&quot;공부 종료 시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MXpKR/btsNJVquJP5/Md9AoEbCJwOU53GpdSvBH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMXpKR%2FbtsNJVquJP5%2FMd9AoEbCJwOU53GpdSvBH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;171&quot; height=&quot;91&quot; data-origin-width=&quot;171&quot; data-origin-height=&quot;91&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공부 종료 시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333;&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;a href=&quot;https://abit.ly/lisbva&quot;&gt;https://abit.ly/lisbva&lt;/a&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;</description>
      <category>NestJS</category>
      <category>습관형성</category>
      <category>오공완</category>
      <category>직장인자기계발</category>
      <category>패스트캠퍼스</category>
      <category>패스트캠퍼스후기</category>
      <category>환급챌린지</category>
      <author>NangIn</author>
      <guid isPermaLink="true">https://nang-in.tistory.com/154</guid>
      <comments>https://nang-in.tistory.com/154#entry154comment</comments>
      <pubDate>Wed, 30 Apr 2025 22:50:27 +0900</pubDate>
    </item>
    <item>
      <title>패스트캠퍼스 환급챌린지 29일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기</title>
      <link>https://nang-in.tistory.com/153</link>
      <description>&lt;p style=&quot;background-color: #e7f8ff; color: #000000; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;강의 내용 정리&amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Jwt Strategy 적용하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;관련 패키지 설치&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;pnpm i @nestjs/jwt @nestjs/passport passport passport-jwt
pnpm i -D @types/passport-jwt
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@nestjs/jwt: NestJS에서 JWT 사용을 위한 모듈&lt;/li&gt;
&lt;li&gt;passport-jwt: JWT 기반 Passport 전략&lt;/li&gt;
&lt;li&gt;@types/passport-jwt: 타입 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;JWT 발급 로직 분리&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;issueToken() 함수 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기존 로그인 로직에서 토큰 발급 코드 분리&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;함수 시그니처:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;async issueToken(user: User, isRefreshToken: boolean): Promise&amp;lt;string&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;내부 로직:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;type: access 또는 refresh&lt;/li&gt;
&lt;li&gt;secret: .env에서 설정한 값&lt;/li&gt;
&lt;li&gt;expiresIn: access &amp;rarr; 5분(300초), refresh &amp;rarr; 24시간&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;로그인 컨트롤러 정리&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Post('login-passport')
@UseGuards(LocalAuthGuard)
async loginPassport(@Request() req) {
  return {
    accessToken: await this.authService.issueToken(req.user, false),
    refreshToken: await this.authService.issueToken(req.user, true),
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LocalAuthGuard로 사용자 검증&lt;/li&gt;
&lt;li&gt;검증 성공 시 request.user에서 사용자 정보 추출&lt;/li&gt;
&lt;li&gt;JWT 2종 발급 후 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;JWT Strategy 생성&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;jwt.strategy.ts
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get('ACCESS_TOKEN_SECRET'),
    });
  }

  async validate(payload: any) {
    return payload; // request.user로 들어감
  }
}
​&lt;/code&gt;&lt;/pre&gt;
&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ExtractJwt.fromAuthHeaderAsBearerToken() 사용&lt;/li&gt;
&lt;li&gt;secretOrKey: access 토큰 시크릿 사용&lt;/li&gt;
&lt;li&gt;validate()의 반환값이 request.user에 들어감&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;JWT Auth Guard 생성&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;export class JwtAuthGuard extends AuthGuard('jwt') {}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이름 기반 guard 사용 방지 &amp;rarr; 안전한 타입 기반 코드 작성 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;보호된 라우트 생성 및 토큰 검증&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Get('private')
@UseGuards(JwtAuthGuard)
getPrivateData(@Request() req) {
  console.log('run'); // guard 통과 시 실행
  return req.user;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;JWT 토큰 필요&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실패 시&lt;/b&gt; &amp;rarr; Unauthorized 에러 응답&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성공 시&lt;/b&gt; &amp;rarr; validate의 반환값이 request.user에 담김&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;학습 후기&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestJS에서 JWT 기반 인증을 구축하면서 가장 인상 깊었던 점은 인증 흐름이 전략 단위로 완전히 분리된다는 점이었습니다. 기존에는 로그인 시 JWT를 직접 발급하고 검증하는 로직을 한곳에 몰아넣기 쉬웠는데, JwtStrategy를 사용하면 토큰 발급과 검증을 명확히 역할별로 나눌 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 Passport의 구조 덕분에 JWT 전략을 별도 파일로 작성하고, validate() 메서드 내부에서 토큰의 payload만 넘겨주면 request.user에 자동으로 주입된다는 점이 매우 편리했습니다. 덕분에 라우트에서는 인증된 사용자 객체를 별도 로직 없이 사용할 수 있었고, 이는 전반적인 코드의 가독성과 유지보수성을 높여주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 access 토큰과 refresh 토큰의 발급 로직을 issueToken() 함수로 분리하여 설계하니, 토큰 발급 정책을 일관되게 관리할 수 있었습니다. 이 방식은 추후 Redis나 블랙리스트 기반의 리프레시 토큰 관리 전략으로 확장할 때도 유리하다는 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Guard를 통해 인증이 필요한 라우트를 보호하는 구조 역시 직관적이었습니다. @UseGuards(JwtAuthGuard) 한 줄로 해당 라우트를 보호할 수 있고, 토큰이 없거나 잘못된 경우에는 Unauthorized 에러가 자동으로 처리된다는 점에서 불필요한 예외 처리 코드를 줄일 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 처음에는 PassportStrategy의 추상화 구조가 조금 낯설게 느껴졌습니다. 특히 jwtFromRequest, secretOrKey 등의 설정이 어떻게 작동하는지 이해하는 데 시간이 걸렸습니다. 그럼에도 불구하고 흐름을 파악한 뒤에는 NestJS의 DI 시스템과 함께 잘 어우러져 있다는 점에서 큰 만족감을 느꼈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 이번 학습을 통해 느낀 가장 큰 인사이트는, 인증 로직을 전략화함으로써 인증 시스템의 확장성과 유연성을 확보할 수 있다는 점이었습니다. 추후 OAuth2, SNS 로그인, 2차 인증 등의 기능도 각기 다른 Strategy로 관리하면, 시스템 전반의 복잡도를 최소화하면서 기능은 점차 확장해나갈 수 있을 것입니다. NestJS에서 Passport와 JWT를 활용한 인증 설계는, 단순한 구현을 넘어 인증 아키텍처의 핵심적 설계 방식임을 깨닫게 되었습니다.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;학습 인증샷&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;284&quot; data-origin-height=&quot;188&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPzbun/btsNKepSy1j/iovtyuRvVIWNAjMsKUzKwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPzbun/btsNKepSy1j/iovtyuRvVIWNAjMsKUzKwK/img.png&quot; data-alt=&quot;수강 인증 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPzbun/btsNKepSy1j/iovtyuRvVIWNAjMsKUzKwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPzbun%2FbtsNKepSy1j%2FiovtyuRvVIWNAjMsKUzKwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;284&quot; height=&quot;188&quot; data-origin-width=&quot;284&quot; data-origin-height=&quot;188&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;수강 인증 사진&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1232&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PIV9s/btsNJ4gH9Oq/KK7ip5SY7xOgiKY1R50fyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PIV9s/btsNJ4gH9Oq/KK7ip5SY7xOgiKY1R50fyk/img.png&quot; data-alt=&quot;학습 인증샷&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PIV9s/btsNJ4gH9Oq/KK7ip5SY7xOgiKY1R50fyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPIV9s%2FbtsNJ4gH9Oq%2FKK7ip5SY7xOgiKY1R50fyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1232&quot; height=&quot;440&quot; data-origin-width=&quot;1232&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;학습 인증샷&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;195&quot; data-origin-height=&quot;83&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sRoIa/btsNMgGtmh8/9SWLP3BMBe44tjsOF06kA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sRoIa/btsNMgGtmh8/9SWLP3BMBe44tjsOF06kA1/img.png&quot; data-alt=&quot;공부 시작 시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sRoIa/btsNMgGtmh8/9SWLP3BMBe44tjsOF06kA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsRoIa%2FbtsNMgGtmh8%2F9SWLP3BMBe44tjsOF06kA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;195&quot; height=&quot;83&quot; data-origin-width=&quot;195&quot; data-origin-height=&quot;83&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공부 시작 시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;182&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZTFW9/btsNLC4idqA/RXG69KTiLxOrYH9SghoSHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZTFW9/btsNLC4idqA/RXG69KTiLxOrYH9SghoSHk/img.png&quot; data-alt=&quot;공부 종료 시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZTFW9/btsNLC4idqA/RXG69KTiLxOrYH9SghoSHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZTFW9%2FbtsNLC4idqA%2FRXG69KTiLxOrYH9SghoSHk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;182&quot; height=&quot;86&quot; data-origin-width=&quot;182&quot; data-origin-height=&quot;86&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공부 종료 시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333;&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;a href=&quot;https://abit.ly/lisbva&quot;&gt;https://abit.ly/lisbva&lt;/a&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;</description>
      <category>NestJS</category>
      <category>습관형성</category>
      <category>오공완</category>
      <category>직장인자기계발</category>
      <category>패스트캠퍼스</category>
      <category>패스트캠퍼스후기</category>
      <category>환급챌린지</category>
      <author>NangIn</author>
      <guid isPermaLink="true">https://nang-in.tistory.com/153</guid>
      <comments>https://nang-in.tistory.com/153#entry153comment</comments>
      <pubDate>Tue, 29 Apr 2025 22:43:04 +0900</pubDate>
    </item>
    <item>
      <title>패스트캠퍼스 환급챌린지 28일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기</title>
      <link>https://nang-in.tistory.com/152</link>
      <description>&lt;p style=&quot;background-color: #e7f8ff; color: #000000; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;강의 내용 정리&amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;passport 이론&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Passport란 무엇인가?&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;모듈화된 인증 시스템&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다양한 인증 방식을 &lt;b&gt;유연하게 연결&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;ex) 이메일/비밀번호, 구글 로그인 등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드량을 줄일 수 있는 장점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;복잡한 인증 로직을 전략으로 추상화해 둔 덕분&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;미들웨어 기반&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 코드에 &lt;b&gt;비파괴적으로 통합 가능&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코어는 가볍고 일반화된 구조&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다양한 전략을 끼워 넣을 수 있게 설계됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;세션 방식과 토큰 방식 모두 지원&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전략마다 방식이 다를 뿐, Passport 자체는 모두 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;풍부한 생태계&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수많은 전략 패키지 존재 (ex. passport-local, passport-jwt, passport-google 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;패스포트 사용 시 주의점&lt;/b&gt;&amp;nbsp;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;장점&lt;/td&gt;
&lt;td&gt;단점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인증 코드 작성이 간편&lt;/td&gt;
&lt;td&gt;추상화로 인해 유연성 부족&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;다양한 전략 즉시 활용 가능&lt;/td&gt;
&lt;td&gt;내부 동작을 정확히 파악하지 않으면 디버깅 어려움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;미들웨어 형태로 쉽게 통합&lt;/td&gt;
&lt;td&gt;특정 프로젝트와 맞지 않을 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Passport 사용 방식 요약&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;전략(Strategy) 구현&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: LocalStrategy, JwtStrategy&lt;/li&gt;
&lt;li&gt;validate() 메서드를 필수로 구현해야 함&lt;/li&gt;
&lt;li&gt;이 안에서 인증 로직 작성 (ex. 유저 검증, 비밀번호 비교 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전략 주입&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Injectable()로 NestJS에서 DI로 사용&lt;/li&gt;
&lt;li&gt;PassportStrategy()를 확장하여 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Guard로 적용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AuthGuard('local') &amp;rarr; 해당 전략(LocalStrategy) 적용&lt;/li&gt;
&lt;li&gt;전략 이름은 커스터마이징 가능&lt;/li&gt;
&lt;li&gt;JWT 전략으로 바꾸려면 'jwt'만 지정해주면 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@UseGuards(AuthGuard('local'))
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;패스포트를 쓰는 이유 vs 쓰지 않는 이유&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Passport 사용 장점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증에 필요한 로직을 거의 직접 작성하지 않아도 됨&lt;/li&gt;
&lt;li&gt;전략 교체가 간편 (JWT &amp;harr;︎ Local 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Passport 사용 단점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추상화가 많아 직접 제어가 어려움&lt;/li&gt;
&lt;li&gt;자유도가 낮아 세세한 커스터마이징이 어렵고&lt;/li&gt;
&lt;li&gt;프로젝트 요구사항에 따라 &lt;b&gt;과한 복잡성을 유발&lt;/b&gt;할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Local Strategy 적용하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Passport 관련 패키지 설치&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;autoit&quot;&gt;&lt;code&gt;pnpm i @nestjs/passport passport passport-local
pnpm i -D @types/passport-local
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@nestjs/passport: NestJS에서 Passport 전략을 쉽게 쓰도록 도와주는 모듈&lt;/li&gt;
&lt;li&gt;passport: 기본 Passport.js&lt;/li&gt;
&lt;li&gt;passport-local: Local 전략용&lt;/li&gt;
&lt;li&gt;@types/passport-local: 타입 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;전략 디렉토리 및 Local Strategy 생성&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/auth/strategy/local.strategy.ts 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전략 클래스 기본 구성&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super({ usernameField: 'email' }); // 기본값은 username
  }

  async validate(email: string, password: string): Promise&amp;lt;User&amp;gt; {
    return this.authService.authenticate(email, password);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;usernameField 변경: 기본은 username, email로 지정 가능&lt;/li&gt;
&lt;li&gt;validate는 자동 호출되는 콜백 &amp;rarr; 여기서 유저 검증 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;AuthService에 검증 로직 분리&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 로그인 로직에서 유저 검증 부분만 함수로 분리:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;async authenticate(email: string, password: string): Promise&amp;lt;User&amp;gt; {
  const user = await this.userRepository.findOne({ where: { email } });
  if (!user) throw new BadRequestException('잘못된 로그인 정보입니다.');

  const passOk = await bcrypt.compare(password, user.password);
  if (!passOk) throw new BadRequestException('잘못된 로그인 정보입니다.');

  return user;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Guard로 적용하기&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 방식 (문자열 사용):&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@UseGuards(AuthGuard('local'))
@Post('login-passport')
loginPassport(@Request() req) {
  return req.user; // validate에서 반환된 유저
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;전용 Guard로 추출 (오타 방지)&lt;/b&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;export class LocalAuthGuard extends AuthGuard('local') {}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@UseGuards(LocalAuthGuard)
@Post('login-passport')
loginPassport(@Request() req) {
  return req.user;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;학습 후기&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestJS에서 인증 시스템을 구축하면서 Passport를 적용해보니, 단순히 &quot;전략을 등록해서 Guard로 감싼다&quot;는 문법 이상의 통찰이 생겼습니다. Passport는 단순한 인증 라이브러리가 아니라, 인증 방식을 추상화하고 전략적으로 분리할 수 있도록 도와주는 플러그인 시스템에 가까운 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Local Strategy를 적용하면서 느낀 가장 큰 장점은 인증 흐름이 명확해진다는 점입니다. 기존에는 로그인 요청에서 이메일, 비밀번호를 직접 검증하고 응답하는 코드가 라우터에 함께 존재했다면, 전략으로 분리한 후에는 validate() 메서드에 인증 책임을 위임할 수 있었습니다. 이 덕분에 인증 로직과 라우팅 로직의 관심사가 명확히 분리되었고, 테스트하기도 쉬워졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 AuthGuard('local')과 같이 전략을 문자열로 지정하여 사용할 수 있고, 이를 별도의 Guard 클래스로 추출해 관리하면 전략 이름 오타를 방지할 수 있으며 재사용성도 높아졌습니다. 특히, usernameField를 email로 변경해 Postman 테스트에서도 혼란 없이 진행할 수 있었고, 실제 전략 간 전환(JWT &amp;harr; Local)도 매우 간편했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 단점도 있었습니다. Passport는 전략 내부 동작이 추상화되어 있어, 처음에는 흐름을 따라가기 어렵고 디버깅이 복잡하게 느껴졌습니다. 특히 NestJS에서 Guard가 validate() 결과를 자동으로 req.user에 실어주는 구조는 문서를 보지 않으면 파악하기 어려운 부분이었습니다. 하지만 내부 흐름을 이해한 후에는 오히려 일관된 인증 구조를 구성할 수 있다는 장점으로 다가왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 학습을 통해 느낀 가장 중요한 점은, 인증은 단순히 로그인 여부를 판단하는 절차가 아니라, 애플리케이션의 전반적인 보안 흐름을 설계하는 것이라는 사실입니다. Passport를 도입함으로써 인증 전략을 별도로 설계하고, 확장성과 테스트 가능성을 확보할 수 있었던 점이 매우 인상 깊었습니다. 앞으로 JWT 전략까지 결합하면 더 강력한 인증 시스템을 구축할 수 있을 것으로 기대됩니다.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;학습 인증샷&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;287&quot; data-origin-height=&quot;134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BVnUe/btsNLod7XBD/7akJprv8V8idGTw7tSj5ik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BVnUe/btsNLod7XBD/7akJprv8V8idGTw7tSj5ik/img.png&quot; data-alt=&quot;수강 인증 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BVnUe/btsNLod7XBD/7akJprv8V8idGTw7tSj5ik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBVnUe%2FbtsNLod7XBD%2F7akJprv8V8idGTw7tSj5ik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;287&quot; height=&quot;134&quot; data-origin-width=&quot;287&quot; data-origin-height=&quot;134&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;수강 인증 사진&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;518&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1gTAP/btsNKgOEngP/9LIqBTerq9h4in7GD6fKbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1gTAP/btsNKgOEngP/9LIqBTerq9h4in7GD6fKbk/img.png&quot; data-alt=&quot;학습 인증샷&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1gTAP/btsNKgOEngP/9LIqBTerq9h4in7GD6fKbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1gTAP%2FbtsNKgOEngP%2F9LIqBTerq9h4in7GD6fKbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1143&quot; height=&quot;518&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;518&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;학습 인증샷&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;150&quot; data-origin-height=&quot;76&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cy1g5c/btsNLp475jH/jCaOqn7SMQdda0LmKdQ3u1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cy1g5c/btsNLp475jH/jCaOqn7SMQdda0LmKdQ3u1/img.png&quot; data-alt=&quot;공부 종료 시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cy1g5c/btsNLp475jH/jCaOqn7SMQdda0LmKdQ3u1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcy1g5c%2FbtsNLp475jH%2FjCaOqn7SMQdda0LmKdQ3u1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;150&quot; height=&quot;76&quot; data-origin-width=&quot;150&quot; data-origin-height=&quot;76&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공부 종료 시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;192&quot; data-origin-height=&quot;96&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dnzhAi/btsNKg80Jo8/BQ07THHZ03tWHlkZD1Y3sk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dnzhAi/btsNKg80Jo8/BQ07THHZ03tWHlkZD1Y3sk/img.png&quot; data-alt=&quot;공부 시작 시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dnzhAi/btsNKg80Jo8/BQ07THHZ03tWHlkZD1Y3sk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdnzhAi%2FbtsNKg80Jo8%2FBQ07THHZ03tWHlkZD1Y3sk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;192&quot; height=&quot;96&quot; data-origin-width=&quot;192&quot; data-origin-height=&quot;96&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공부 시작 시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333;&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;a href=&quot;https://abit.ly/lisbva&quot;&gt;https://abit.ly/lisbva&lt;/a&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;</description>
      <category>NestJS</category>
      <category>습관형성</category>
      <category>오공완</category>
      <category>직장인자기계발</category>
      <category>패스트캠퍼스</category>
      <category>패스트캠퍼스후기</category>
      <category>환급챌린지</category>
      <author>NangIn</author>
      <guid isPermaLink="true">https://nang-in.tistory.com/152</guid>
      <comments>https://nang-in.tistory.com/152#entry152comment</comments>
      <pubDate>Mon, 28 Apr 2025 18:56:02 +0900</pubDate>
    </item>
    <item>
      <title>패스트캠퍼스 환급챌린지 27일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기</title>
      <link>https://nang-in.tistory.com/151</link>
      <description>&lt;p style=&quot;background-color: #e7f8ff; color: #000000; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;강의 내용 정리&amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;로그인&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;로그인 엔드포인트 구성&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Post('login')
loginUser(@Headers('Authorization') token: string) {
  return this.authService.login(token);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨트롤러에서 @Post('login')으로 로그인 라우트 설정&lt;/li&gt;
&lt;li&gt;@Headers('Authorization')을 통해 Basic Token 수신&lt;/li&gt;
&lt;li&gt;서비스 메서드 login(token: string)에 위임&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Basic Token 디코딩&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;parseBasicToken() 유틸로 다음 작업 수행:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;'Basic base64Encoded' &amp;rarr; 공백 기준 split&lt;/li&gt;
&lt;li&gt;base64Encoded를 디코딩하여 &quot;email:password&quot; 포맷 획득&lt;/li&gt;
&lt;li&gt;: 기준 split &amp;rarr; 이메일과 비밀번호 분리&lt;/li&gt;
&lt;li&gt;유효하지 않으면 BadRequestException 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;사용자 확인 및 비밀번호 비교&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이메일로 사용자 조회:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const { email, password } = this.parseBasicToken(rawToken);
const user = await this.userRepository.findOne({ where: { email } });
if (!user) throw new BadRequestException('잘못된 로그인 정보입니다!');
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;없으면 &amp;rarr; &quot;잘못된 로그인 정보입니다.&quot; 예외&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;비밀번호 비교:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;vbnet&quot;&gt;&lt;code&gt;const passOk = await bcrypt.compare(password, user.password);
if (!passOk) throw new BadRequestException('잘못된 로그인 정보입니다!');
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실패 시 동일 예외 던짐 (힌트 방지 목적)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;JWT 모듈 설정 및 토큰 발급&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@nestjs/jwt 설치
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;pnpm i @nestjs/jwt
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JwtModule.register()로 모듈 구성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;elm&quot;&gt;&lt;code&gt;// auth.module.ts
@Module({
  imports: [TypeOrmModule.forFeature([User]), JwtModule.register({})],
  controllers: [AuthController],
  providers: [AuthService],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;환경변수에 비밀 키 및 만료시간 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;#JWT
ACCESS_TOKEN_SECRET=codefactory
REFRESH_TOKEN_SECRET=codefactory
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;app.moule.ts에 등록
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;ConfigModule.forRoot({
      isGlobal: true,
      validationSchema: Joi.object({
        //...
        ACCESS_TOKEN_SECRET: Joi.string().required(),
        REFRESH_TOKEN_SECRET: Joi.string().required(),
      }),
    }),
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;jwtService 주입
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;  constructor(
		// ...
    private readonly jwtService: JwtService,
  ) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;토큰 두 개 발급:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;const accessTokenSecret = this.configService.get&amp;lt;string&amp;gt;(
  'ACCESS_TOKEN_SECRET',
);
const refreshTokenSecret = this.configService.get&amp;lt;string&amp;gt;(
  'REFRESH_TOKEN_SECRET',
);

return {
  refreshToken: await this.jwtService.signAsync(
    {
      sub: user.id,
      role: user.role,
      type: 'refresh',
    },
    {
      secret: refreshTokenSecret,
      expiresIn: '24h',
    },
  ),
  accessToken: await this.jwtService.signAsync(
    {
      sub: user.id,
      role: user.role,
      type: 'access',
    },
    { secret: accessTokenSecret, expiresIn: 300 },
  ),
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Access Token&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유효기간: 5분 (300s)&lt;/li&gt;
&lt;li&gt;용도: 프라이빗 API 접근&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Refresh Token&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유효기간: 24시간&lt;/li&gt;
&lt;li&gt;용도: Access Token 재발급&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JWT Payload 구성&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Access/Refresh 공통 페이로드:&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;{
  sub: user.id,
  role: user.role,
  type: 'access' | 'refresh'
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;signAsync() 호출 시 옵션 별도 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;학습 후기&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의를 통해 로그인 로직을 직접 구현하며 인증 시스템의 신뢰성과 보안성을 어떻게 확보하는지 구체적으로 이해할 수 있었습니다. Basic Auth를 이용한 로그인 요청 수신부터 JWT 토큰 발급까지의 흐름을 손으로 따라가면서, 인증이 &quot;단순히 동작하는 것&quot;과 &quot;보안적으로 안전한 것&quot;은 완전히 다르다는 사실을 체감했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Basic 토큰을 디코딩하고 이메일과 비밀번호를 분리하는 과정은 간단해 보였지만, 입력 포맷 오류에 대한 철저한 검증이 중요하다는 점을 깨달았습니다. 입력값 하나조차 방어적으로 다루지 않으면 보안 취약점으로 이어질 수 있다는 교훈이 인상 깊었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bcrypt를 이용한 비밀번호 비교에서는, 저장된 해시값과 입력값을 안전하게 비교하는 것이 얼마나 중요한지 느꼈습니다. 또한 로그인 실패 시 항상 동일한 에러 메시지를 반환해 해커에게 단서를 주지 않는 세심한 설계가 필요하다는 것도 알게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 발급 과정에서는 access token과 refresh token을 분리하고, 각각 다른 secret과 만료 시간을 설정해 보안과 UX를 모두 고려하는 방법을 배웠습니다. 특히 jwt.io를 통해 직접 디코딩해보면서, Header, Payload, Signature 구조와 변조 검증 방식까지 확실히 이해할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 과정을 통해 인증 시스템을 구축할 때 단순 구현이 아닌 보안을 항상 우선순위에 두어야 함을 다시 느꼈습니다. 앞으로도 인증 로직을 구현할 때 이번 경험을 바탕으로 더욱 신중하고 견고하게 설계하겠다는 다짐을 하게 되었습니다.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;학습 인증샷&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;314&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CyXFw/btsNCoZwtPX/u07IZlRy3m6xCyjAC4LYG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CyXFw/btsNCoZwtPX/u07IZlRy3m6xCyjAC4LYG1/img.png&quot; data-alt=&quot;수강 인증 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CyXFw/btsNCoZwtPX/u07IZlRy3m6xCyjAC4LYG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCyXFw%2FbtsNCoZwtPX%2Fu07IZlRy3m6xCyjAC4LYG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;314&quot; height=&quot;124&quot; data-origin-width=&quot;314&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;수강 인증 사진&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bV3QYK/btsNAG1H1lM/87xVAyouJX8izBCmZxJLG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bV3QYK/btsNAG1H1lM/87xVAyouJX8izBCmZxJLG0/img.png&quot; data-alt=&quot;학습 인증샷&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bV3QYK/btsNAG1H1lM/87xVAyouJX8izBCmZxJLG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbV3QYK%2FbtsNAG1H1lM%2F87xVAyouJX8izBCmZxJLG0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1032&quot; height=&quot;466&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;466&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;학습 인증샷&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;210&quot; data-origin-height=&quot;117&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IHWjR/btsNBwjnYWn/oiSjOpap5hVhaKLm1GDksK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IHWjR/btsNBwjnYWn/oiSjOpap5hVhaKLm1GDksK/img.png&quot; data-alt=&quot;공부 시작 시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IHWjR/btsNBwjnYWn/oiSjOpap5hVhaKLm1GDksK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIHWjR%2FbtsNBwjnYWn%2FoiSjOpap5hVhaKLm1GDksK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;210&quot; height=&quot;117&quot; data-origin-width=&quot;210&quot; data-origin-height=&quot;117&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공부 시작 시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;207&quot; data-origin-height=&quot;96&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/X6pHt/btsNCfnT4aF/x4auFVCjmnhj6kkbz7CLb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/X6pHt/btsNCfnT4aF/x4auFVCjmnhj6kkbz7CLb1/img.png&quot; data-alt=&quot;공부 종료 시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/X6pHt/btsNCfnT4aF/x4auFVCjmnhj6kkbz7CLb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FX6pHt%2FbtsNCfnT4aF%2Fx4auFVCjmnhj6kkbz7CLb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;207&quot; height=&quot;96&quot; data-origin-width=&quot;207&quot; data-origin-height=&quot;96&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공부 종료 시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333;&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;a href=&quot;https://abit.ly/lisbva&quot;&gt;https://abit.ly/lisbva&lt;/a&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;</description>
      <category>NestJS</category>
      <category>습관형성</category>
      <category>오공완</category>
      <category>직장인자기계발</category>
      <category>패스트캠퍼스</category>
      <category>패스트캠퍼스후기</category>
      <category>환급챌린지</category>
      <author>NangIn</author>
      <guid isPermaLink="true">https://nang-in.tistory.com/151</guid>
      <comments>https://nang-in.tistory.com/151#entry151comment</comments>
      <pubDate>Sun, 27 Apr 2025 23:11:41 +0900</pubDate>
    </item>
    <item>
      <title>패스트캠퍼스 환급챌린지 26일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기</title>
      <link>https://nang-in.tistory.com/149</link>
      <description>&lt;p style=&quot;background-color: #e7f8ff; color: #000000; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;강의 내용 정리&amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;User Module 작업하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;인증 구현의 시작: 회원가입 준비&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증 기능은 복잡하므로 &lt;b&gt;모듈을 분리&lt;/b&gt;하여 개발
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;User 모듈 &amp;rarr; 사용자 정보 관리&lt;/li&gt;
&lt;li&gt;Auth 모듈 &amp;rarr; 인증 전담 (재사용성 고려)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Nest CLI로 모듈 및 리소스 생성&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Auth 모듈 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;nest g resource auth
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;REST API 선택, CRUD 엔드포인트는 생성 안 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;User 모듈 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;nest g resource user
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;REST API 선택, CRUD 엔드포인트는 생성함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;User Entity 구성&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BaseTable 상속 (공통 컬럼 포함)&lt;/li&gt;
&lt;li&gt;주요 컬럼 정의:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;export enum Role {
  ADMIN = 'admin',
  PAID_USER = 'paid_user',
  USER = 'user',
}

@Entity('users')
export class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  email: string;

  @Column()
  password: string;

  @Column({ enum: Role, default: Role.USER })
  role: Role;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;User 서비스 및 CRUD 구현&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UserModule에 import
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// user.module.ts
@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UserController],
  providers: [UserService],
})

// app.module.ts
entities: [Movie, MovieDetail, Director, Genre, User],
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;UserService에 Repository 주입:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;constructor(
  @InjectRepository(User)
  private readonly userRepository: Repository&amp;lt;User&amp;gt;
) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CRUD 기능 순서대로 구현:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;전체 조회&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;findAll() {
  return this.userRepository.find();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단일 조회&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;async findOne(id: number) {
  const user = await this.userRepository.findOne({ where: { id } });
  if (!user) throw new NotFoundException('존재하지 않는 사용자입니다.');
  return user;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;생성&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;create(createUserDto: CreateUserDto) {
  return this.userRepository.save(createUserDto);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수정&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;async update(id: number, updateUserDto: UpdateUserDto) {
    const user = await this.userRepository.findOne({ where: { id } });
    if (!user) throw new NotFoundException('존재하지 않는 사용자입니다.');
    await this.userRepository.update({ id }, updateUserDto);
    return await this.userRepository.findOne({ where: { id } });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;삭제&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;remove(id: number) {
  return this.userRepository.delete(id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Controller 설정 및 Pipe 적용&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Param('id', ParseIntPipe)로 라우트 파라미터 타입 검증 적용&lt;/li&gt;
&lt;li&gt;기본적인 RESTful 라우트 생성 완료&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Auth Module 회원가입 작업하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;인증 시작 및 구조 설계&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;목표&lt;/b&gt;: 인증 시스템 구축의 첫걸음으로 회원가입 기능부터 구현&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구성 모듈&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;User 모듈: 사용자 관리 담당 (CRUD)&lt;/li&gt;
&lt;li&gt;Auth 모듈: 인증 담당 (회원가입, 로그인 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;User 엔티티 정의&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기본 필드&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Entity('users')
export class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  email: string;

  @Column()
  @Exclude({ toPlainOnly: true })
  password: string;

  @Column({ enum: Role, default: Role.USER })
  role: Role;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;id: 기본 PK&lt;/li&gt;
&lt;li&gt;email: @Column({ unique: true })&lt;/li&gt;
&lt;li&gt;password: @Exclude({ toPlainOnly: true }) &amp;rarr; 응답 시 숨김&lt;/li&gt;
&lt;li&gt;role: enum 기반 권한 관리 (Admin, PaidUser, User)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본값: User&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;직렬화 설정&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Exclude() 데코레이터 &amp;rarr; 비밀번호, createdAt, updatedAt, version 숨김&lt;/li&gt;
&lt;li&gt;컨트롤러에 @UseInterceptors(ClassSerializerInterceptor) 설정 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;AuthController - 회원가입 API 구현&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;라우팅&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POST /auth/register: 회원가입 요청을 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;요청 형식&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;헤더: Authorization: Basic &amp;lt;base64-encoded-email:password&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모듈 등록&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [AuthController],
  providers: [AuthService],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컨트롤러 구조&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Post('register')
registerUser(@Headers('authorization') token: string) {
  return this.authService.register(token);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서비스 등록&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository&amp;lt;User&amp;gt;,
  ) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;AuthService - 회원가입 로직 구현&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Basic 토큰 파싱&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;  parseBasicToken(rawToken: string) {
    // 1) 토큰을 ' ' 기준으로 스플릿한 후 토큰 값만 추출하기
    // ['Basic', 'token']
    const basicSplit = rawToken.split(' ');

    if (basicSplit.length !== 2) {
      throw new BadRequestException('토큰 포맷이 잘못됐습니다!');
    }

    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const [_, token] = basicSplit;

    // 2) 추출한 토큰을 base64 디코딩해서 이메일과 비밀번호로 나눈다.
    const decoded = Buffer.from(token, 'base64').toString('utf-8');

    // &quot;email:password&quot;
    const tokenSplit = decoded.split(':');

    if (tokenSplit.length !== 2) {
      throw new BadRequestException('토큰 포맷이 잘못됐습니다!');
    }

    const [email, password] = tokenSplit;

    return { email, password };
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이메일 중복 검사&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const user = await this.userRepository.findOne({ where: { email } });
if (user) throw new BadRequestException('이미 존재하는 이메일입니다.');
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비밀번호 해싱 처리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;bcrypt 설치
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;pnpm i bcrypt 
pnpm i -D @types/bcrypt
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;bcrypt 사용: hash(password, saltRounds)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;const hash = await bcrypt.hash(
  password,
  this.configService.get('HASH_ROUNDS') as number,
);

await this.userRepository.save({ email, password: hash });

return this.userRepository.findOne({ where: { email } });
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해싱된 비밀번호로 저장&lt;/li&gt;
&lt;li&gt;saltRounds는 .env 또는 config 모듈에서 관리 (기본값 10)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;HASH_ROUNDS=10
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;// app.module.ts
validationSchema: Joi.object({
  // ...
  HASH_ROUNDS: Joi.number().required(),
}),

// auth.service.ts
constructor(
  // ...
  private readonly configService: ConfigService,
) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용자 등록 및 반환&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해싱된 비밀번호로 DB 저장&lt;/li&gt;
&lt;li&gt;저장된 사용자 조회 후 반환 (단, password는 직렬화에서 제외됨)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;회원가입 테스트 (Postman)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Base64 수동 인코딩&lt;/b&gt;: email:password &amp;rarr; Authorization: Basic xxxxxxx&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Postman 내장 기능 사용&lt;/b&gt;: Authorization 탭 &amp;rarr; Basic Auth 선택 &amp;rarr; 자동 인코딩&lt;/li&gt;
&lt;li&gt;요청 성공 시 사용자 정보 반환 (비밀번호 등은 노출되지 않음)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;학습 후기&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 학습을 통해 인증 시스템을 처음부터 직접 구축하는 과정이 왜 이렇게 세밀하게 설계되어야 하는지를 뼈저리게 느꼈습니다. 특히 사용자 모듈과 인증 모듈을 분리하여 개발하는 설계 방식을 통해, 모듈화된 구조가 얼마나 프로젝트 유지보수성과 확장성에 중요한 역할을 하는지 체감할 수 있었습니다. 앞으로 다양한 인증 방식이 추가될 수 있다는 점을 고려하여 처음부터 인증 기능을 분리하는 것이 현명한 판단임을 깨달았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;User 엔티티를 설계할 때 단순히 email과 password만 저장하는 것이 아니라, Role을 명시적으로 구분하여 관리하는 방식이 굉장히 인상 깊었습니다. 이 작은 설계가 나중에 사용자 권한을 다루거나, 관리자와 일반 사용자를 구분하는 복잡한 로직에서 큰 역할을 하게 된다는 사실을 배웠습니다. 또한 패스워드에 @Exclude 데코레이터를 적용하고 직렬화 처리를 통해 응답 데이터에서 민감 정보를 안전하게 숨기는 과정을 보면서, 보안은 단순히 데이터베이스 저장뿐만 아니라 응답 레이어에서도 고려해야 한다는 점을 알게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auth 모듈을 통해 회원가입 로직을 구현하면서 Basic Auth 방식을 사용하는 것이 꽤나 깔끔한 패턴이라는 것을 알게 되었습니다. Base64 인코딩된 이메일과 비밀번호를 받아 서버에서 디코딩하고 검증하는 흐름은 직접 구현해보니 단순하지만 강력한 방식이라는 느낌을 받았습니다. 다만 Base64 인코딩은 암호화가 아니라 단순 인코딩일 뿐이므로 반드시 HTTPS로 통신해야 실제 보안이 확보된다는 점도 함께 인지하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호를 bcrypt를 사용해 해싱하는 부분도 매우 중요한 학습이었습니다. 해싱 과정을 직접 적용해보니, 단순한 저장이 아니라 보안성을 높이는 핵심 프로세스임을 실감할 수 있었습니다. 특히 saltRounds를 통해 해싱 속도를 조절하고, 이 값을 환경변수로 관리하는 패턴을 통해 실무에서는 모든 하드코딩을 피하고 설정 가능하게 만들어야 한다는 좋은 습관도 함께 배웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체적인 작업 흐름을 따라가면서 느낀 점은, 하나의 회원가입 기능을 완성하기 위해 수많은 작은 고려사항들이 존재한다는 것이었습니다. 그동안 당연하게 생각했던 로그인/회원가입 기능들이 사실은 보안, 설계, 확장성 등 다양한 측면을 모두 고려한 결과물이라는 사실을 새삼 깨닫게 되었습니다. 이번 과정을 통해 단순히 코드만 따라치는 것이 아니라, 왜 이렇게 설계하는지까지 이해하는 것이 얼마나 중요한지 깊이 체감했습니다. 앞으로도 비슷한 기능을 구현할 때 이번에 배운 설계 원칙과 보안 개념을 반드시 적용하겠다는 다짐을 하게 되었습니다.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;학습 인증샷&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;291&quot; data-origin-height=&quot;169&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daLKZp/btsNCa8gTY9/Ar8ogk6GZnp4qDtpsqXqFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daLKZp/btsNCa8gTY9/Ar8ogk6GZnp4qDtpsqXqFk/img.png&quot; data-alt=&quot;수강 인증 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daLKZp/btsNCa8gTY9/Ar8ogk6GZnp4qDtpsqXqFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdaLKZp%2FbtsNCa8gTY9%2FAr8ogk6GZnp4qDtpsqXqFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;291&quot; height=&quot;169&quot; data-origin-width=&quot;291&quot; data-origin-height=&quot;169&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;수강 인증 사진&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;989&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6movF/btsNAPRChEf/ZQM5ls07Z3yup159XAJXXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6movF/btsNAPRChEf/ZQM5ls07Z3yup159XAJXXk/img.png&quot; data-alt=&quot;학습 인증 샷&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6movF/btsNAPRChEf/ZQM5ls07Z3yup159XAJXXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6movF%2FbtsNAPRChEf%2FZQM5ls07Z3yup159XAJXXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;989&quot; height=&quot;348&quot; data-origin-width=&quot;989&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;학습 인증 샷&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;201&quot; data-origin-height=&quot;116&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjaqda/btsNAfQvxWY/Kl5omEFqzfvWaETMhHD5M1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjaqda/btsNAfQvxWY/Kl5omEFqzfvWaETMhHD5M1/img.png&quot; data-alt=&quot;공부 시작 시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjaqda/btsNAfQvxWY/Kl5omEFqzfvWaETMhHD5M1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcjaqda%2FbtsNAfQvxWY%2FKl5omEFqzfvWaETMhHD5M1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;201&quot; height=&quot;116&quot; data-origin-width=&quot;201&quot; data-origin-height=&quot;116&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공부 시작 시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;212&quot; data-origin-height=&quot;133&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbYKPj/btsNCorHvdD/ikLkkANs2wbGyKKB6kt6RK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbYKPj/btsNCorHvdD/ikLkkANs2wbGyKKB6kt6RK/img.png&quot; data-alt=&quot;공부 종료 시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbYKPj/btsNCorHvdD/ikLkkANs2wbGyKKB6kt6RK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbYKPj%2FbtsNCorHvdD%2FikLkkANs2wbGyKKB6kt6RK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;212&quot; height=&quot;133&quot; data-origin-width=&quot;212&quot; data-origin-height=&quot;133&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공부 종료 시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333;&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;a href=&quot;https://abit.ly/lisbva&quot;&gt;https://abit.ly/lisbva&lt;/a&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;</description>
      <category>NestJS</category>
      <category>습관형성</category>
      <category>오공완</category>
      <category>직장인자기계발</category>
      <category>패스트캠퍼스</category>
      <category>패스트캠퍼스후기</category>
      <category>환급챌린지</category>
      <author>NangIn</author>
      <guid isPermaLink="true">https://nang-in.tistory.com/149</guid>
      <comments>https://nang-in.tistory.com/149#entry149comment</comments>
      <pubDate>Sat, 26 Apr 2025 20:33:51 +0900</pubDate>
    </item>
  </channel>
</rss>