한용재님의 'NestJS 로 배우는 백엔드 프로그래밍' 책을 읽고 작성하는 글입니다.
GitHub: https://github.com/dextto/book-nestjs-backend
Ch1. Hello NestJS
1.1 NestJS 의 장점
Express 또는 Fastify 프레임워크를 래핑하여 동작합니다.
1.2 Express 가 좋을까, NestJS 가 좋을까
NestJS 는 2022년 3월에 8.4.0 을 릴리스했고, 깃허브를 통해 커튜미케이션도 활발히 하고 있습니다.
1.3. NestJS 설치
1.3.1 Node.js 설치
집필 시점에 최신 Node.js LTS 버전은 16.14.0 입니다. (지금은 20.xx 대)
1.3.2 NestJS 프로젝트 생성
설치
npm i -g @nestjs/cli
참고로 글로벌 환경에서 패키지가 설치되는 경로는 npm root -g 명령어로 확인할 수 있습니다.
보일러플레이트 코드 생성
nest new project-name # nest new . 로 하면 폴더를 생성하지 않고 현재 디렉토리에 생성
공식 예제 스타터 프로젝트를 클론해도 된다.
GitHub: https://github.com/nestjs/typescript-starter
예제 소스 클론
git clone https://github.com/dextto/book-nestjs-backend.git
cd examples/ch1-intro
npm i
npm run start:dev
--watch 옵션은 소스 코드 변경을 감지하여 코드를 저장할 때마다 서버를 다시 구동하는 옵션입니다.
1.4. 책에서 만들 애플리케이션: 유저 서비스
유저 서비스를 만들기 위해 부가적으로 해야할 일
- 환경 변수 설정
- 요청 유효성 검사
- 인증
- 로깅
- 헬스 체크
- CQRS
- 클린 아키텍처
- 단위 테스트
Ch2. 웹 개발 기초 지식
2.1 웹 프레임워크
2.2 Node.js
ECMA 스크립트 2015 (ES6) 에서 프로미스가 도입되면서 간결한 표현으로 작성할 수 있게 되었고, ECMA 2017 에서는 async, await 가 추가되면서 비동기 코드를 마치 동기식으로 처리하는 것 처럼 코드를 작성할 수 있게 되었습니다.
2.3. 이벤트 루프
2.4 패키지 의존성 관리
2.4.1 package.json
package.json 파일의 역할은 다음과 같습니다.
- 애플리케이션이 필요로 하는 패키지 목록을 나열합니다.
- 각 패키지는 시맨틱 버저닝 규칙으로 필요한 버전을 기술합니다.
- 다른 개발자와 같은 빌드 환경을 구성할 수 있습니다. 버전이 달라 발생하는 문제를 예방합니다.
시멘틱 버저닝 규칙은
[Major].[Minor].[Patch]-[label]
- Major: 호환성
- Minor: 기능
- Patch: 버그 수정 패치
- label: pre, alpha, beta 와 같이 버전에 대한 부가 설명을 붙이고자 할 때 문자열로 작성합니다.
2.4.2 package-lock.json
node_modules 나 package.json 파일의 내용이 바뀌면 npm install 명령을 수행할 때 자동 수정됩니다.
2.4.3 package.json 파일 분석
- name: 패키지의 이름 (패키지를 npm 에 공개하지 않는다면 선택 사항)
- private: true 로 설정할 경우 공개되지 않음
- version: 패키지의 버전. 공개할 패키지를 만들고 있다면 버전명에 신경을 써야 함
- description: 설명
- license: vozlwldml fkdltjstm
- scripts: npm run 명령과 함께 수행할 수 있는 스크립트
- dependencies: 패키지가 의존하는 다른 패키지
- devDependencies: 개발, 테스트환경에서만
- jest: 테스트 라이브러리 Jest 를 위한 환경 구성 옵션. NestJS 는 기본으로 Jest 를 이용한 테스트를 제공
2.5 타입스크립트
2.5.1 변수 선언
타입스크립트에서 변수를 선언하는 방식
[선언 키워드] [변수명]: [타입]
선언 키워드는 const, let 또는 var
2.5.2 타입스크립트에서 지원하는 타입
자바스크립트의 타입은 원시 값 (primitive value) 과 객체 (object), 함수 (function) 가 있습니다. typeof 키워드를 이용하여 인스턴스의 타입을 알 수 있습니다.
typeof instance === "undefined"
원시 값 타입
typeof | 설명 | 할당 가능한 값 | Python |
boolean | true, false | bool | |
null | '유효하지 않음' 을 나타냄 | null | None |
undefined | 값이 존재하지 않음. | undefined | None |
number | 배정밀도 64비트 형식 IEEE 754 의 값 | -(2^53-1) 와 2^53-1 사이의 정수와 실수 +Infinity, Infinity NaN (Not a Number) |
int, float |
bigint | Number 의 범위를 넘어서는 정수를 안전하게 저장하고 연산할 수 있게 해줌 | 예: const x = 2n ** 53n; 정수 끝에 n 을 추가 |
int |
string | 문자열. 변경 불가능 (immutable) 함 | 예: 'hello', "world" 홑따옴표 또는 쌍따옴표로 둘러싸인 문자열 |
str |
symbol | 유일하고 변경 불가능한 원시 값. 객체 속성의 키로 사용할 수 있음 |
객체 타입
const dexter = {
name: 'Dexter Han',
age: 21,
hobby: ['Movie', 'Billiards'],
}
자바스크립트에는 개발할 때 유용한 내장 객체들이 있습니다.
- Date: 1970 년 1월 1일 UTC 자정과의 시간 차이를 밀리초 단위로 나타낸 것으로 시간을 다룰 때 사용합니다.
- 배열(Array): 정수를 키로 가지는 일련의 값을 가진 객체입니다. 코드로 표현할 때는 대괄호([]) 로 표현합니다.
- 키를 가진 컬렉션: Map, WeakMap 은 키와 값을 가지는 객체 타입이고, Set 과 WeakSet 은 유일값들로 이루어진 컬렉션 객체 타입입니다.
- JSON: JSON 은 자바스크립트에서 파생된 경량의 데이터 교환 형식이지만 많은 프로그래밍에서 사용됩니다.
MDN 내장객체 문서: https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects
함수 타입
함수의 결과로 반환할 수도 있습니다. 이러한 특징을 일급 함수 (first-class function) 라고 합니다.
any / unknown / never
타입스크립트에는 특수한 타입이 있습니다. any 는 자바스크립트와 같이 어떠한 타입의 값도 받을 수 있는 타입입니다. 이 특성 때문에 런타임에 오류를 일으킬 가능성이 있습니다. unknown 타입은 any 타입과 마찬가지로 어떤 타입도 할당 가능하지만 다른 변수에 할당 또는 사용할 때 타입을 강제하도록 하여 any 가 일으키는 오류를 줄여줍니다. never 타입의 변수에는 어떤 값도 할당할 수 없습니다. 함수의 리턴 타입으로 지정하면 함수가 어떤 값도 반환하지 않는다는 것을 뜻하고, 다음과 같이 특정 타입의 값을 할당받지 못하도록 한흔데 사용할 수도 있습니다.
type NonString<T> = T extends string ? never : T;
2.5.3 타입 정의하기
인터페이스 선언
interface User {
name: string
age: number
}
const user: User = {
name: 'Dexter',
age: 21,
}
클래스 선언
class User {
constructor(name: string, age: number) {}
}
const user: User = new User('Dexter', 21)
접근제한자를 쓰지 않으면 public 변수가 된다.
멤버변수를 사용할 때는 this.name 과 같이 this 키워드와 함께 사용된다.
또 type 키워드로 새로운 타입을 만들 수 있습니다.
type MyUser = User
2.5.4 타입 구성하기
자바스크립트는 덕 타이핑을 지원합니다.
유니언 타입
function getLength(obj:string|string[]) {
return obj.length
}
type Status = 'Ready' | 'Waiting'
열거형
enum Status {
READY = 'Ready',
WAITING = 'Waiting',
}
2.6 데커레이터
Nest 는 데커레이터 (decorator) 를 적극 활용합니다. 클래스, 메서드, 접근자, 프로퍼티, 매개변수에 적용 가능합니다. 예를 들어 다음 코드는 유저 생성 요청의 본문을 데이터 전송 객체 (DTO) 로 표현한 클래스입니다.
class CreateUserDto {
@IsEmail()
@MaxLength(60)
readonly email: string
@isString()
@Matches(...)
readonly password: string
}
tsconfig.json 파일을 보겠습니다. experimentalDecorators 옵션이 true 로 설정되어 있습니다. 이 옵션을 켜야 데커레이터를 사용할 수 있습니다.
2.6.1 데커레이터 합성
@f
@g
test
수학적으로는 f(g(x)) 와 같습니다.
- 각 데커레이터의 표현은 위에서 아래로 평가됩니다.
- 그런 다음 결과는 아래에서 위로 함수로 호출됩니다.
2.6.2 클래스 데커레이터
선언 파일: 타입스크립트 소스 코드를 컴파일할 때 생성되는 파일로 타입시스템의 타입 추론을 돕는 코드가 포함되어 있습니다.
2.6.3 메서드 데커레이터
2.6.4 접근자 데커레이터
접근자 바로 앞에 선언합니다.
2.6.5 속성 데커레이터
클래스의 속성 바로 앞에 선언됩니다.
2.6.6 매개변수 데커레이터
생성자 또는 메서드의 매개변수에 선언되어 적용됩니다.
Ch3. 애플리케이션의 관문: 인터페이스
3.1 컨트롤러
CRUD 보일러플레이트 코드 생성
nest g res [name]
- users
- dto
- create-user.dto.ts
- update-user.dto.ts
- entities
- user.entity.ts
- users.controller.ts
- users.module.ts
- users.service.ts
- dto
controller 에서 service 를 연결해줄 뿐만 아니라, 모듈도 만들어준다. 모듈은 무슨역할을 하는걸까?
3.1.1 라우팅
Nest 는 라우팅 패스가 지정된 클래스나 함수의 이름이 무엇이든 전혀 상관하지 않습니다.
3.1.2 와일드카드 사용
@Get('he*lo')
getHello(): string {
}
- *
- ?
- +
- ()
- -, .: 문자열로 취급
3.1.3 요청 객체
- @Req
- @Query
- @Param(key?: string)
- @Body
3.1.4 응답
Nest 는 string, number, boolean 과 같이 자바스크립트 원시 타입을 리턴할 경우 직렬화 없이 바로 보내지만, 객체를 리턴한다면 직렬화를 통해 JSON 으로 자동 변환해줍니다. Express 라면 @Res 데커레이터를 이용해서 Exprss 응답 객체를 다룰 수 있습니다.
상태 코드를 다른 값으로 바꾸길 원한다면 @HttpCode(202) 와 같이 사용한다.
- 404: throw new BadRequestException(msg)
- 400: throw new NotFoundException(msg)
3.1.5 헤더
응답에 커스텀 헤더를 추가하고 싶다면 @Header 데커레이터를 사용하면 됩니다.
3.1.6 리디렉션
@Redirect 는 보통 301, 307, 308 을 사용합니다.
@Redirect('https://nestjs.com', 301)
3.1.7 라우트 매개변수
@Delete(':userId/memo/:memoId')
deleteUserMemo(
@Param('userId') userId: string,
3.1.8 하위 도메인 라우팅
@Controller({host: ':version.api.localhost'})
...
@Get()
index(@HostParam('version') version: string): string {
return 'Hello, API ${version}'
}
3.1.9 페이로드 다루기
body
@Post()
create(@Body() createUserDto: CreateUserDto) {
}
query
export class GetUsersDto {
offset: number
limit: number
}
3.2 유저 서비스의 인터페이스
관점 지향 프로그래밍
인터셉터로 로깅, snake_case 를 camelCase 로 바꿀 수 있다.
Ch4. 핵심 도메인 로직을 포함하는 프로바이더
4.1 프로바이더
서비스, 저장소 (repository), 팩터리 (factory), 헬퍼 (helper) 등 여러 가지 형태로 구현이 가능합니다.
@Injectable 데커레이터를 선엄함으로써 다른 어떤 Nest 컴포넌트에서도 주입할 수 있는 프로바이더가 됩니다. 별도의 스코프를 지정해주지 않으면 일반적으로 싱글턴 인스턴스가 생성됩니다.
4.2 프로바이더 등록과 사용
4.2.1 프로바이더 등록
프로바이더 인스턴스 역시 모듈에서 사용할 수 있도록 등록을 해줘야 합니다.
4.2.2 속성 기반 주입
- @Inject
- @Injectable
4.3 유저 서비스에 회원 가입 로직 구현하기
4.3.1 UsersService 프로바이더 생성
서비스 생성
nest g s Users
4.3.2 회원가입
uuid 설치
npm i uuid
npm i --save-dev @types/uuid
4.3.3 회원 가입 이메일 발송
무료로 이메일 전송을 해주는 nodemailer 라는 라이브러리를 사용
NPM: https://www.npmjs.com/package/nodemailer
설치
npm i nodemailer
npm i @types/nodemailer --save-dev
4.3.4 이메일 인증
4.3.5 로그인
쉬어가는 페이지. 스코프
스코프 종류
- DEFAULT: 모두 싱글턴 프로바이더의 인스턴스
- REQUEST: 들어오는 요청마다 별도의 인스턴스
- TRANSIENT: 임시. 지정한 인스턴스 공유 x
프로바이더에 스코프 적용하기
@Injectable({scope: Scope.REQUEST})
Ch5. SW 복잡도를 낮추기 위한 모듈 설계
Ch6. 동적 모듈을 활용한 환경 변수 구성
Ch7. 파이프와 유효성 검사: 요청이 제대로 전달되었는가
Ch8. 영속화: 데이터를 기록하고 다루기
Ch9. 요청 처리 전에 부가 기능을 수행하기 위한 미들웨어
Ch10. 권한 확인을 위한 가드: JWT 인증/인가
10.1 가드
인가는 퍼미션, 롤, 접근 제어 목록 (access control list, ACL) 같은 개념을 사용하여 유저가 가지고 있는 속성으로 리소스 사용을 허용할지 판별합니다.
10.2 가드를 이용한 인가
가드는 CanActivate 인터페이스를 구현해야 합니다.
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return this.validateRequest(request);
}
private validateRequest(request: Request) {
return false;
}
}
10.2.1 실행 콘텍스트
CanActivate 함수는 ExecutionContext 인스턴스를 인수로 받습니다. ExecutionContext 는 ArgumentsHost 를 상속받는데, 요청과 응답에 대한 정보를 가지고 있습니다. 우리는 HTTP 로 기능을 제공하고 있으므로 인터페이스에서 제공하는 함수 중 switchToHttp() 함수를 사용하여 필요한 정보를 가져올 수 있습니다.
10.2.2 가드 적용
@UseGuards(AuthGuard) 와 같이 사용하면 됩니다. AuthGuard 인스턴스의 생성은 Nest 가 맡아서 합니다. 만약 여러 종류의 가드를 적용하고 싶다면 쉼표로 이어 선언하면 됩니다.
전역으로 가드를 적용하고 싶다면 부트스트랩 과정을 수정해야 합니다.
가드에 종속성 주입을 사용해서 다른 프로바이더를 주입해서 사용하고 싶다면 커스텀 프로바이더로 선언해야 합니다.
10.3 인증
10.3.1 세션 기반 인증
10.3.2 토큰 기반 인증
10.4 JWT
10.4.1 헤더
- typ: 유형
- alg: 알고리즘
10.4.2 페이로드
클레임이라 부르는 정보를 포함
10.4.3 시그니처
암호화할 때 사용하는 secret 은 토큰을 생성하고 검증하는 서버에서만 안전한 방벙으로 저장해야 합니다.
10.5 유저 서비스의 이메일 인증 처리와 JWT 발급
10.5.1 회원 가입 이메일 인증
10.5.2 로그인
jwt 생성
import * as jwt from 'jsonwebtoken';
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import authConfig from 'src/config/authConfig';
import { ConfigType } from '@nestjs/config';
interface User {
id: string;
name: string;
email: string;
}
@Injectable()
export class AuthService {
constructor(
@Inject(authConfig.KEY) private config: ConfigType<typeof authConfig>,
) { }
login(user: User) {
const payload = { ...user };
return jwt.sign(payload, this.config.jwtSecret, {
expiresIn: '1d',
audience: 'example.com',
issuer: 'example.com',
});
}
10.5.3 JWT 인증: 회원 정보 조회
10.5.4 가드를 이용한 인가 처리
Nest 에서 제공하는 가드를 이용하여 이를 핸들러 코드에서 분리해봅시다.
@UseGuards(AuthGuard)
@Get(':id')
async getUserInfo(...) {
}
Ch11. 로깅: 애플리케이션의 동작 기록
Ch12. 모든 것은 항상 실패한다: 예외 필터
Ch13. 인터셉터로 요청과 응답을 입맛에 맞게 바꾸기
Ch14. 테스크 스케줄링
Ch15. 헬스 체크: 댁의 서버는 건강하신가요
Ch16. CQRS 를 이용한 관심사 분리
Ch17. 클린 아키텍처
Ch18. 테스트 자동화
18.1 소프트웨어 테스트
테스트 주도 개발 (test-driven development, TDD) 을 활용하여 테스트 코드를 먼저 작성하고 이를 기반으로 실제 소프트웨어의 코드를 작성해나가는 방법론도 있습니다.
E2E (end-to-end) 테스트는 사용자의 행동을 코드로 작성한 것입니다.
18.2 Nest 의 기본 테스트 프레임워크: Jest 와 SuperTest
- 테스트 실행되는 환경 제공: test runner
- 테스트 상황 가정: assertion
- 테스트 기대결과 비교: matcher
- 테스트 더블
Nest 는 기본 프레임워크로 Jest 와 SuperTest 를 제공합니다.
이 책에서는 SuperTest 를 이용한 테스트는 소개하지 않으며, 단위 테스트만 사용합니다.
18.3 Jest 를 이용한 단위 테스트 예시
Jest 공식문서: https://jestjs.io/docs/configuration
테스트 코드는 describe() 와 it() 구문으로 구성됩니다. descrive() 는 test suite 를 작성하는 블록입니다. test suit 는 테스트들을 의미 있는 단위로 묶은 것입니다.
it() 구문은 특정 테스트 시나리오를 작성하는 부분입니다.
- TDD
- BDD (Behavior-Driven Development): Given / When / Then 으로 작성
describe() 와 it() 구문 외에 SetUp, TearDown 이라 부르는 개념이 있습니다. test suit 내에서 모든 테스트 케이스를 수행하기 전에 수행해야 하는 조건이 있다면 SetUp 구문으로 반복 작업을 줄일 수 있습니다. 마찬가지로 테스트 후에 후처리가 필요하다면 TearDown 에서 공통 처리합니다. Jest 에서는 beforeAll(), beforeEach(), afterAll(), afterEach() 4가지의 구문을 제공합니다.
외부 모듈을 임의의 객체로 다루는 것, 이 개념을 test double 이라고 합니다. test double e을 세부적으로 dummy, fake, stub, spy, mock 로 나눕니다.
- dummy: 테스트를 위해 생성된 가짜 데이터
- fake: 인메모리 DB 와 같이 메모리에 적재해서 속도를 개선. 잘못된 데이터가 남아도 상관없는 세션 등과 같은 것을 대상으로 테스트할 때 사용합니다.
- spy: 테스트 수행 정보를 기록합니다.
- stub: 함수 호출 결과를 미리 준비된 응답으로 제공합니다.
- mock: stub 과 비슷한 역할을 합니다. 테스트 중에 만들어진 호출에 미리 준비된 답변을 제공하며 일반적으로 테스트를 위해 프로그래밍된 것 외에는 응답하지 않습니다.
18.4 유저 서비스 테스트
jest.fn() 은 어떠한 동작도 하지 않는 함수라는 뜻입니다.
create
create user
18.5 테스트 커버리지 측정
CLI
npm run test:cov
만약 사내에서 테스트 커버리지 기준을 정했다면 결과를 만족하지 못했을 경우 릴리스를 하지 못하도록 CI/CD 과정에 포함시킬 수도 있을 것입니다.
테스트 커버리지 측정을 수행하면 프로젝트 루트 디렉터리 아래에 coverage 디렉터리가 생깁니다. 이 디렉터리에 커버리지 측정 결과가 저장되어 있습니다. HTML 로 리포트를 볼 수도 있고, 각 모듈을 선택하여 라인별로 수행된 부분과 수행하지 않은 부분을 확인할 수 있습니다.
'Backend > 노트' 카테고리의 다른 글
러닝 타입스크립트 (0) | 2024.03.30 |
---|---|
NestJS로 API 만들기 (1) | 2024.03.26 |
파이썬으로 개발하는 빅데이터 기반 맛집 추천 서비스 (ft. Django, FastAPI) 초격차 패키지 Online (0) | 2023.12.22 |
FastAPI 를 사용한 파이썬 웹 개발 (2) | 2023.12.01 |
고랭 애플로그인 구현 (golang apple login) (0) | 2023.05.06 |