(최종 프로젝트 진행중)
어제에 이어 git rebase 방식에 대해 알아보는 시간을 가졌다. 어제치 TIL에 업데이트하였다. 이해도가 조금 더 쌓이면 팀원들과 공유해볼 생각이다. 깃헙으로 협업할 때 더 편리하게 작업할 수 있을 것 같아서.
Nest.js의 provider, controller, module에 관해 공식 문서를 빠르게 훑으며 정리해보았다. 지난번에 게시판 CRUD를 Nest.js로 만들면서 가졌던 의문점들을 상당수 해결할 수 있었어서 좋았다. 이것 하나에 시간을 너무 지체하였나 싶기도 하지만, 들인 시간과 심리적 안정감을 저울질해보았을 때 앞으로 프로젝트를 진행하는 데 있어서 옳았던 판단이라고 생각된다.
※ 이하는 스스로 공부하며 적어둔 노트이며 불확실한 내용이 있을 수 있습니다. 학습용으로 적합하지 않음을 유념해주세요. ※
[Nest.js] 기본 공부
[Nest.js] 기본 공부
Nest.js 공식 문서 (영문)
Nest.js 공식 문서 번역 (2챕터 까지밖에 없음)
Nest가 사용하는 3계층 : Controller(=consumer), Service(=provider), data
@Injectable()는 의존성 주입을 위한 데코레이터.
Nest에서 발생하는 제어 역전이란, 곧 Controller에 Service를 주입하는 것이란 뜻으로 쓰인다고 봐도 될 것 같다.
Node 기반 HTTP 프레임워크는 크게 Express, Koa, Fastify가 있는데 Nest에서는 그 중 Express(platform-express)를 기본으로 사용한다. (추가 설정 필요 없음)
[Nest.js] controller
[Nest.js] controller
: Routing 메커니즘을 통해 어떤 컨트롤러가 어떤 요청을 받아 처리할지를 판단하게 되는 계층.
: 어떤 클래스에 @Controller를 달고 @Get 등의 데코레이터를 사용하게 되면, Nest는 그로 인해 제공된 metadata를 이용해 라우팅 지도(routing map)를 그릴 수 있게 된다.
라우팅 지도(routing map): 요청 URL과 그를 받아 처리하는 컨트롤러들을 묶은 관계도.
Routing, 경로 나누기
API 엔드포인트
: 웹서비스에서 클라이언트가 서버의 API에 접근할 수 있는 URL
: HTTP 요청 메소드 + 라우트 경로(Route path)
// 예제3
import { Controller, Get, Param } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get('all')
findAll(): string {
return 'This action returns all cats';
}
@Get(':id')
findOne(@Param('id') id: number): string {
return 'This action returns one cats';
}
@Get('musical')
findMusical() {
return 'This action returns musical CATs';
}
}
⇒ Nest는 GET /cats/musical URL 요청을 findMusical() 메서드에 매핑한다.
⇒ Nest에서 라우트 경로(Route path)는 ‘컨트롤러 경로 + 핸들러 경로’로 이루어지게 된다.
Request object, 요청 객체의 특정 속성에 직접 접근하기
Nest는 @Res()
처럼 핸들러의 시그니쳐에 데코레이터를 추가하여 Nest에 주입하도록 지시하여 (Express나 Fastify) 요청 객체에 액세스할 수 있다. 세션이나 헤더 등 요청 세부 정보가 필요할 때 사용하면 된다.
import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';
@Controller('cats')
export class CatsController {
@Get()
findAll(@Req() request: Request): string {
return 'This action returns all cats';
}
}
(타입스크립트를 사용할 경우, 위의 예제처럼 express의 Request 타이핑을 위해서는 @types/express 패키지를 설치하면 타입 도움을 받을 수 있음 참고)
@nestjs/common 이 제공하는 Request 관련 데코레이터들:
Nest의 데코레이터 | Express(?)에서의 객체명 |
@Request(), @Req() | req |
@Response(), @Res() * | res |
@Next() | next |
@Session() | req.session |
@Param(key?: string) | req.params / req.params[key] |
@Body(key?: string) | req.body / req.body[key] |
@Query(key?: string) | req.query / req.query[key] |
@Headers(name?: string) | req.headers / req.headers[name] |
@Ip() | req.ip |
@HostParam() | req.hosts |
Resources, Nest가 제공하는 HTTP 메서드 데코레이터 목록:
@Get()
, @Post()
, @Put()
, @Delete()
, @Patch()
, @Options()
, and @Head()
.
추가로 위의 모든 메소드를 포함하는(담당하는) 엔드포인트 @All()
이 있다.
Route wildcards, 경로명에 사용할 수 있는 와일드카드
?
, +
, *
, and ()
를 사용하면 RegExp에서의 의미 그대로 사용할 수 있다.
@Get('ab*cd') // 'abcd', 'ab_cd', 'abecd', 'ab-cd', 'ab.cd' 모두 가능.
findAll() {
return 'This route uses a wildcard';
}
반대로 하이픈 ( -
)과 점 (.
)은 글자 그대로의 의미로 해석된다. (와일드카드 문자 X)
Status code, 상태 코드 반환
디폴트는 200, POST 요청인 경우는 201이다.
@HttpCode() 데코레이터를 핸들러 레벨에 붙이면 응답 코드를 바꿀 수 있다.
import { HttpCode } from '@nestjs/common'
@Post()
@HttpCode(204)
create() {
return 'This action adds a new cat';
}
(여기서부터 대강 정리. 더욱 부정확할 수 있다. 다시 한 번 살펴봐야 함)
Headers, 헤더
@Header() 데코레이터를 이용한 수작업 응답 헤더 반환:
@Post()
@Header('Cache-Control', 'none')
create() {
return 'This action adds a new cat';
}
Redirection, 경로 재할당하기
@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs(@Query('version') version) {
if (version && version === '5') {
return { url: 'https://docs.nestjs.com/v5/' };
}
}
Route parameters
@Get(':id')
findOne(@Param() params): string {
console.log(params.id);
return `This action returns a #${params.id} cat`;
}
혹은
@Get('/articles/:id')
async getArticleById(@Param('id') articleId: number) {
return await this.boardService.getArticleById(articleId);
}
⇒ @Params() 안에 특정 ‘값’을 넣으면 (’id’같이) 그 변수만 가리키게 되고, 아무 인자도 주지 않으면 params 전체 값들을 가지는 객체를 가리키는 매개변수가 되게 된다.
Sub-Domain Routing
@Controller 데코레이터는 들어온 HTTP 요청의 host부가 특정 값과 같은 경우에만 요청을 받도록 옵션을 줄 수 있다.
@Controller({ host: 'admin.example.com' })
export class AdminController {
@Get()
index(): string {
return 'Admin page';
}
}
이런 식으로 host부에 params를 적용시켜 따로 빼낼 수도 있음:
@Controller({ host: ':account.example.com' })
export class AccountController {
@Get()
getInfo(@HostParam('account') account: string) {
return account; // account.example.com 아마도...?
}
}
- 실제 반환값은 뭘까?
Scopes
모든 HTTP 요청이 개별로 처리되는 “the request/response Multi-Threaded Stateless Model”과 달리, Nest에서는 모든 요청이 ‘공유된다’… 그래서 싱글톤 인스턴스를 사용하는 게 안전함을 보장한다고.
Asynchronicity
Nest에서는 ‘유예된(deferred)’ 값을 리턴하는 async 함수도 작성 가능하다… 예를 들면 이런 것:
@Get()
async findAll(): Promise<any[]> {
return [];
}
Request payloads, 요청 바디 다루기
@Body() 데코레이터와 손수 정의하는 DTO(Data Transfer Object) 스키마 인터페이스/클래스를 이용해 HTTP 요청의 body 내용을 읽어올 수 있다.
// create-cat.dto.tsJS
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
// cats.controller.tsJS
@Post()
async create(@Body() data: CreateCatDto) {
return `This action adds a new cat, named ${data.name}`;
}
⇒ 여기서 main.ts에서의 ValidationPipe의 쓰임새가 등판한다.
// main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({ transform: true }));
await app.listen(3000);
}
bootstrap();
ValidationPipe: create() 같은 ‘메소드 핸들러’가 받는 속성들을 검열한다. 예를 들면 CreateCatDto에서 정의된 name
, age
, breed
속성은 ‘화이트리스트’에 속하게 되고, 이 화이트리스트에 속하지 않은 다른 모든 속성들은 반환 결과에 반영되지 않도록 한다.
받는 옵션으로 transform?: boolean, disableErrorMessages?: boolean 등이 있다…
Handling errors, 에러 핸들링
(다른 챕터에서 다룸)
Getting up and running, 컨트롤러 등록
CatsController 클래스를 만들어두었어도 그 자체로는 Nest가 얘가 존재하는지 인식할 수 없다.
controller들은 항상 @Module() 데코레이터에 속한다. 루트 모듈인 AppModule에 controller를 등록하는 예시는 다음과 같다:
// app.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
@Module({
controllers: [CatsController],
})
export class AppModule {}
⇒ AppModule이라는 자체 제작 클래스에, @Module() 데코레이터를 통해 metadata를 ‘붙여(attach)서’
- AppModule 클래스가 모듈 클래스로 인식되게 하였고
- 어떤 controller가 사용되어야 하는지 알 수 있게 되었다.
Library-specific approach
지금까지의 Nest 표준 응답 방식과 대비되는, Express나 Fastify 라이브러리 자체의 응답 객체를 사용하는 방식.
(줄임)
(참고)
Nest.js 영문 공식 문서
그 한글 번역본
[Nest.js] provider
[Nest.js] provider
= 서비스(Application) 계층 이라고 볼 수 있다.
: 의존성으로 주입할 수 있는 클래스.
: ‘비즈니스 로직들을 presentation 계층으로 공급한다’는 의미에서 ‘공급자’
- Service, Repository, Factory, Helper 등의 기본 클래스가 정의되어 있다.
- 커스텀 provider를 만들 수도 있다.
- @Injectable() 데코레이터를 달고 있다.
- Nest의 제어 역전 컨테이너(Nest IoC container)가 관리하는 대상. 의존 관계들을 알아서 ‘해석’하고, 생명주기를 관장하는 등.
‘Service’ 클래스가 대표적인 provider
@Injectable() 데코레이터를 달고 정의된다.
The@Injectable()
decorator attaches metadata, which declares thatCatsService
is a class that can be managed by the Nest IoC container.
의존성 주입을 할 때 생성자에 단순히 ‘타입’을 넘겨주기만 하면 알아서 새 인스턴스를 생성하고 반환해준다.
In Nest, thanks to TypeScript capabilities, it's extremely easy to manage dependencies because they are resolved just by type.
constructor(private catsService: CatsService) {}
Provider registration, 등록하기
provider(Service)와 consumer(Controller)를 다 만들었으면 app.module.ts에 controllers와 providers 항목에 등록해줌으로써 해당 ‘controller’가 이 ‘provider’ 클래스에 의존하고 있음을 Nest에게 알려줄 수 있다….
DTO와 Interface, Controller, Service가 있는 Cat 모듈의 폴더 구조는 다음과 같다:
Scopes, 생명주기
기본적으로 모든 providers의 생명은 앱과 시작과 끝을 같이한다. 즉 앱이 처음 실행될 때 모든 앱 내의 의존 관계(=딸린 providers)가 해석되어야 하고, 따라서 딸린 providers가 “생성(instantiated)”된다. 앱을 종료할 때는 모든 provider가 파괴된다.
물론 이와 다른 생명 길이를 적용해줄 수도 있다.
(참고: https://docs.nestjs.com/providers#scopes)
Custom providers, 손수 만드는 providers
기본 Providers (Services, Fatories 등) 클래스 외에도 사용자 정의 provider를 만들 수 있다. Nest의 제어 역전 컨테이너 (IoC container)가 provider 간의 의존 관계를 해석하는 게 그만큼 강력해서 가능하다.
Optional providers, 옵셔널로 사용하기
Provider를 갖다 쓰는 주체(=consumer = 서비스 객체)가 해당 provider를 제공받거나 받지 않겠다(대신 디폴트 값을 쓰겠다)고 구현할 수도 있다.
해당 provider를 주입받는 생성자 시그니처에 @Optional()를 달아주면 된다.
import { Injectable, Optional, Inject } from '@nestjs/common';
@Injectable()
export class HttpService<T> {
constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}
⇒ 이 ‘private httpClient: T’는 인수로 안 주어져도 괜찮다.
⇒ 이 인수 ‘private httpClient: T’는 ‘HTTP_OPTIONS’라는 또다른 provider를 주입받는다…
Property-based injection, 속성에 주입하기
‘속성 기반 주입’. 지금까지의 ‘생성자 기반 주입(constructor-based injection)’과 대비되는 개념.
…
타겟 속성에 @Inject() 데코레이터를 달아주면 된다.
import { Injectable, Inject } from '@nestjs/common';
@Injectable()
export class HttpService<T> {
@Inject('HTTP_OPTIONS')
private readonly httpClient: T;
}
(참고)
Nest.js 영문 공식 문서
그 한글 번역본
“Resolve” = “(변수명만으로는 알 수 없는)(문맥에 맞게) 해석한다”
“Resolve” = “(변수명만으로는 알 수 없는)(문맥에 맞게) 해석한다”
용례:
- Dependency injection 상황에서, “resolve an implementation to an interface”
- Package manager가 일할 때, “resolve packages dependencies”
- Web에 관련하여, “resolve a hostname”
- Nest.js 문서에서, “When the application is bootstrapped, every dependency must be resolved, and therefore every provider has to be instantiated.”
Similarly, resolving packages dependencies usually requires installing the missing packages.- it isn't enough to know the names of the missing components, you have to actually acquire their contents, and the contents are not predictable from the name alone.
⇒ 한 마디로 해당 패키지를 ‘설치하고 그 내용물을 풀어서 읽어들여야’ 해당 ‘이름’이 뭘 뜻하고 있는 건지 알 수 있을 때, ‘패키지 의존성을 푼다/해석한다’고 표현한다.
⇒ 한 마디로 어떤 코드(변수명)가 문맥에 의존적인 상황에 쓰임.
↔ 반대로 16진수를 10진수로 변환하거나 문자열을 전부 소문자로 변환한다거나 하는 경우엔 ‘resolve’한다고 하면 안되고 단순히 ‘transform’ 한다고 해야 한다.
이에 따라 위의 용례를 번역해보면:
- 의존성을 주입할 때 “인터페이스가 구현되는 것을 해석한다”
- 패키지 매니저가 일할 때 “패키지 각각에 딸린 의존 패키지들을 해석한다”
- 웹과 관련하여 “호스트명을 (문맥에 맞게) 해석한다”
- Nest.js 문서에서는, “애플리케이션이 실행될 때 모든 의존 관계가 (모두) 해석되어야 한다”
[Nest.js] module
[Nest.js] module
@Module()
데코레이터는 Nest가 애플리케이션 구조를 만들때 사용할 수 있는 메타데이터를 제공해주는 역할
Nest에서 모듈은 기본적으로 싱글톤이다. 따라서 어떤 provider든지 여러 모듈들에서 공유할 때 ‘같은 인스턴스’임이 보장된다.
애플리케이션 그래프
: Nest가 모듈과 프로바이더 간의 관계 및 종속성을 연결하기 위해 사용하는 내부 데이터 구조
어플리케이션이 커지면 컴포넌트를 분리해야하고, 컴포넌트를 구성하는 효과적인 방법으로 여러 개의 모듈을 사용하게 된다.
모듈의 기본 구조:
@Module({
imports: [TypeOrmModule.forFeature([Article])],
controllers: [BoardController],
providers: [BoardService, ArticleRepository],
})
export class BoardModule {}
⇒ 우선 @Module() 데코레이터 자체가 객체를 인자값으로 요구한다. 이 객체에 들어가는 속성은:
- controllers: Presentation 계층(=컨트롤러 계층) 집합. 이 모듈 안에서 정의된, (애플리케이션 실행시) 인스턴스화 되어야 하는 controller의 집합
- providers: Application 계층(=서비스 계층) 집합. Nest 인젝터(Injector: 의존성을 주입하는 Nest의 내부 모듈)가 인스턴스화시키고 적어도 이 모듈 안에선 공유될 provider의 집합
- imports: 해당 모듈에서 필요한 provider를 제공하는/노출하는(export) 모듈 집합. providers 항목에 어떤 provider를 넣고 싶을 때… 그 provider를 실제로 제공해줄 다른 모듈.
- 왜 최상단에 import … from으로 임포트해오지 않고 이렇게 하는 건지?
- exports: 해당 모듈이 밖으로 제공하는 providers의 부분 집합. 이 모듈을 사용하게 될 다른 모듈이 사용할 수 있도록 노출(export)할 provider 목록.
Feature modules, 기능/특징 모듈
: CatsController
와 CatsService
같이, 같은 도메인/기능/특징을 가지는 코드들을 모아 관계를 정립시킨 모듈 (CatsModule
)
// cats/cats.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
Shared modules, 공유 모듈
Nest에서 모듈은 기본적으로 싱글톤이다. 따라서 어떤 provider든지 여러 모듈들에서 공유할 때 ‘같은 인스턴스’임이 보장된다.
모든 모듈은 자동적으로 공유 모듈이다. 한 번 생성되면 다른 모듈들에서 사용될 수 있다.
예) cat 모듈의 CatsService 인스턴스를 다른 모듈에서도 쓰고 싶다고 할 때, cat 모듈에서 CatsService를 노출해주면(=exports 항목에 추가해주면) 된다.
// cats.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService]
})
export class CatsModule {}
이후로는 CatsModule을 임포트하는 어떤 다른 모듈이든 CatsService를 provider로 사용할 수 있고, 이 때의 CatsService는 모두 같은 인스턴스임이 보장된다.
// (내가 작성하는) some.module.ts 예시:
import { CatsModule } from './cats.module' // ? 이렇게 모듈 자체를 임포트 해오는 게 맞나?
@Module({
imports: [CatsModule],
providers: [CatsService],
})
export class SomeOtherModule {}
Module re-exporting, 모듈 재 노출
Dependency injection
Global modules, 전역 모듈
Dynamic modules, 동적 모듈
Uploaded by N2T