[Week13] Ch3 Nest JS

안나경·2024년 4월 12일

크래프톤정글

목록 보기
52/57

3.1 컨트롤러(controller)

컨트롤러는 요청(req)을 받고
처리된 결과를 응답(rep)로 돌려주는 인터페이스.

엔드 포인트 라우팅 메커니즘으로
각 컨트롤러가 받을 수 있는 요청을 분류.

컨트롤러를 사용 목적에 따라 구분하여
모듈화된 소프트 웨어 작성.

(아마 컨트롤러 별로 요청, 응답을 하는데.
route로 무슨 요청을 받고 처리할지 분류하는 것을 가리키는 듯함.)

만들고자 하는 리소스의 CRUD 보일러 플레이트를
한번에 생성이 가능하다.

nest g resource [name]

module, controller, service, entity, dto등
코드나 테스트 코드 자동 생성.

3.1.1 라우팅

보통 localhost의 루트 경로로
요청이 처리되는 중.

app.controller.ts

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

@Controller를 선언하여,
해당 클래스가 컨트롤러 역할을 하게 됨.

gethello()@Get 데커레이터를 가져
루트 경로로 들어오는 요청을 처리.

라우팅 경로는 @Get 데커레이터의 인수로 관리가 가능하다.
ex
@Get('/hello')

@Controller 데커레이에 인수를 전달하면,
라우팅 경로의 접두어, Prefix를 지정.
ex)
@Controller('app') 이면 '...3000/app/hello'
라우팅 경로가 됨.

3.1.2 와일드카드 사용

별표(*) 문자 사용시 문자열 가운데 어떤 문자가 와도 상관없이
라우팅 패스를 구성하겠다는 말.

이러한 와일드카드는 컨트롤러에서만 적용되는 것은 아님.

3.1.3 요청 객체

클라이언트가
요청과 함께 서버가 원하는 정보를 함께 전송하면
Nest는 데이터를 핸들러가 다룰 수 있는 객체로 변환한다.

이렇게 변환된 객체는 @Req 데커레이터를 이용하여 다루는데

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

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(@Req() req: Request): string {
    console.log(req);
    return this.appService.getHello();
  }
}

봤는데 아주 뭔가 개많음.
Req, Request 둘다 import 해야한다.

아무튼 요청 객체를 직접 다루지는않고
Nest에서는 @Query() @Param(key?: string) @Body() 등의
데커레이터로 쿼리 매개 변수, 패스(경로) 매개변수, 본문을 쉽게 받음.

3.1.4 응답

응답은 라우팅 패스로
어떤 요청을 받을 수 있는지 콘솔 로그로
받게 되는 상태 코드까지 한번에 확인 가능.

string, number, boolean 같은 경우
바로 보내지만,

객체를 리턴한다면 직렬화로 JSON으로 자동 변환함.
물론 라이브러리별 응답 객체를 직접 다룰 수도 있음.

  @Get()
  findAll(@Res() res) {
    const users = this.usersService.findAll();

    return res.status(200).send(users);
  }

Res 경우 common에서 import 하면 됨.
@Res로 Express 응답 객체를 다룰수 있다고 함.
(근데 라이브러리별 응답 객체를 다룬다는게 뭐지.)

(이제보니 Fastify 등의 서버를 쓸수도 있어서
아무튼 유연하게 내가 쓰려는 서버에 따라
변환해서 받아올수 있다는 듯 하다.)

(express와 nest는 뭐가 다르냐면,
둘다 웹 프레임워크고 당연히 종류는 다르지만
모듈적인 부분이 다른....듯.)
(계층적 레벨은 같은 듯함.)

상태 코드 변환하기

데커레이터 @HttpCode 사용.

import { HttpCode } from '@nestjs/common';

  @HttpCode(202)
  @Patch(':id')
  update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
    return this.usersService.update(+id, updateUserDto);
  }

HTTP 202(Accepted)

요청이 성공적으로 접수되었으나, 아직 해당 요청을 처리중이거나 처리 시작전.
HTTP가 나중에 요청 처리 결과를 나타내는 비동기 응답을 보낼 방법이 없음.

요청 처리 도중 예외를 던져야한다면,
특정 조건을 걸고 그걸 위반한다면 예외를 던져주자.

  @Get(':id')
  findOne(@Param('id') id: string) {
    if (+id < 1) {
      throw new BadRequestException('id는 0보다 큰 값이어야합니다.');
    }
    return this.usersService.findOne(+id);
  }
❯ curl -X GET http://localhost:3000/users/0
{"message":"id는 0보다 큰 값이어야합니다.","error":"Bad Request","statusCode":400}%   

전자에 알수없는 warning이 생겼지만 그냥 됨.
NotFoundException 객체로 전달한 것도 400이 발생함.

JSON 결과 읽기 편하게 출력하는 법
curl -X GET http://loaclhost:3000/users/0 | jq

(그냥 이쁘더라.)

3.1.5 헤더

Nest는 응답 헤더 역시 자동 구성.
커스텀 헤더를 추가하고 싶다면 @Header 데커레이터를 사용.
인수로 헤더 이름과 값을 받는다.

