728x90

Module은 @Module() 데코레이터가 달린 클래스를 말한다.

이 데코레이터는 Nest가 애플리케이션 구조를 효과적으로 구성하고 관리 할 수 있도록 메타데이터를 제공한다.

 

모든 Nest 애플리케이션은 적어도 하나의 모듈을 가진다.

바로 application graph에서 출발점에 해당하는 루트 모듈이다.

이 그래프는 Nest가 관계와 의존관계를 해결하기 위해 사용하는 내부구조이다.

작은 애플리케이션은 하나의 모듈만 가질 수 있지만, 보통은 그렇지는 않다.

모듈은 구성 요소들을 조직할 때, 굉장히 추천되는 방식이다.

대부분의 애플리케이션에서, 기능들에 따라 밀접에서 관련되어 캡슐화된 멀티 모듈 구조를 가지게 될 것이다.

 

@Module() 데코레이터는 모듈을 명시한 하나의 싱글 오브젝트를 가진다.

providers Nest injector에 의해 인스턴스화 되거나, 모듈간에 공유되어야 하는 Provider들
controllers 해당 모듈에서 인스턴스화 되어야 하는 Controller들
imports 이 모듈에서 필요한 Provider를 내보내는 Module들
exports 이 모듈에 있는 Provider 중에 다른 모듈에서도 사용 할 수 있도록 내보내야 하는 Provider들

 

모듈은 기본적으로는 provider들을 캡슐화 한다.

이 말은 현재 모듈에서 사용하는 일부의 provider들을 주입받거나, 다른 모듈에서 명시해서 주입받아야 한다는 말이다.

 

  • Feature modules

우리의 예시에서 CatsController와 CatsService는 매우 밀접한 관계가 있었으며, 같은 애플리케이션 도메인에서 제공되었었다.

그러면 이것들을 기능 모듈에 그룹 지을 수 있을 것 같다.

기능 모듈은 특정한 기능들의 코드를 조직해서 편리하게 영역을 만들어준다.

이것은 SOLID원칙을 준수하며, 팀과 애플리케이션의 성장에 굉장히 중요하다.

 

cats를 모아서 cats module로 묶어 모듈로 분리해보자.

nest g module cats

이렇게 cats 모듈을 만들고, 이 곳으로 모든 코드들을 다 옮겨준다.

 

import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule {}

이렇게 AppModule에 CatsModule을 넣어준다.

 

이제 다음과 같은 구조가 될 것이다.

 

  • Shared modules

Nest에서 모듈들은 기본적으로 싱글톤이다.

그렇기에 같은 인스턴스를 멀티 모듈에서 쉽게 공유가 가능하다.

모든 모듈은 기본적으로는 공유 모듈이다.

모듈은 일단 생성되면 어느 모듈에서든 사용이 가능하다.

우리가 CatsService를 다른 모듈간에 공유한다고 상상해보자.

일단 CatsModule에서 해당 CatsService를 export 해야한다.

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  imports: [CatsModule],
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})
export class CatsModule {}

이제 CatsModule을 import한 모듈은 CatsService에 접근이 가능하며 해당 인스턴스를 공유한다는 것도 굉장히 중요한 포인트이다.

 

만약 직접 CatsService를 가져가서 등록하게 된다면, 물론 이것도 가능은 하다.

하지만 그렇게하면 각각의 모듈이 각각의 인스턴스를 만들며, 같은 서비스가 또 생성되기에 메모리가 낭비되고 만약 해당 서비스에 상태가 존재했다면 이런 상태가 일치하지 않는 상태 불일치 문제가 발생하기도 한다.

 

이것을 모듈에서 감싸고 export 하는 방법은 CatsModule을 import하는 모든 모듈들에 같은 인스턴스가 공유되는 것을 보장한다.

이것은 메모리의 낭비를 줄이고 같은 상태를 공유하기에 예상치 못한 에러를 줄일 수 있다.

이런 서비스를 효율적으로 애플리케이션 간에 공유하는 것은 모듈화와 DI가 가능한 프레임워크의 핵심적인 기능이다.

 

  • Module re-exporting

모듈은 자신의 내부 provider를 export 할 수 있다.

그리고 추가적으로 자신이 import 했던 모듈을 다시 export 할 수 있다.

아래처럼 CommonModule은 CoreModule에게 import, export 되고 있으며, 이것은 다른 모듈이 이거 하나로 다 import 될 수 있도록 만든다.

@Module({
  imports: [CommonModule],
  exports: [CommonModule],
})
export class CoreModule {}

 

 

  • Dependency injection

모듈 클래스는 provider를 주입 받을 수 있다.

@Module({
  imports: [CatsModule],
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})
export class CatsModule {
	constructor(private catsService: CatsService){}
}

그러나 순환참조 문제 때문에 모듈 자체에서 provider를 주입받을 수는 없다.

 

  • Global modules

만약 어디에서 항상 사용하는 모듈이 있다면, 그것을 모두 추가해주기는 힘들것이다.

Nest에서 전역으로 제공되는 provider를 만들고 싶다면, @Global 데코레이터로 글로벌 모듈로 만드는 것이 좋다.

