[03장] 애플리케이션의 관문 - 인터페이스

Kim Seohyun·2022년 8월 25일
0
post-thumbnail

✏️ NestJS로 배우는 백엔드 프로그래밍 를 공부하며 정리하는 글입니다 ✏️
⚙️ 관련 코드는 Github 에서 보다 자세히 보실 수 있습니다 ⚙️

3.1 컨트롤러(controller)

Nest 의 컨트롤러는 MVC 패턴에서 말하는 그 컨트롤러를 말한다. 컨트롤러는 들어오는 요청(request)를 받고 처리된 결과를 응답(response)으로 돌려주는 인터페이스 역할을 한다.

컨트롤러는 엔드포인트 라우팅(routing) 메커니즘을 통해 각 컨트롤러가 받을 수 있는 요청을 분류한다. 이때 사용 목적에 따라 구분하면 구조적이고 모듈화된 소프트웨어를 작성할 수 있습니다.

🧐 MVC(Model–View–Controller) 패턴
소프트웨어 공학에서 사용되는 소프트웨어 디자인 패턴이다.
MVC에서 Model 은 애플리케이션의 정보(데이터)를 나타내며, View 는 텍스트, 체크박스 항목 등과 같은 사용자 인터페이스 요소를 나타내고, Controller 는 데이터와 비즈니스 로직 사이의 상호동작을 관리한다.
이 패턴을 성공적으로 사용하면, 사용자 인터페이스로부터 비즈니스 로직을 분리하여 애플리케이션의 시각적 요소나 그 이면에서 실행되는 비즈니스 로직을 서로 영향 없이 쉽게 고칠 수 있는 애플리케이션을 만들 수 있다.
더 자세한 것은 여기에서 읽어보자.

컨트롤러를 작성해보자.
소스코드를 직접 작성해도 되지만 아래 명령어와 같이 1장에서 설치한 nest CLI를 이용하여 쉽게 생성할 수 있다.

$ nest g controller <name>

자동 생성된 코드를 보면 AppController 는 AppModule 에 선언되어 있고, AppService 를 import 해서 사용하고 있다. 모듈과 서비스(프로바이더)에 대해서는 이후에 배우게 된다.
아래의 명령어를 사용하면 만들고자 하는 리소스의 CRUD 보일러 플레이트 코드를 한 번에 생성할 수도 있다. name 리소스에 대한 module, controller, service, entity, dto 코드와 테스트 코드를 자동 생성해 준다.

$ nest g resource <name>

nest -h 명령어로 다른 Nest 구성요소를 확인할 수 있다. 이후 nest g 명령어에 생성하고자 하는 구성요소를 name 또는 alias 로 작성하면 된다. 예를 들어, nest g module <name>nest g mo <name> 과 같은 결과를 생성한다.