라이브러리에서 제공하는 응답객체로 res.header() 사용도 가능.

책에선 뭔가 삐까뻔쩍한 것으로 Response 확인하는데 뭐지.

import { HttpCode, Header } from '@nestjs/common';

  @Header('Custom', 'Test Header')
  @Get(':id')
  findOne(@Param('id') id: string) {
    if (+id < 1) {
      throw new BadRequestException('id는 0보다 큰 값이어야합니다.');
    }
    return this.usersService.findOne(+id);
  }
❯ curl -X GET http://localhost:3000/users/1 -v 
...
* Connected to localhost (::1) port 3000
> GET /users/1 HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/8.4.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Custom: Test Header
...

으머 잘 나온다.

3.1.6 리디렉션

서버가 요청 처리후,
클라이언트를 다른 페이지로 이동시키는 것을 리디렉션, redirection이라고 함.

응답 본문에 URL을 포함해서 스스로 이동하게 해도되지만
@Redirect 데커레이터를 사용하면 쉽게 구현 가능.
두번째 인수는 상태 코드로, 301은 완전히 이동되었다는 뜻.

200 등으로 변환해도 되지만 301, 307, 308처럼 Redirect로
정해진 응답코드가 아니면 브라우저가 제대로 반응하지 않을수도 있음.

import { HttpCode, Header, Redirect } from '@nestjs/common';
...
  @Header('Custom', 'Test Header')
  @Redirect('https://nestjs.com', 301)
  @Get(':id')
  findOne(@Param('id') id: string) {
    if (+id < 1) {
      throw new BadRequestException('id는 0보다 큰 값이어야합니다.');
    }
    return this.usersService.findOne(+id);
  }
❯ curl 'http://localhost:3000/users/1' -v
*   Trying [::1]:3000...
* Connected to localhost (::1) port 3000
> GET /users/1 HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/8.4.0
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< X-Powered-By: Express
< Custom: Test Header
< Location: https://nestjs.com
...

동적으로 리디렉트하고 싶다면
응답으로 다음과 같은 객체를 리턴하자.

{
	"url": string,
    "statusCode": number
}

예를 들어 쿼리 매개변수로 버전 숫자를 전달받아
해당 버전 페이지로 이동한다고 하면...
return { url : ~ };
식으로 아예 함수내에 이런 식으로 리턴.

3.1.7 라우트 매개 변수

패스 매개변수라고도 함.
예를 들어, http://localhost:3000/users/1로 요청 시
1에 해당하는 부분이 유저 ID로, 동적으로 구성 중.

전달받은 매개변수를 함수 인수에 @Param 데커레이터로 주입받을 수 있음.
(그러니까 주소가 있고
매개변수를 받으면 라우트 주소에 @Param으로 추가할수 있다는 것같음.)

전달받는 방법은 2가지.

  • 매개변수가 여러개 전달되면 객체로 한 번에 받는 방법.
    단, 이러면 params 타입이 any가 되어 권장 x.
    라우트 매개 변수는 항상 string일테니 명시적으로 { [key: string]: string} 타입 지정도 가능.
  @Delete(':userId/memo/:memoid')
  deletoUserMemo(@Param() params: { [key: string]: string}){
    return `userID: ${params.userId}, memoID: ${params.memoid}}`;
  }
  • 라우팅 매개변수를 따로 받는것.
    대개 매개변수 많이 받게 구성하지 않기떄문에 그리 길어지진 않음.
  @Delete(':userId/memo/:memoid')
  deletoUserMemo(@Param('userId') userId: string,
  @Param('memoId') memoId: string,){
    return `userID: ${params.userId}, memoID: ${params.memoid}}`;
  }

3.1.8 하위 도메인 라우팅

API 요청은 따로 처리하되
API에서 안되는건 원래의 도메인에서 처리하도록 원할 경우.

하위 도메인 라우팅 기법이 가능.

nest g co Api


이건 나중에 보지뭐.

3.1.9 페이로드 다루기

POST, PUT, PATCH 요청은 보통 처리에 필요한 데이터를 함께 실어보냄.
이 데이터 덩어리, 즉 payload를 본문이라고 함.

NestJS에는 데이터 전송 객체, data transfer object DTO
구현되어있어 본문을 쉽게 다룰 수 있음.

앞서 생성한 Users 리소스를 생성하기 위해
POST /users 에 들어오는 본문을 CreateUserDto로 받는다.

create-user-dto-ts 파일.

export class CreateUserDto {
  name: string;
  email: string;
}

user.controller.ts

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    const { name, email } = createUserDto;

    return `유저를 생성했습니다. 이름: ${name}, 이메일: ${email} `;
  }
❯ curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -d '{"name":"YOUR_NAME", "email" : "YOUR_EMAIL@gmail.com"}'
유저를 생성했습니다. 이름: YOUR_NAME, 이메일: YOUR_EMAIL@gmail.com %                                                                                         

흠 덕분에 dto는 dto 객체를 다루는 곳이라는걸 알게 됨.

GET 요청은 주소에 포함한다고 하며,