@Global()
@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})
export class CatsModule {}

@Global() 데코레이터는 모듈을 전역으로 만든다.

글로벌 모듈은 한 번만 등록하며, 보통 루트 혹은 코어 모듈에서 한다.

 

  • Dynamic modules

Nest에서 다이나믹 모듈은 런타임에 생성되도록 만들어준다.

만약 유연한 설계가 필요한 개발자라면 굉장히 유용한 기능이다.

특정한 옵션에 따라 모듈을 커스텀 할 수 있기 때문이다.

@Module({
  providers: [Connection],
  exports: [Connection],
})
export class DatabaseModule {
  static forRoot(entities = [], options?): DynamicModule {
    const providers = createDatabaseProviders(options, entities);
    return {
      module: DatabaseModule,
      providers: providers,
      exports: providers,
    };
  }
}

'Node > Nest 공식문서' 카테고리의 다른 글

Providers  (0) 2025.08.21
Controllers  (0) 2025.08.20
First steps  (2) 2025.08.18
Introduction  (4) 2025.08.18
728x90

Prodiver는 Nest의 가장 핵심적인 개념이다.

Service, Repository, Factory, Hepler와 같은 Nest의 기본 클래스들이 모두 Provider로 다루어진다.

Provider의 핵심적인 개념은 각각의 형태로 관계를 가지고, 의존성에 맞도록 주입되는 것이다.

이런 객체들을 연결해주는 것은 Nest runtime system의 큰 과제이다.

 

이전 장에서 CatsController를 만들었던 것을 생각해보자.

컨트롤러는 단순히 Http 요청을 처리하고, 복잡한 작업은 Provider에게 위임해야 한다.

여기서 Provider는 Nest에서 providers로 선언된 자바스크립트 클래스를 말한다.

 

  • Services

이번에는 예시로 CatsService를 만들어보자.

해당 서비스는 데이터의 저장과 조회를 담당하며, CatsController에서 사용할 예정이다.

이런 애플리케이션 로직을 다루기 때문에, provider 모듈에서 다뤄지게 되는 것이다.

 

import { Cat } from './interfaces/cat.interface';
import { Injectable } from '@nestjs/common';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];
  create(cat: Cat) {
    this.cats.push(cat);
  }
  findAll(): Cat[] {
    return this.cats;
  }
}

CatsService는 하나의 멤버 변수와 두개의 메서드를 가지고 있다.

여기서 가장 중요한 포인트는 @Injectable() 데코레이터이다.

해당 데코레이터는 클래스에 메타데이터를 추가해서, CatsService가 Nest의 IoC 컨테이너에 의해 관리될 수 있도록 한다.

 

이 CatsService는 클래스의 생성자를 통해 주입되게 된다.

import {
  Controller,
  Body,
  Get,
  Post,
  Put,
  Delete,
  Param,
  Query,
} from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';

@Controller('/cats')
export class CatsController {
  constructor(private readonly catService: CatsService) {}
  @Post()
  create(@Body() createCatDto: CreateCatDto): string {
    console.log(createCatDto);
    return 'This action adds a new cat';
  }
  @Get()
  findAll(@Query('age') age: number, @Query('breed') breed: string): string {
    return `This action returns all cats filtered by age: ${age} and breed: ${breed}`;
  }
  @Get(':id')
  findOne(@Param('id') id: string) {
    return `This action returns a #${id} cat`;
  }
  @Put(':id')
  update(@Param('id') id: string, @Body() createCatDto: CreateCatDto) {
    return `This action updates a #${id} cat ${createCatDto.name}`;
  }
  @Delete(':id')
  remove(@Param('id') id: string) {
    return `This action removes a #${id} cat`;
  }
}

 

  • Dependency injection

Nest는 의존성 주입이라고 많이 알려진 강력한 디자인 패턴을 통해 빌드된다.

이것에 대해서는 Angular의 문서를 참고하면 좋다고 한다.

https://angular.dev/guide/di

 

Angular

The web development framework for building modern apps.

angular.dev

TypeScript를 활용하기에 Nest에서는 타입을 통해 이러한 의존성들이 관리될 수 있다.

나중에 저 Angular의 문서를 읽고서도 블로그를 써보도록 해야겠다.

 

  • Scopes

provider는 생명주기를 가지게 되며, 그 생명주기는 보통 애플리케이션의 생명주기와 동행한다.

애플리케이션이 부트스트랩되면, 모든 의존성들은 해결되어야 할 것이며 모든 provider가 instantiated되어야 한다는 것이다.

비슷하게 애플리케이션이 종료된다면, 이런 Provider는 모두 같이 종료되어야 한다.

그러나 이런 Provider를 범위를 지정해서 넣도록 할 수 있다.

그럴 때는 이 생명 주기가 애플리케이션이 되는 것이 아니라, 특정 요청에 대해 묶이게 되는 것이다.

나중에 Injection Scope에서 더 자세히 알아보도록 하자.

 

  • Optional providers

가끔은 항상 해결할 필요가 없는 의존성이 있을 수도 있다.