라우팅(routing)

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 데코레이터를 가지고 있다. 루트 경로(http://localhost:3000)로 들어오는 요청을 처리할 수 있게 되었다. 즉, 라우팅 경로를 @Get 데코레이터의 인자로 관리할 수 있게 되었다.
경로를 루트 경로가 아니라 /hello 로 변경해 보자.

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

그리고 다시 루트 경로로 요청을 보내면 404 Not found 에러가 발생한다.

{
    "statusCode": 404,
    "message": "Cannot GET /",
    "error": "Not Found"
}

대신 http://localhost:3000/hello 로 주소를 변경하면 정상 동작하는 것을 확인할 수 있다.

🧐 반영이 되지 않는다면?
yarn start 로 서버에 접속했다면 build 가 되지 않았기 때문에 변경사항이 반영되지 않는다. 그래서 변경사항이 생긴 경우, yarn build && yarn start 를 해줘야 한다. 또는 yarn start:dev 를 사용하여 live-reload 가 가능하다.
다 자세한 내용은 여기서 읽어보자.

@Controller 데코레이터에도 인자를 전달할 수 있다. 이를 통해 라우팅 경로의 prefix 를 지정한다. 예를 들어 @Controller('app') 이라고 했다면 이제 http://localhost:3000/app/hello 라는 경로로 접근해야 한다. prefix 는 보통 컨트롤러가 맡은 리소스의 이름을 지정하는 경우가 많다.

와일드카드 사용

라우팅 패스는 와일드카드를 이용하여 작성할 수 있다. 예를 들어 별표(*) 문자를 사용하면 문자열 가운데 어떤 문자가 와도 상관없이 라우링 패스를 구성하겠다는 뜻이다. 예를 들어, 아래의 코드는 helo , hello, he__lo 와 같은 경로로 요청을 받을 수 있다.

@Get('he*lo')
getHello(): string {
  return this.appService.getHello();
}

* 외에 ?, +, () 문자 역시 정규 표현식에서의 와일드 카드와 동일하게 동작한다. 단, 하이픈(-)과 점(.)은 문자열로 취급한다. 즉, @Get('he.lo') 는 hello 로 요청할 수 없다.
와일드 카드는 컨트롤러의 패스를 정할 때만 사용하는 것이 아니다. 앞으로 배우게 될 많은 컴포넌트에서 이름을 정할 때 사용할 수 있다. 이후에는 반복해서 설명하지 않을 것이므로 필요하다면 와일드카드를 적용해보자.

요청 객체(request object)

클라이언트는 어떤 요청을 보내면서 종종 서버가 원하는 정보를 함께 전송한다. 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)는 HTTP 요청을 나타낸다. 요청 객체가 어떻게 구성되어 있는지 console 에서 확인해보자(서버 접속 식 VSCode termial 에서 확인 가능). 쿼리 스트링, 파라미터, 헤더와 본문 외 많은 정보를 가지고 있다. 더 자세한 내용은 Express 문서를 참고하자.
API를 작성할 때 요청 객체를 직접 다루는 경우는 드물다. Nest 는 @Query(), @Param(key?: string), @Body 데코레이터를 이용해서 요청에 포함된 쿼리 파라미터, 패스 파라미터, 본문을 쉽게 받을 수 있도록 해주기 때문이다.

응답(response)

nest g resource Users 명령어로 Users 리소스에 대한 CRUD API 를 만들어보자. 다음, 서버를 실행하면 어떤 라우팅 패스를 통해 요청을 받을 수 있는지 콘솔 로그를 통해 확인할 수 있다.

...
[Nest] 79350  - 08/10/2022, 1:57:23 PM     LOG [RoutesResolver] UsersController {/users}: +0ms
[Nest] 79350  - 08/10/2022, 1:57:23 PM     LOG [RouterExplorer] Mapped {/users, POST} route +1ms
[Nest] 79350  - 08/10/2022, 1:57:23 PM     LOG [RouterExplorer] Mapped {/users, GET} route +0ms
[Nest] 79350  - 08/10/2022, 1:57:23 PM     LOG [RouterExplorer] Mapped {/users/:id, GET} route +0ms
[Nest] 79350  - 08/10/2022, 1:57:23 PM     LOG [RouterExplorer] Mapped {/users/:id, PATCH} route +1ms
[Nest] 79350  - 08/10/2022, 1:57:23 PM     LOG [RouterExplorer] Mapped {/users/:id, DELETE} route +0ms
...

🧐 반영이 되지 않는다면?
app.module.tsimports: [UsersModule], 라고 Users 리소스가 라우팅 될 수 있도록 설정 되어 있는지 확인해보자.

🧐 CLI로 자동 생성된 update 는 PATCH 메서드를 사용
http 메서드에는 업데이트 동작을 기술하는 메서드가 2가지 있다. PUT 은 리소스 전체를 교체할 때 쓰고, PATCH 는 리소스의 일부를 업데이트 할 때 사용한다. 하지만 실제 구현시에는 이를 엄격하게 지키지 않고 PUT 을 보통 사용하지만, 만약 PATCH 가 사용됐다면 이같은 뜻을 가진다고 생각하면 된다.

이를 각각 요청하면 다음과 같은 결과가 나온다.