예를 들어 유저 목록을 가져오는 요청은
GET /users/?offset=0&limit=10 과 같이 페이징 옵션이 포함되도록 구성 가능.

offset은 건너뛸 개수, limit은 이후 몇개의 데이터를 가져올지 지정.

이 두 쿼리 매개변수를 @Query DTO로 묶어서 처리 가능.

3.2 유저 서비스의 인터페이스

유저 서비스의 인터페이스를 정의하고
컨트롤러를 구현해보자.

기능 / 엔드포인트/ 본문데이터 예(JSON) / 패스 매개변수 / 응답

으로 구성.

  • 회원 가입
    POST /users
    { "name": "YOUR_NAME", "email":"YOUR_EMAIL@gmail.com", "password":"PASSWORD"}
    ...
    201
  • 이메일 인증
    POST /users/email-verify
  • 로그인
    POST /users/login
  • 회원정보조회
    GET /users/:id

아 쓰기 귀찮다

nest g co Users 로 UserController 생성.
(이렇게 생성할때 컨트롤러가 겹치지 않게
잘 관리하기.)

users/users.controlle.ts

import { Body, Controller, Post } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users')
export class UsersController {
  @Post()
  async createUser(@Body() dto: CreateUserDto): Promise<void> {
    console.log(dto);
  }
}

src/users/dto/create-user.dto.ts

export class CreateUserDto {
  readonly name: string;
  readonly email: string;
  readonly password: string;
}

아 젠장
Controller가 무슨 데커레이터인지 까먹음
아 라우터에 prefix 넣어주는 데커레이터였지.

회원가입 요청의 본문을 CreateUserDto 클래스로 받는 중.

해당 엔드포인트로 POST 요청이 오면 createUser 메서드가 호출됩니다.
createUser 메서드는 @Body() 데코레이터를 사용하여 요청의 본문(body)을 가져옵니다. 이 본문은 CreateUserDto 형식의 객체여야 합니다. 이 DTO(Data Transfer Object)는 클라이언트에서 보낸 데이터의 형식을 정의합니다.

Post를 하면 생성자 메서드를 호출하면서
객체를 파싱해서 받아내는 거군.

그리고 Promise<타입>에서 타입은 TypeScript 수준에서
컴파일 에러를 띄우기 위해 적는다는군.

❯ curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -d '{"name":"YOUR_NAME", "email" : "YOUR_EMAIL@gmail.com"}'
서버 콘솔
{ name: 'YOUR_NAME', email: 'YOUR_EMAIL@gmail.com' }

아 password 디폴트로 만들어주는거 아니네

아무튼 이 방식으로
내가 만들고자 하는 기능을 라우팅 별로
Controller로 관리되는 함수 내에
내가 요청하려는 method에 따라 지정해서
Body든 Query든 parsing 해서
받는대로 구현을 한다.

(대개 입력을 받아야하는건 Promise와 async로 함.)

import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UserLoginDto } from './dto/user-login.dto';
import { VerifyEmailDto } from './dto/verify-email.dto';
import { UserInfo } from './UserInfo';

@Controller('users')
export class UsersController {
  @Post()
  async createUser(@Body() dto: CreateUserDto): Promise<void> {
    console.log(dto);
  }

  @Post('/email-verify')
  async verifyEmail(@Query() dto: VerifyEmailDto): Promise<string> {
    console.log(dto);
    return;
  }

  @Post('/login')
  async login(@Body() dto: UserLoginDto): Promise<string> {
    console.log(dto);
    return;
  }

  @Get('/:id')
  async getUserInfo(@Param('id') userId: string): Promise<UserInfo> {
    console.log(userId);
    return;
  }
}

근데 아직 Query와 Body, Param의 구별을 잘 모르겠군.

export class VerifyEmailDto {
  signupVerifyToken: string;
}

이건 Query로 하고...

export class UserLoginDto {
  email: string;
  password: string;
}

이건 body고...

export interface UserInfo {
  id: string;
  name: string;
  email: string;
}

이건 Param이기도 한데
선언 자체도 class가 아니라 interface네?

요청은 이런 식.
전부 서버 콘솔에 뜸.

❯ curl -X POST http://localhost:3000/users/email-verify\?signupVerifyToken\=test_token
❯ curl -X POST http://localhost:3000/users/login -H "Content-Type: application/json" -d '{"email": "YOUR_EMAIL@gamil.com", "password": "PASSWORD"}'
❯ curl -X GET http://localhost:3000/users/user-id
{ signupVerifyToken: 'test_token' }
{ email: 'YOUR_EMAIL@gamil.com', password: 'PASSWORD' }
user-id

아, 쿼리 구분 : 주소에 데이터가 오는 경우.
바디 구분 : 응답 본문에 유저 데이터가 오는 경우.
Param 구분 : 정보 조회시 매개 변수를 받아 Param으로 찾는데
Get에서 받은 인수로 (이때 Param과 Get의 인수는 이름이 일치.)
값을 찾는 거같은데....

왜 class가 아니라 interface를 쓰지...
아 단순히 타입 정의에 가까운 개념이라... 그런 듯?

암튼 정말 도움이 되는군.

profile
개발자 희망...

0개의 댓글