예를 들어, 특별한 구성 요소에 의존할 수 있지만, 해당 객체가 제공되지 않는 경우 기본값을 사용하도록 할 수 있다.

이런 경우 해당 의존성은 선택적으로 간주되며, 설정을 따로 해주지 않더라도 에러가 발생해서는 안된다.

 

공급자를 선택적으로 표시하기 위해서는, 생성자에 @Optional() 데코레이터를 사용하면 된다.

import { Injectable, Optional, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> {
  constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}

여기 예시에서는 custom provider를 사용하기에 HTTP_OPTIONS라는 커스텀 토큰을 포함하고 있다.

여기서는 의존성을 생성자의 클래스로 지정했다.

나중에 해당 챕터에서 더 자세히 알아보자.

 

  • Provider registration

이번에 CatsService라는 Provider와 CatsController라는 Consumer를 정의했었다.

우리는 이것들을 Nest에 등록해서 주입과정에서 다룰 수 있도록 해야한다.

등록은 app.module.ts를 수정해서 진행해야 하며, providers에 service를 넣어주면 된다.

import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class AppModule {}

 

'Node > Nest 공식문서' 카테고리의 다른 글

Modules  (0) 2025.08.23
Controllers  (0) 2025.08.20
First steps  (2) 2025.08.18
Introduction  (4) 2025.08.18
728x90

컨트롤러는 사용자로부터 오는 요청을 받고 다시 응답을 돌려주는 것에 대한 책임이 있다.

 

컨트롤러는 사용자로부터 온 특정한 요청을 어디로 보낼 것인지에 대한 목적으로 만들어졌다. 라우팅 메커니즘은 어떤 컨트롤러가 각각의 요청을 받을 것인지를 결정한다. 종종, 컨트롤러는 여러개의 라우트를 가지며, 그 각각의 라우트는 다른 동작을 수행한다.

 

컨트롤러를 만들기 위해서, 우리는 클래스와 데코레이터를 사용한다. 데코레이터는 해당 클래스의 메타 정보를 통해, Nest가 일치하는 응답을 라우팅 해 줄 수 있도록 도와준다.

 

  • Routing

예시에서 우리는 기본적인 컨트롤러를 만들기 위해 필요한 @Controller() 데코레이터를 사용할 것이다. cats라는 접두사를 가진 경로를 명시하며, @Controller에 접두사를 작성하는 것은 이후에 추가적으로 해당 경로를 더 명시할 필요없이 그룹화 할 수 있도록 도와준다. 예를 들어 우리가 Cats와 상호작용하는 라우트를 만들고 싶다면, 해당 경로의 접두사는 /cats가 될 것이며, @Controller('cats')로 작성하면 이 경로들을 그룹화 해주는 것이다. 그렇기에 컨트롤러에 추가할 라우트들에 'cats를 반복해서 작성 할 필요가 없다.

 

import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

 

@Get()이라는 Http 요청 데코레이터는 findAll()이라는 메서드 이름 앞에 위치하며, Nest가 특정 경로에 대한 라우트를 생성하도록 한다. 이제 이 route path는 controller에 이미 명시해둔 경로와 메서드에 작성한 데코레이터의 경로를 조합하여 만들어진다. 여기에서는 @Get()에 경로 명시가 없기 때문에 /cats로 들어오는 @Get() 요청에 대해 동작하는 것이다.

 

위의 예시에서 해당 경로로 Get 요청이 들어온다면, Nest는 라우트하여 findAll() 메서드를 호출한다. 여기서 메서드의 이름은 임시이며, Nest는 이 컨트롤러의 메서드 이름에 의미를 부여하지 않는다.

 

해당 메서드는 아마 200 status code를 리턴할 것이다, 저기 작성해둔 문자열과 함께 말이다. 이것에 대해 설명하기 위해서는 2가지의 응답을 바꾸는 방법에 대해 알아야 한다.

 

standard
(recommended)
해당 내장 메서드를 이용하면, Javascript가 객체 혹은 배열을 리턴 할 때, Json으로 자동 변환되어 돌려주게 된다. 하지만 만약 Javascript의 원시타입(string, number..)만 응답한다면, 직렬화를 시도하지 않는다. 
Library-specific 만약 @Res()로 사용하는 response object와 관련된 라이브러리를 사용할 수 있다. 이것을 사용하면, 개발자는 response.status(200).send()과 같이 작성해서 객체를 응답 할 수 있다.

 

  • Request object

핸들러는 종종 클라이언트에서 보낸 요청의 세부 정보를 알아와야 할 때가 있다. Nest는 이런 요청 객체에 대한 접근을 허용한다. 개발자는 @Req() 데코레이터를 사용하여 사용자가 보낸 요청의 객체에 접근이 가능하다.

 

import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(@Req() request: Request): string {
    console.log(request);
    return 'This action returns all cats';
  }
}

 

위와 같은 코드로 request를 출력해보니, 

이런식으로 긴데이터가 들어왔던 것을 볼 수 있었다.

이렇게 많은 데이터에서 원하는 속성을 찾아서 가져오기는 힘들 것 같고, @Body()와 @Query()같은 전용 데코레이터가 있기에 이것들을 사용해서 객체에 접근하게 될 것이다.

 