경로method상태 코드body
/usersPOST201This action adds a new user
/usersGET200This action returns all users
/users/:idGET200This action returns a #${id} user
/users/:idPATCH200This action updates a #${id} user
/users/:idDELETE200This action removes a #${id} user
  • postman 등으로 요청 시, #${id} 가 아닌 #NaN 로 뜨는 것은 users 데이터가 없기 때문
  • 응답 본문은 스트링 값을 가지고 있는데 이는 UsersController 의 각 메서드가 리턴하는 값임

Nest 는 string, number, boolean 과 같이 자바스크립트 원시 타입을 리턴할 경우 직렬화 없이 바로 보내지만, 객체를 리턴한다면 직렬화를 통해 JSON 으로 자동 변환해준다. 이 방법이 권장하는 방법이긴 하지만 라이브러리별 응답 객체를 직접 다룰 수도 있다.
예를 들어 Express 를 사용한다면 Express response object 를 아래와 같이 @Res 데코레이터를 이용해서 다룰 수 있다.

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

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

위의 상태 코드를 보면 Nest 는 CRUD 에 대해 성공 응답으로 POST 는 201, 그 외는 200 을 가진다. 만약 이 상태코드를 다른 값으로 바꾸길 원한다면 어떻게 해야 할까? 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);
}

만약 요청을 처리하는 도중 에러가 발생하거나 예외를 던져야 한다면 어떻게 해야 할까? 예를 들어 유저 정보 조회(GET /users/:id) 요청했는데 id 는 1부터 시작하는 규칙을 가지고 있다고 하자. 만약 id 가 1보다 작은 값이 전달될 경우 400 Bad Request 예외를 던져야 한다.

@Get(':id')
findOne(@Param('id') id: string) {
  if (+id < 1) {
    throw new BadRequestException('id는 0보다 큰 값이어야 합니다.');
  }

  return this.usersService.findOne(+id);
}

id 를 0으로 요청한 결과이다. NotFoundException 객체의 생성자로 전달한 메시지와 함께 상태코드가 400인 에러가 발생한다.

  • 본인은 보통 가독성을 위해 postman 으로 request 를 요청하지만 손쉬운 방법이 있음을 보이기 위해 위와 같이 curl 명령어를 사용해봄

헤더(header)

Nest는 응답 헤더 역시 자동 구성해준다.
만약 응답에 커스텀 헤더를 추가하고 싶다면 @Header 데코레이터를 사용하면 된다. 인자로 헤더 이름과 값을 받는다. 물론 라이브러리에서 제공하는 응답객체를 사용해서 res.header() 메서드로 직접 설정도 가능하다.

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

@Header('Custom', 'Test Header')
@Get(':id')
findOneWithHeader(@Param('id') id: string) {
  return this.usersService.findOne(+id);
}

curl 명령어를 사용할 때 -v (verbose) 옵션을 주면 헤더와 같은 자세한 정보를 얻을 수 있다. 이때, -X 옵션을 생략하면 GET 으로 동작한다.

$ curl http://localhost:3000/users/1 -v
Note: Unnecessary use of -X or --request, GET is already inferred.
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET /users/1 HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Custom: Test Header
< Content-Type: text/html; charset=utf-8
< Content-Length: 29
< ETag: W/"1d-MU9PTdoaF+1jeHzvs+kaeFq7QDs"
< Date: Mon, 27 Sep 2021 01:23:29 GMT
< Connection: keep-alive
<
* Connection #0 to host localhost left intact
This action returns a #1 user* Closing connection 0
  • 헤더를 데코레이터로 추가함에 따라 설정이 Custom: Test Header 임을 확인할 수 있다.

리디렉션(redirection)

종종 서버는 요청을 처리한 후 요청을 보낸 클라이언트를 다른 페이지로 이동시키고 싶은 경우가 있다. 응답 본문에 redirectUrl 을 포함시켜 클라이언트가 스스로 페이지를 이동해도 되지만, @Redirect 데코레이터를 사용하면 쉽게 구현이 가능하다.

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

@Redirect('https://nestjs.com', 301)
@Get(':id')
findOne(@Param('id') id: string) {
  return this.usersService.findOne(+id);
}