아래는 각각의 데코레이터들이 어떤것과 대응되는지를 보여주는 표이다.

@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

 

만약 @Res()나 @Response()를 사용하면, Nest에서 해당 핸들러가 응답까지 알아서 처리한다고 생각하고 끝까지 처리해주지 않는다.

그렇기에 해당 데코레이터를 사용하면 응답까지 개발자가 완성해서 리턴해줘야 한다.

 

  • Resources

이전에 cats의 GET API를 만들어보았다. 이번에는 POST를 통해 새로운 핸들러를 만들어보도록 하자.

import { Controller, Get, Post } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
  @Post()
  create(): string {
    return 'This action adds a new cat';
  }
}

굉장히 간단하며, Nest는 표준의 Http 메서드를 데코레이터를 통해 모두 지원한다.

만약 모든 메서드를 지원하는 엔드포인트를 만들고 싶다면 @All() 데코레이터를 사용하면 된다.

 

  • Route wildcards

패턴을 통해서 라우트하는 기능도 Nest는 지원한다. *을 사용하면 와일드카드로 해당 위치에는 어떤 조합의 경로도 라우트해준다. 

아래의 예시처럼 사용하면 seungkyu/ 뒤의 경로로 어떤 문자열이 오더라도 해당 핸들러로 라우팅해준다.

@Get('abcd/*')
findAll(){
	return 'This route uses a wildcard';
}

 

 

  • Status code

기본적인 응답 코드는 항상 200이고, POST 메서드의 응답코드는 항상 201이다.

이거를 @HttpCode()를 통해 핸들러 레벨에서 쉽게 바꿀 수 있다.

@Post()
@HttpCode(204)
create(){
	return 'This action adds a new cat';
}

 

물론 중간에 에러가 발생하면 바뀌기도 하며, @Res() 혹은 @Response()를 사용해서 직접 넘길 때도 바뀌기도 한다.

 

  • Response headers

response의 header를 직접 명시하기 위해, @Header() 데코레이터를 사용할 수 있다.

물론 res.header()를 통해서 직접 접근도 가능은 하다.

@Post()
@Header('Cache-Control', 'no-store')
create(){
	return 'This action adds a new cat';
}

 

  • Redirection

특정 주소로 리다이렉트 하고 싶다면, @Redirect() 데코레이터에 url과 statusCode를 명시해주면 된다.

만약 statusCode를 명시하지 않는다면, statusCode의 기본값은 302이다.

@Get()
@Redirect('https://nestjs.com', 301)

 

  • Route parameters

정적인 경로만으로 요청하면, 원하는 동적 데이터들을 받을 수 없다.

만약 id와 같은 값을 동적으로 받아오고 싶다면, 파라미터 토큰을 넣어서 가져올 수 있다.

아래 @Get() 데코레이터 예제의 라우트 매개변수 토큰이 바로 그 방식이다.

라우트에 정의한 파리미터는 메서드에 @Param() 데코레이터를 통해 접근 할 수 있다.

@Get(':id')
findOne(@Param() params: any): string{
	console.log(params.id);
    return `This action returns a #${params.id} cat`;
}

 

@Param() 데코레이터는 메서드의 파라미터 데코레이터며, 경로 파라미터에 정의된 값을 메서드가 사용 할 수 있도록 해준다.

위에서는 id에 접근하기 위해 params.id를 사용했지만, 라우트 파라미터의 이름에 직접 접근하여 바로 값을 가져올 수도 있다.

 

@Get(':id')
findOne(@Param('id') id: string): string{
	return `This action returns a #${id} cat`;
}

 

  • Sub-domain routing

@Controller() 데코레이터는 host 옵션을 받을 수 있다.

host 옵션은 해당 주소에서 온 HTTP 요청만 처리하겠다는 의미이다.

@Controller({host: 'admin.example.com'})
export class AdminController{
	@Get()
    index(): string {
    	return 'Admin page';
    }
}

 

여기서도 주소에서 동적인 값을 찾아서 올 수 있다.

host에서 선언된 매개변수는 @HostParam() 데코레이터를 추가해서 접근 가능합니다.

@Controller({host: ':account.example.com'})
export class AccountController{
	@Get()
   	getInfo(@HostParam('account') account: string) {
    	return account;
    }
}

 

  • Asynchronicity

자바스크립트의 기능을 최대한 활용하기 위해, 비동기 데이터 핸들링을 사용한다.

모든 async 함수들은 해당 데이터를 다루며 자동으로 값을 반환하는 Promise 타입을 반환한다.

@Get()
async findAll(): Promise<any[]>{
	return [];
}

 

해당 코드도 충분히 멋지지만, Nest는 RxJs를 통한 observable stream도 지원한다.

Nest가 해당 데이터를 구독하며, 내부적으로 알아서 값을 반환하게 된다.

@Get()
findAll(): Observable<any[]>{
	return of([]);
}

 

  • Request payloads

이전에서 했던 POST는 사용자가 보내는 파라미터들을 받지 않았었다.

그것을 @Body()로 해결해보자.

 

우선 사용하기 전에 DTO를 정의해보자.