데코레이터의 두번째 인자는 상태코드다. 아래 예시를 보자. 301 Moved Permanatly 는 요청한 리소스가 헤더에 주어진 리소스로 완전히 이동됐다는 뜻이다. 이 상태코드를 200과 같이 다른 것으로 바꾸어 응답할 수 있다. 하지만 301, 307, 308과 같이 Redirect 로 정해진 응답코드가 아닐 경우 브라우저가 제대로 반응하지 않을 수 있다.

$ curl 'http://localhost:3000/users/1' -v
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET /users/1 HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< X-Powered-By: Express
< Custom: Test Header
< Location: https://nestjs.com
< Vary: Accept
< Content-Type: text/plain; charset=utf-8
< Content-Length: 52
< Date: Tue, 14 Sep 2021 21:14:59 GMT
< Connection: keep-alive
<
* Connection #0 to host localhost left intact
Moved Permanently. Redirecting to https://nestjs.com* Closing connection 0

만약 요청 처리 결과에 따라 동적으로 리디렉트 하고자 한다면 응답을 다음 객체와 같이 리턴하면 된다.

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

예를 들어 쿼리 파라미터로 버전 숫자를 전달받아 해당 버전의 페이지로 이동한다고 하면 다음처럼 구현할 수 있다. 참고로 앞서 설명했듯이 Nest는 자바스크립트 객체를 리턴하면 JSON 스트링으로 직렬화를 해서 보내준다.

@Get('redirect/docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs(@Query('version') version) {
  if (version && version === '5') {
    return { url: 'https://docs.nestjs.com/v5/' };
  }
}

이제 브라우저에서 http://localhost:3000/redirect/docs?version=5 를 입력하면 https://docs.nestjs.com/v5/ 페이지로 이동된다.

라우트 파라미터(route parameter)

라우트 파라미터는 이미 앞선 예제에서 사용했다. 1번 유저의 정보를 가져오기 위해 http://localhost:3000/users/1 로 요청을 하자. 여기서 1에 해당하는 부분은 유저 ID 인데 경로를 구성하는 파라미터가 된다. 전달받은 파라미터는 함수 인자에 @Param 데코레이터로 주입받을 수 있다.
라우트 파라미터를 전달받는 방법은 2가지가 있다. 먼저 파라미터가 여러 개 전달될 경우 객체로 한번에 받는 방법이다. 이 방법은 params 의 타입이 any 가 되어 권장하지 않는다. 물론 라우트 파라미터는 타입이 항상 string 이기 때문에 아래와 같이 명시적으로 { [key: string]: string } 타입을 지정해 주는 방법이 있다.

@Delete(':userId/memo/:memoId')
deleteUserMemo(@Param() params: { [key: string]: string }) {
  return `userId: ${params.userId}, memoId: ${params.memoId}`;
}

일반적인 방법은 다음 코드처럼 라우팅 파라미터를 따로 받는 것이다. REST API 를 구성할 때 라우팅 파라미터의 개수가 너무 많아지지 않게 설계하는 것이 좋기 때문에 따로 받아도 코드가 많이 길어지지는 않는다.

@Delete(':userId/memo/:memoId')
deleteUserMemo(
  @Param('userId') userId: string,
  @Param('memoId') memoId: string,
) {
  return `userId: ${userId}, memoId: ${memoId}`;
}

하위 도메인 라우팅(subdomain routing)

서버에서 제공하는 기능을 API 로 외부에 공개하기로 했다고 가정하자. 현재 사용하고 있는 도메인은 example.com 이고, API 요청은 api.example.com 으로 받기로 했다. 즉, http://example.com, http://api.example.com 로 들어온 요청을 서로 다르게 처리하고 싶다. 또한 하위 도메인에서 처리하지 못하는 요청은 원래의 도메인에서 처리되도록 하고 싶다. 이런 경우 하위 도메인 라우팅 기법을 쓸 수 있다.

아래와 같이 ApiController 라는 새로운 컨트롤러를 생성하자.

$ nest g co ApiController

app.controller.ts 에 이미 루트 라우팅 경로를 가진 엔드포인트가 존재한다. ApiController 에도 같은 엔드포인트를 받을 수 있도록 할 것인데 이를 위해 ApiController 가 먼저 처리될 수 있도록 다음 방법을 통해 순서를 수정한다.

@Module({
  controllers: [ApiController, AppController],
    ...
})
export class AppModule { }

@Controller 데코레이터는 ControllerOptions 객체를 인자로 받는데 host 속성에 하위 도메인을 기술하면 된다.

@Controller({ host: 'api.example.com' }) // 하위 도메인 요청 처리 설정
export class ApiController {
  @Get() // 같은 루트 경로
  index(): string {
    return 'Hello, API'; // 다른 응답
  }
}

이제 각각 GET 요청을 보내면 응답을 다르게 주는 것을 볼 수 있다.

로컬에서 테스트를 하기 위해 하위 도메인을 api.localhost 로 지정하면 curl 명령어가 제대로 동작하지 않다. 이는 api.localhost 가 로컬 요청을 받을 수 있도록 설정되어 있지 않기 때문이다. 이를 해결하기 위해 아래와 같이 /etc/hosts 파일의 마지막에 127.0.0.1 api.localhost 을 추가하고 서버를 다시 구동하면 된다. 윈도우의 경우도 해결 방법이 있으니 다른 자료를 참고하자.

...
127.0.0.1 api.localhost
127.0.0.1 v1.api.localhost

Controller 에도 변경사항을 아래와 같이 반영하자.

@Controller({ host: 'api.localhost' }) // localhost로 변경
export class ApiController {
  @Get()
  index(): string {
    return 'Hello, API';
  }
}

이제 각각 요청을 보내면 원하는 대로 잘 동작한다.

$ curl http://localhost:3000
Hello World!
$ curl http://api.localhost:3000
Hello, API

앞서 우리는 요청 패스를 @Param 데코레이터로 받아 동적으로 처리할 수 있었다. 유사하게 @HostParam 데코레이터를 이용하면 서브 도메인을 변수로 받을 수 있다. API 버저닝을 하는 방법이 여러가지 있지만 하위 도메인을 이용하는 방법을 많이 사용한다. 다음과 같이 하위 도메인 라우팅으로 쉽게 API를 버전별로 분리할 수 있다.

@Controller({ host: ':version.api.localhost' })
export class ApiController {
  @Get()
  index(@HostParam('version') version: string): string {
    return `Hello, API ${version}`;
  }
}

분리 결과는 다음과 같다. host param 이 없는 host 로 요청을 하면 기존 도메인으로 요청이 처리되는 것도 볼 수 있다.

$ curl http://v1.api.localhost:3000
Hello, API v1
$ curl http://api.localhost:3000
Hello World!

페이로드(payload) 다루기

POST, PUT, PATCH 요청은 보통 처리에 필요한 데이터를 함께 실어 보낸다. 이 데이터 페이로드를 본문(body)이라고 한다. NestJS 는 본문을 DTO(Data Transfer Object) 를 정의하여 쉽게 다룰 수 있다.

앞서 생성한 Users 리소스를 생성하기 위해 POST /users 로 들어오는 본문을 CreateUserDto 로 받고 있다. 회원가입을 하기 위한 이름과 이메일을 DTO 로 추가해보자.

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

controller 에도 변경사항을 아래와 같이 반영하자.

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

  return `유저를 생성했습니다. 이름: ${name}, 이메일: ${email}`;
}

이제 유저 생성 API를 요청하고 본문에 데이터가 잘 들어가 있는지 확인해보자.

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

GET 요청에서 서버에게 전달할 데이터를 포함할 때는 일반적으로 요청 주소에 포함시킨다. 예를 들어 유저 목록을 가져오는 요청은 GET /users?offset=0&limit=10 과 같이 페이징 옵션이 포함되도록 구성할 수 있다. offset 은 데이터 목록 중 건너뛸 개수를 의미하고 limit 은 offset 이후 몇 개의 데이터를 가져올 지 결정한다. 이 두 개의 쿼리 파라미터를 아래와 같이 @Query DTO 로 묶어 처리할 수 있다.

export class GetUsersDto {
  offset: number;
  limit: number;
}