DTO는 네트워크를 통해 데이터를 전송할 때, 어떻게 보내야할지 명시하는 객체이다.

Nest에서는 인터페이스 혹은 클래스를 사용해 DTO 스키마를 정의한다.

하지만 이 중에서도 class로 정의하는 것을 추천한다.

클래스는 ES6 자바스크립트 문법이기에 javascript 자체로 컴파일이 가능하다.

그에 비해, 인터페이스는 변환 과정에서 삭제되기 때문에 Nest는 런타임 중에 해당 인터페이스를 참조 할 수 없다.

이것은 파이프같은 기능들이 런타임 중 변수의 타입에 접근해야 하기에 중요하다.

 

export class CreateCatDto{
	name: string;
    age: number;
    breed: string;
}

 

이제 CatsController에 넣어보자.

 

import { Body, Controller, Get, Post } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
  @Post()
  create(@Body() createCatDto: CreateCatDto): string {
    console.log(createCatDto);
    return 'This action adds a new cat';
  }
}

 

이렇게 @Body() 데코레이터를 통해, POST에서 사용자가 보낸 데이터를 사용할 수 있다.

 

  • Query parameters

@Query()를 사용해 요청에서 들어온 쿼리 파라미터를 가져올 수 있다.

  @Get()
  findAll(@Query() age: number, @Query() breed: string): string {
    return `This action returns all cats filtered by age: ${age} and breed: ${breed}`;
  }

 

이런 핸들러에 다음과 같은 요청을 보내면

http://localhost:3000/cats?age=2&breed=thief

 

이렇게 응답이 오는 것을 볼 수 있다.

 

  • Getting up and running

CatsController를 완벽하게 다 작성했어도, 동작이 바로 되는 것은 아니다.

컨트롤러는 모듈의 일부이며, 우리는 @Module 데코레이터에 이 컨트롤러를 넣어줘야 한다.

import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';

@Module({
  controllers: [CatsController],
})
export class AppModule{}

'Node > Nest 공식문서' 카테고리의 다른 글

Modules  (0) 2025.08.23
Providers  (0) 2025.08.21
First steps  (2) 2025.08.18
Introduction  (4) 2025.08.18
728x90

nest를 빠르게 이해하기 위해 CRUD를 만들어보도록 하겠다.

 

  • Language

TypeScript를 사용하지만, Node.js도 사용이 가능하다. 만약 Javascript를 사용하고 싶다면 Babel 컴파일러를 사용하면 된다.

 

  • 요구사항

Nest로 개발하기 위해서는 Node.js 20버전 이상으로 설치가 되어 있어야 개발이 가능하다.

 

  • 설정

npm 전역에 nest를 설치하고, nest로 프로젝트를 만들면 된다.

npm i -g @nestjs/cli
nest new {project-name}

 

이렇게 생성을 하면 다음과 같이 파일들이 생기게 될 것이다.

 

이거에 대한 간략한 설명들을 해보자면

app.controller.ts 싱글 라우트를 가지고 있는 기본 컨트롤러
app.controller.spec.ts 컨트롤러의 유닛 테스트
app.module.ts 애플리케이션의 가장 기본 모듈
app.service.ts 하나의 메서드를 가지고 있는 기본 서비스
main.ts Nest application을 생성해주는 엔트리 파일

 

main.ts는 다음과 같이 작성되어 있다.

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

 

Nest 인스턴스를 만들기 위해서는 저 NestFactory.create를 사용해야 한다.

 

NestFactory.create()는 다음과 같이 INestApplication 인터페이스를 반환한다. 이 인터페이스는 여러가지 메서드들을 제공하며, 이 main.ts에서는 그냥 단순히 Http Listener를 만들어주는 것이다.

 

이제 이 nest의 개발 방법에 따라 개발자가 기능을 확장해나가면 된다.

 

  • 플렛폼

Nest는 플랫폼에 종속되지 않는 것에 목적을 두었다. 플랫폼에서 독립적이라면 비즈니스 로직을 다른 타입의 애플리케이션 간에 재사용 할 수 있으니 말이다. 기술적으로 Nest는 Node의 웹 프레임워크에서나 사용가능하다. 여기 2개의 바로 사용가능한 플랫폼이 존재한다. 바로 express와 fastify다.

platform-express express는 가장 잘 알려진 경량화 node 웹 프레임워크다. 실무에서 이미 검증된 프레임워크로 커뮤니티에서 많은 리소스들을 재공한다. @nestjs/platform-express는 기본적으로 platform-express를 사용하며, 이 플랫폼을 사용하기 위해서는 별도의 조치는 필요없다.
platform-fastify Fastify는 높은 성능과 적은 오버헤드에 초점을 둔 프레임워크이다.

 

어떤 프레임워크를 사용하더라도 이렇게 상속받아서 nest를 동작할 수 있다.

 

  • 애플리케이션 실행

설치가 완료되었다면, 아래와 같은 명령어를 입력하여 들어오는 Http 요청을 받을 수 있다.

npm run start

 

그러면 이렇게 다음과 같은 로그가 나온다.

 

그리고 main.ts에 명시되어있는 포트에 요청하면

이렇게 응답이 오는 것을 볼 수 있다.

 

만약 개발 과정 중에 있다면

npm run start:dev

이 명령어를 통해 자동으로 변경되는 파일을 재컴파일 해서 서버를 재실행 할 수 있다.

 

 

'Node > Nest 공식문서' 카테고리의 다른 글

Modules  (0) 2025.08.23
Providers  (0) 2025.08.21
Controllers  (0) 2025.08.20
Introduction  (4) 2025.08.18
728x90

https://docs.nestjs.com/

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

 

 

  • Introduction

Nest는 node.js 서버를 더 효율적이고 확장가능하도록 돕기 위한 프레임워크이다. Nest는 최신 자바스크립트 문법과 타입스크립트를 완벽하게 지원하며, 이를 통해 객체지향프로그래밍, 함수형 프로그래밍, 반응형 프로그래밍을 사용하여 개발할 수 있다.

 

Nest는 내부적으로 Express 위에서 동작하지만, 선택적으로 Fastify에서도 동작이 가능하다.

 

Nest는 node.js 프레임워크에서 직접 라우터를 짜지 않아도, 상위 추상화 개념을 이용하여 개발 할 수 있도록 한다.

 

  • Philosophy

최근 몇 년 동안 Node.js 덕분에 Javascript는 웹의 프론트와 백에서 공용어가 되었습니다. 하지만 Angular, React, Vue와 같이 훌륭한 프론트엔드 애플리케이션이 등장했지만, 서버에서의 Node.js는 아키텍처라는 근본적인 문제를 효과적으로 해결하지는 못했습니다.

 

Nest는 개발자가 효과적으로 테스트하고, 확장가능하고, 느슨하게 결합하고, 애플리케이션을 쉽게 유지보수 할 수 있도록 즉시 사용가능한 애플리케이션을 제공한다.

 

  • Installation

시작을 하기 위해서는 Nest cli 혹은, starter project를 clone 하는 방법으로 기본 틀을 가져올 수 있다.

 

Nest cli 방법을 사용하기 위해서는 다음의 명령어를 따라가면 된다. 이것은 새로운 디렉토리를 만들며, 해당 디렉토리를 Nest 파일과 모듈로 채우게 된다.

 

npm i -g @nestjs/cli
nest new {project-name}

 

이렇게 틀을 받아오고, 

npm run start

로 서버를 실행해서 http://localhost:3000/ 에 접속하면 페이지가 보일것이다.

 

'Node > Nest 공식문서' 카테고리의 다른 글

Modules  (0) 2025.08.23
Providers  (0) 2025.08.21
Controllers  (0) 2025.08.20
First steps  (2) 2025.08.18
728x90

어떤 관계들이 존재하는지에 대해서는 생략하고, 바로 OneToMany와 ManyToOne의 관계에 대해서 알아보자.

 

여기서는 사용자와 해당 사용자가 작성하는 보고서에 대한 관계로 설명을 해보려고 한다.

 

우선 OneToMany 관계이다.

한명의 사용자가 많은 양의 보고서를 작성할거기에 One이 사용자, Many가 보고서이다.

따라서 OneToMany는 User 쪽에 작성하는 데코레이션이다.

 

    @OneToMany(() => Report, (report) => report.users)
    reports: Report[];

 

@OneToMany로 2가지의 파라미터를 넘긴다.

우선 첫번째 파라미터는 Many 쪽의 타입이다.

Many로 가져올 타입을 명시하며, () => Report 처럼 함수형으로 반환하는 이유는 곧 보겠지만

Report 쪽에도 이렇게 Users 타입을 주게 되는데, 이 때 두 Entity 간에 순환 참조 에러가 발생하게 된다.

그렇기에 함수의 리턴타입으로 명시해주는 것이다.

 

그리고 2번째 파라미터는, Many 쪽에서 해당 Entity를 가지고 있는 property다.

지금은 하나밖에 없어서 그냥 users로 명시하고 있지만

 

만약 updateUser가 추가된다면

    @OneToMany(() => Report, (report) => report.updatedUser)
    updateReports: Report[];

이렇게 updateUser와 같은 속성을 명시해주는 것이다.

 

이런 이유로 OneToMany에도 2가지의 파라미터를 더 넘기게 된다.

 

이제 Many쪽으로 넘어가보자.

    @ManyToOne(() => Users, (Users) => Users.reports)
    users: Users

Many쪽도 다음과 같은 이유로 2개의 파라미터를 작성한다.

 

그리고 이렇게 작성을 하면 Many 쪽에는

다음과 같이 user쪽의 PK를 가지는 column이 추가된다.

 

근데 여기서 다음과 같은 문제가 발생한다.

우선 지금과 같이 작성해서는

    @Get("/check")
    @UseGuards(AuthGuard)
    async checkLogin(@Authentication() authentication: Users): Promise<Users>{
        console.log(authentication.reports)
        return authentication
    }

해당 코드에서 undefined가 출력되게 된다.

OneToMany 관계에서 Many를 불러오지 못하는 것이다.

 

불러오기 위해서는 다음과 같이 eager:true를 설정해줘야 한다.