지금까지 백엔드 애플리케이션의 관문이라 할 수 있는 컨트롤러를 Nest에서 어떻게 사용하는지 살펴보았다. 컨트롤러는 서버로 들어오는 요청을 처리하고 응답을 가공한다. 서버에서 제공하는 기능을 어떻게 클라이언트와 주고 받을지에 대한 인터페이스를 정의하고 데이터의 구조를 기술한다.

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

이제 앞서 1장에서 소개한 유저 서비스의 인터페이스를 정의하고 컨트롤러를 구현하자. 우리의 유저 서비스는 외부에서 4가지의 요청을 받아 처리한다. 프론트 엔드의 UI 화면을 함께 만들지 않기 때문에 프론트에서 요청이 전달된다고 가정하고 진행하겠다.

인터페이스 정의

각 요청에 대한 인터페이스를 다음과 같이 정의했다.

기능엔드포인트더미 데이터쿼리 파라미터라우트 파라미터응답
회원가입POST /users{"name": "Dexter", "email": "dexter.haan@gmail.com", "password": "PASSWORD"}201
이메일 인증POST /users/email-verify{“signupVerifyToken”: “임의의 문자열”}201(access token)
로그인POST /users/login{"email": "dexter.haan@gmail.com", "password": "PASSWORD"}201(access token)
회원정보 조회GET /users/:id{"name": "Dexter", "email": "dexter.haan@gmail.com", "password": "PASSWORD"}id: 유저 생성시 만들어진 유저 ID. email이 아니라 임의의 문자열201(회원 정보)

AppController 와 AppService 에 UsersModule 를 반영하기 위해 controller imports 에 UsersModule 을 추가해주자.

@Module({
  imports: [UsersModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

이제 위에서 정의한 인터페이스를 컨트롤러에 만들어 보자.

$ nest g co Users

이제 src 디렉토리 내의 파일은 이렇게 구성되었다.

src
├── app.module.ts
├── main.ts
└── users
    └── users.controller.ts

인터페이스 구현

회원가입 인터페이스를 UsersController 에 구현해보자.

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);
  }
}

회원가입 요청의 본문을 CreateUserDto 클래스로 받고 있다. src/users/dto/create-user.dto.ts 파일을 만들고 이름, 이메일, 패스워드 데이터를 정의하는 DTO를 정의하자.

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

마찬가지로 나머지 인터페이스들도 컨트롤러에 아래와 같이 구현한다.

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;
  }
}
  • L15: 이메일 인증시 URL에 포함되어 전달되는 쿼리 파라미터를 @Query 데코레이터와 함께 선언한 DTO로 받는다.
    export class VerifyEmailDto {
      signupVerifyToken: string;
    }
  • L21: 로그인을 할 때 유저가 입력한 데이터는 본문으로 전달되도록 하자.
    export class UserLoginDto {
      email: string;
      password: string;
    }
  • L26~27: 유저 정보 조회시 유저 아이디를 패스 파라미터 id 로 받는다. @Get 데코레이터의 인자에 있는 id 와 @Param 데코레이터의 인자로 있는 id 는 이름이 같아야 한다.

이제 서버를 구동시키고 요청을 보내서 dto 에 데이터가 제대로 전달되는지 확인하자. 반환값은 추후 구현할 것이기 때문에 바로 return 했다. 요청 헤더에는 본문이 JSON 형식임을 나타내는 Content-Type 헤더를 추가한다.

회원가입

$ curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -d '{"name": "Seohyun", "email": "seohyun.kim@mail.com"}'
{
  name: 'Seohyun',
  email: 'seohyun.kim@mail.com',
  password: 'PASSWORD'
}

이메일 인증

$ curl -X POST http://localhost:3000/users/email-verify\?signupVerifyToken\=test_token
{ signupVerifyToken: 'test_token' }

로그인

$ curl -X POST http://localhost:3000/users/login -H "Content-Type: application/json" -d '{"email": "seohyun.kim@mail.com", "password": "PASSWORD"}'
{ email: 'seohyun.kim@mail.com', password: 'PASSWORD' }

회원정보 조회

$ curl -X GET http://localhost:3000/users/user-id
user-id
profile
EWHA Cyber Security 19

0개의 댓글

관련 채용 정보