@OneToMany(() => Report, (report) => report.users, {
        eager: true
    })
    reports: Report[];

이러면 출력은 된다.

 

하지만 또 문제가 발생한다.

이러면 로그인만 하더라도 모든 report까지 조회하게 된다.

로그인을 하면 user entity를 조회하게 될텐데, 그 때마다 report를 조회한다면 큰 오버헤드 일 것이다.

 

그렇기에 필요할 때만 가져오도록 만들어야 한다.

그러기 위해서는 해당 property를 다음과 같이 수정한다.

    @OneToMany(() => Report, (report) => report.users, {
        lazy: true
    })
    reports: Promise<Report[]>;

이렇게 타입을 Promise로 설정하고, lazy를 true로 하면 해당 property를 사용하려고 할 때 다시 쿼리를 날려서 report를 조회해온다.

 

테스트로 다음과 같이 코드를 작성하고 실행하면

    @Get("/check")
    @UseGuards(AuthGuard)
    async checkLogin(@Authentication() authentication: Users): Promise<Users>{
        console.log("before query")

        console.log(await authentication.reports)
        return authentication
    }

 

로그가 출력되고 그 다음에 다시 쿼리를 날리는 것을 볼 수 있다.

 

이 외에도 다양한 방법의 관계에서의 설정들이 있을텐데, 꾸준하게 nest를 공부하면서 알아보도록 하자.

728x90

기존에는 데이터베이스와 관련된 부분을 그냥 코드로 작성했었다.

 

이런 부분을 당연히 그대로 git에 올리면 안되기 때문에 환경변수로 만들어서 주입해야 한다.

 

우선 환경변수를 .env 파일로부터 가져오기 위해 다음 의존성들을 추가해준다.

 

그리고는 .env.{환경}으로 환경변수를 저장할 파일을 만들어준다.

 

당연히 올리면 안되기에 gitignore에 추가해준다.

 

그리고 데이터베이스는 같게 하더라도 스키마는 다르게 해주었다.

DB_HOST=localhost
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=1204
DB_DATABASE=usedCars_dev

.env.development

 

DB_HOST=localhost
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=1204
DB_DATABASE=usedCars_test

.env.test

 

테스트에서 다른 데이터베이스를 사용하기 위해 구별해주었다.

 

이제 app.module.ts로 가서 해당 환경변수들을 주입해줘야 한다.

@Module({
  imports: [
      ConfigModule.forRoot({
        isGlobal: true,
        envFilePath: `.env.${process.env.NODE_ENV}`
      })
   ]
})
export class AppModule {}

우선 ConfigModule.forRoot()로 환경변수를 읽어올 파일을 지정해준다.

 

여기에서 .env의 뒷 부분을 모듈 런타임의 환경변수로 지정해서 development, test를 구별해주었다.

 

그 다음에는 기존에 사용하던 TypeOrmModule을 수정해줘야 한다.

forRoot가 아니라 forRootAsync로 가져와야 한다.

forRoot는 정적이거나 process.env로 접근할 때 사용하며

forRootAsync는 동적으로 config 파일을 가져올 때 사용한다고 한다.

 

      TypeOrmModule.forRootAsync({
        inject: [ConfigService],
        useFactory: (config: ConfigService) => {
          return {
            type: "mysql",
            host: config.get<string>('DB_HOST'),
            port: config.get<number>('DB_PORT'),
            username: config.get<string>('DB_USERNAME'),
            password: config.get<string>('DB_PASSWORD'),
            database: config.get<string>('DB_DATABASE'),
            entities: [__dirname + '/**/*.entity{.ts,.js}'],
            synchronize: true
          }
        }
      }),

inject를 통해서 환경변수를 가져올 서비스를 지정해준다.

 

이제는 해당 환경에 대한 정보를 서버 시작 전에 넘겨줘야 한다.

이 쪽 부분에 해당하는 내용을 말이다.

 

현재 npm을 통해서 서버를 실행하고 있으니

package.json의 이쪽 부분을 수정해주면 된다.

 

우리는 런타임 환경을 부여하기 위해 cross-env 라이브러리를 설치했었다.

우리는 NODE_ENV를 줘야 하기에 NODE_ENV={원하는 값}을 주입해준다.

 

한 번 실행해서 원하는 데이터베이스에 값이 생기는지 확인해보자.

이렇게 설정하고 create-user.http를 실행하니 데이터베이스에 값이 원하는대로 들어가는 것을 볼 수 있었다.

 

728x90

자 이제 로그인을 하면 세션에 사용자 정보를 넣어두고, 다른 API를 요청하면 그 사용자 정보를 가져오도록 해보자.

 

우선 사용자가 로그인을 하면 세션에 사용자의 id를 넣어줘야 한다.

    @Post("/signIn")
    async signIn(@Body() loginUserDto: LoginUserDto, @Session() session: any): Promise<Users>{
        const user = await this.authService.signIn(loginUserDto)

        session.userId = user.id;

        return user;
    }

이렇게 user의 id를 먼저 넣어준다.

 

그리고 로그인을 해보면, 우선 다음과 같이 쿠키가 생긴 것을 볼 수 있다.

 

이제 이 정보를 다른 API에서 불러와야 한다.

 

일단 로그인 체크 API를 만들고 거기에서 사용자의 아이디를 콘솔로 출력해보자.

    @Get("/check")
    async checkLogin(@Session() session: any): Promise<string>{
        console.log(`userId: ${session.userId}`);
        return ""
    }

일단 사용자의 아이디가 출력되었다.

 

사용자 로그아웃도 구현해보자.

 

그냥 userId를 null로 바꿔주면 끝이다.

    @Post("/signOut")
    async signOut(@Session() session: any): Promise<void>{
        session.userId = null;
    }

로그아웃 API를 요청하고 다시 check 해보면 userId가 null로 출력되고 있었다.

 

이렇게 만들었지만, 항상 컨트롤러에서 세션을 통해 userId를 가져올 수는 없을 것이다.

 

로그인을 하지 않았다면 해당 경로의 접근을 막고, 데코레이터를 통해 사용자의 정보를 가져오도록 수정해보자.

@Authentication이라는 데코레이션으로 User의 정보를 가져올 수 있도록 만들어보자.

 

export const Authentication = createParamDecorator(
    (data: any, context: ExecutionContext) => {
        
    }
)

우선 다음과 같은 방법으로 데코레이터를 작성한다.

 

여기서 data는 데코레이터에 넘기로 넘기는 값, ExecutionContext는 NestJs에서 제공하는 Http요청이나 Websocket등 컨텍스트의 실행 정보를 담고 있다고 한다.

 

getType()을 통해 연결종류로 'http', 'rpc', 'ws' 중 하나를 반환하며, 우리는 이 중 http를 사용하기에 switchToHttp()를 통해서 http context를 반환한다.

 

그리고 해당 데코레이터에서 반환환 값은 

여기에서 받아서 사용한다.

 

        const httpRequest = context.switchToHttp().getRequest();

        const userId = httpRequest.session.userId;

이렇게 다음과 같은 방법으로 http로 변환하고 거기서 session을 가져와서 userId를 추출할 수 있다.

하지만 우리는 이 userId를 바탕으로 사용자 정보를 가져오고 싶다.

그렇기에 UserService가 필요하다.

하지만 이 UserService는 그냥 가져오는 것이 아니라, 의존성을 통해 주입받아야 한다.

그렇기에 여기서 우리는 의존성을 주입받을 수 있는 인터셉터를 사용해야 한다.

 

인터셉터를 통해 Session을 읽고 사용자를 조회한다.

그리고 그 조회한 사용자를 데코레이터를 통해 읽어오도록 만드는 것이다.

 

다시 인터셉터부터 만들어보자.

@Injectable()
export class ExtractUserInterceptor implements NestInterceptor {

    constructor(private readonly usersService: UsersService) {}

    async intercept(context: ExecutionContext, next: CallHandler<any>): Promise<Observable<any>> {

        //HTTP Context를 가져온다
        const request = context.switchToHttp().getRequest();

        //session에서 userId를 가져온다.
        const {userId}: {userId: number} = request.session || {};

        //userId가 없으면 인증에러를 반환한다.
        if(!userId) {
            throw new UnauthorizedException();
        }

        //User 정보를 데코레이터에서 가져올 수 있도록 context에 넣기
        request.curUser = await this.usersService.findOne(userId);

        return next.handle()
    }
}

우선 이렇게 만들어두고, 의존성을 주입받을 수 있도록 모듈에 인터셉터를 추가해준다.

 

@Module({
  imports: [TypeOrmModule.forFeature([Users])],
  controllers: [UsersController],
  providers: [UsersService, AuthService, ExtractUserInterceptor],
})
export class UsersModule {}

 

이제 User를 추출할 데코레이터를 다음과 같이 수정하고

export const Authentication = createParamDecorator(
    (data: never, context: ExecutionContext) => {

        const httpRequest = context.switchToHttp().getRequest();

        return httpRequest.curUser;
    }
)

 

 

인터셉터를 달아준 후 테스트 해보도록 하자.

 

이렇게 잘 나오는 것을 볼 수 있다.

 

이거를 컨트롤러마다 설정하기 복잡하다면, 다음과 같은 방법으로 모듈에 글로벌하게 설정할 수 있다.

@Module({
  imports: [TypeOrmModule.forFeature([Users])],
  controllers: [UsersController],
  providers: [
      UsersService, AuthService,
    {
      provide: APP_INTERCEPTOR,
      useClass: ExtractUserInterceptor,
    }],
})
export class UsersModule {}

 

경로 중 인증정보가 없다면 401, 403 에러를 반환하고 싶을 때가 있다.

그럴 때를 위해 CanActivate를 위해 AuthGuard를 만들어주자.

export class AuthGuard implements CanActivate {
    canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
        const request = context.switchToHttp().getRequest();

        return request.session.userId;
    }
}

 

여기서는 userId가 존재하지 않으면 접근을 막는다.

 

다음과 같이 설정하고

 

로그아웃하고 API를 요청하면

이렇게 403으로 거부되는 것을 볼 수 있다.

 

 

+ Recent posts