본문 바로가기
Back-end/기초부터 따라하는 nest.js

시즌 2 #4. 기초부터 따라하는 Nest.js 2 : 이제 Nest.js를 배워봅시다!

by hsloth 2024. 5. 17.

 

지난 시간에는 Express와 MySQL을 연동하는 방법을 알아보고, 간단하게 회원가입 API를 구현했습니다.

https://suloth.tistory.com/198

 

시즌 2 #3. 기초부터 따라하는 Nest.js 2 : Express 배우기(2)

지난 시간에는 간단하게 express 서버 구축, router, query, param, body, middleware에 대해서 배웠습니다.https://suloth.tistory.com/197 시즌 2 #2. 기초부터 따라하는 Nest.js 2 : Express 배우기(1)지난 시간에는 HTTP Meth

suloth.tistory.com

 

이번 시간에는 Nest.js에 대해서 간단하게 알아보는 시간을 가져봅시다!


과제 정답


 

과제1

post.router.js에 게시글을 생성하는 API를 만들어주세요!

 

과제2

생성한 게시글을 GET하는 API를 만들어주세요! 이 때, post의 id값을 url로 입력 받아서 해당하는 id를 가진 post 정보만 가져올 수 있도록 해주세요.

 

과제1은

간단합니다! post.router.js에 다음과 같은 코드를 작성해주면 됩니다.

// post.router.js
const mysql = require("../config");

router.post("/", (req, res, next) => {
  const { title, content, userId } = req.body;

  const sql = `INSERT INTO Post (title, content, userId) VALUES ('${title}', '${content}', '${userId}')`;

  mysql.query(sql);

  res.send("Success to Create Post");
});

 

그리고 postman을 이용해서 요청을 날려주면 됩니다. 이 때, 주의할 점은 userId가 FK(외래키)이기 때문에 user 테이블에 존재하는 id여야한다는 점입니다.

저의 경우에는 id 1, 3이 존재하므로 3으로 입력해주었습니다.

 

그리고 데이터가 정상적으로 Insert 되었는지 확인해봅시다. (mysql 접속은 터미널에서 mysql -u root 를 입력해서 들어가줍시다)

use study;

select * from post;

 

과제2도

이 부분은 아직 배우지 않았습니다. 콜백함수를 사용해서 리턴을 해주어야 합니다. post.router.js에 다음과 같은 함수를 작성해줍시다.

// post.router.js
router.get("/:postId", (req, res, next) => {
  const postId = req.params.postId;

  const sql = `SELECT * FROM Post WHERE id = ${postId} LIMIT 1`;

  // mysql에 query를 날려서 데이터를 가져오는 동작은 비동기로 동작합니다.
  // 즉, 얼마나 기다려야할 지 모른다는 거죠.
  // 그래서 DB에서 데이터를 받아왔을 때, res.json이 실행되도록 함수를 작성해주면 됩니다.
  mysql.query(sql, (err, rows) => {
    if (err) throw err;
    return res.json(rows);
  });
});

 

 

 

전체코드

// post.router.js
const router = require("express").Router();
const mysql = require("../config");

router.post("/", (req, res, next) => {
  const { title, content, userId } = req.body;

  const sql = `INSERT INTO Post (title, content, userId) VALUES ('${title}', '${content}', '${userId}')`;

  mysql.query(sql);

  res.send("Success to Create Post");
});

router.get("/", (req, res) => {
  const title = req.query.title;
  const content = req.query.content;

  res.json({
    title: title,
    content: `query ${content}`,
  });
});

router.get("/title/:title/content/:content", (req, res) => {
  const title = req.params.title;
  const content = req.params.content;

  res.json({
    title: title,
    content: `param ${content}`,
  });
});

router.get("/:postId", (req, res, next) => {
  const postId = req.params.postId;

  const sql = `SELECT * FROM Post WHERE id = ${postId} LIMIT 1`;

  mysql.query(sql, (err, rows) => {
    if (err) throw err;
    return res.json(rows);
  });
});

module.exports = router;

 

 


Nest.js 란?


 

Nest.js 공식 홈페이지에는 이렇게 나와있습니다.

더보기

NestJS는 효율적이고 확장가능한 Node.js 서버 측 애플리케이션을 구축하기 위한 프레임워크입니다. 

이는 프로그레시브 JavaScript를 사용하고 TypeScript 로 구축되어 완벽하게 지원하며 (여전히 개발자가 순수 JavaScript로 코딩할 수 있음) OOP(객체 지향 프로그래밍), FP(기능 프로그래밍) 및 FRP(기능 반응 프로그래밍) 요소를 결합합니다.

내부적으로 Nest는 Express (기본값)와 같은 강력한 HTTP 서버 프레임워크를 사용하며 선택적으로 Fastify 도 사용하도록 구성할 수 있습니다!

Nest는 이러한 일반적인 Node.js 프레임워크(Express/Fastify)보다 높은 수준의 추상화를 제공하지만 해당 API를 개발자에게 직접 공개합니다. 이를 통해 개발자는 기본 플랫폼에서 사용할 수 있는 수많은 타사 모듈을 자유롭게 사용할 수 있습니다.

 

저희는 Express 기반의 Nest.js를 사용하여 "블로그" 프로젝트를 진행할 예정입니다.

 

일단 Nest.js의 특징은 다음과 같습니다.

  • Module 기반으로 의존성을 관리합니다.
  • Typescript를 지원합니다.
  • 데코레이터를 사용하여 함수에 부가적인 기능을 추가해줄 수 있습니다.
  • 백엔드 개발에 필요한 기본적인 기능들에 대해 프레임워크 자체에서 제공해줍니다.
  • Java Spring과 비슷하기 때문에, Spring 개발자분들이 입문하기 비교적 수월합니다.

Nest.js 구조


Nest.js는 기본적으로 세 가지로 이루어져 있습니다.

Module, Controller, Service입니다.

이것들은 기본적으로 Class로 구현되어 있습니다.

 

 

 

Service

말 그대로 서비스입니다. 사용자에게 제공할 서비스 함수들을 가지고 있는 Class라고 생각하면 됩니다.

사용자에게 "나 이 서비스 쓸래"라고 요청이 들어오면, 컨트롤러가 사용자 요청에 맞는 서비스를 사용자에게 전달해줍니다.

조금 더 정확히 말하면, Get /user 요청이 들어오면, 컨트롤러가 요청에 맞는 서비스 함수를 실행해서 결과 값을 사용자에게 전달해줍니다.

 

Controller

컨트롤러. 말그대로 제어를 하는 역할을 하는 녀석입니다.

컨트롤러는 사용자에게 요청을 받아서 요청에 알맞는 서비스를 사용자에게 전달해주는 역할을 합니다.

ex) /user/login 경로로 POST 요청이 오면, Controller가 해당 경로에 해당하는 서비스인 로그인 서비스를 사용자에게 제공하여 사용자는 로그인할 수 있게 된다.
ex2) /main-page 경로로 GET 요청이 오면, Controller가 main-page 경로에 해당하는 서비스인 메인 페이지를 화면에 띄우는 서비스를 제공한다. 따라서 사용자는 메인페이지를 볼 수 있게된다.

 

Module

모듈은 Controller와 Service가 들어있는 상자(Class)라고 생각하면 됩니다.
모듈은 보통 리소스 단위로 작성되며, Class간 의존성을 관리하기 위해 존재한다고 보시면 됩니다.

모듈은 보통 리소스 단위 (유저, 게시글, 인증 등)로 만들어서 해당 서비스에서 필요한 모듈을 불러와서 해당 모듈의 서비스를 사용하는 식으로 이용됩니다.
ex) User Service에서 로그인 서비스 로직을 작성하기 위해서 인증을 위한 Auth Service가 필요합니다. 그러면 User Module에서 Auth Module과 소통하여 Auth Service좀 써도 되겠냐고 물어보는... 이런 식의 느낌만 알아가면 될 것 같습니다.

 

Provider

여기서 갑자기 뜬금없이 프로바이더라는 개념이 등장합니다.

프로바이더란 직역하면 공급자라는 뜻입니다.

간단하게 말하면, 기능을 제공해주는 모든 class를 말합니다. 그리고 공급자는 종속성 주입을 통해 다른 클래스에 무언가의 기능을 제공하는 Class라고 생각하면 될 것 같습니다.

대표적으로 Service, Repository가 Provider라고 할 수 있습니다.

 

사실상, Nest.js의 구성요소 세 가지는 Module, Controller, Provider인 셈이죠. 간단하게 이해를 시키기 위해서 Service라고 했을 뿐입니다. 일단, 아 그렇구나~ 하고 넘어갑시다.

 

 


Nest.js의 구성 요소 및  동작 구조


Express.js를 배울 때, 미들웨어에 대해서 언급한 적이 있습니다.

https://suloth.tistory.com/197

 

시즌 2 #2. 기초부터 따라하는 Nest.js 2 : Express 배우기(1)

지난 시간에는 HTTP Method에 대해서 배웠습니다.https://suloth.tistory.com/196 시즌 2 #1. 기초부터 따라하는 Nest.js 2 : HTTP 메소드이전 시간에는 간단하게 서버와 클라이언트에 대해서 다뤄봤습니다.https:/

suloth.tistory.com

 

미들웨어란 간단히 말하면, 사용자의 요청~응답까지 거치는 모든 함수라고 생각하면 됩니다.

따라서

사용자의 요청이 제대로된 요청인지 확인하는 함수도 미들웨어고,

사용자가 인증된(로그인된) 사용자가 맞는지 인가하는 함수도 미들웨어고,

사용자의 요청을 내가 짠(서버 개발자가 짠) 코드에 맞도록 변형해주는 함수도 미들웨어입니다.

 

Nest.js에서 대표적인 구성 요소는 다음과 같습니다. 또한, 이것들은 미들웨어라고 볼 수 있습니다. 요청을 받아서 다음 함수로 넘기는 역할을 하니 미들웨어라고 볼 수 있죠.

  • Guard
  • Interceptor
  • Pipe
  • Exception Filter

이러한 미들웨어들은 Nest.js 내에서 간편하게 구현할 수 있도록 프레임워크 내에 준비되어 있습니다.

또한, 따로 Middleware를 구현할 수도 있습니다. 이 부분은 나중에 LoggerMiddleware를 가르칠 때 알려드릴 예정입니다.

 

Guard

가드입니다. 경비원. 말 그대로 문 앞을 지키는 녀석이죠.

백엔드 서버 = 집 으로 비유하자면, 집에 들어가려면 가장 먼저 문을 거쳐야하죠? 가드가 그런 녀석입니다. 서버로 요청이 들어오면, 가장 먼저 정상적인 요청인지 확인합니다.

낯선 사람이 문으로 들어오려고 하면 경비원이 신원확인을 하듯이, 요청이 들어오면 해당 요청이 인증된 요청인지 확인하는 역할을 합니다.

대표적인 동작으로는 로그인이 되어있는 사용자가 보낸 요청인지 확인하는 역할을 합니다. 로그인이 되어있으면 해당 요청을 받아드리고(문을 통과시켜주고), 로그인이 되어있지 않으면 요청을 거절합니다(문을 통과시켜주지 않습니다).

 

Interceptor

인터셉터입니다. 말 그대로 가로채는 녀석입니다.

사용자의 요청이나 서버 응답을 가로채서 무언가 조작을 하는 역할을 담당합니다.

잘 이해가 안가죠?

어떻게 보면, Guard도 Interceptor입니다. 사용자의 요청을 서버의 서비스 로직을 타기 전에 가로채서 인증된 사용자인지, 로그인된 사용자인지 검증을 하잖아요? 또한, 사용자의 요청에 담긴 인증 정보에 따라 역할을 구분해서 요청에 역할에 대한 정보를 추가해주기도 합니다. 예를들면 이 사람은 학생이야 라던가 하는 정보를 어딘가에 추가해줍니다. (express로 치면, req라는 객체에 추가해줘요)

응답의 경우, 서비스 로직에서 리턴한 데이터를 원래라면 사용자에게 바로 전달해줘야합니다. 그런데, 사용자에게 전달하기 전에 해당 데이터를 조작하고 싶을 경우가 있잖아요? 예를들면, 사용자에게 요청~응답까지 걸린 시간을 함께 전달해준다던가 하는 등 말이죠. 그럴 경우에도 인터셉터를 사용할 수 있습니다.

인터셉터는 이런식으로 "서비스 로직에 들어가기 전까지의 요청" 혹은 "서비스 로직을 거쳐 나온 응답"을 편의에 따라 조작해주는 역할을 합니다.

 

Pipe

파이프입니다. 관이죠? 파이프 안에서 어떤 일이 벌어질지 아무도 모르잖아요?

Nest.js에서는 "어떠한 데이터가 파이프를 거친다"라고 하면 데이터를 검증하거나 변형시킨다고 생각합니다.

예를들어, 1 이라는 숫자 데이터가 "숫자를 문자로 바꾸는 파이프"를 거치면, "1" 이라는 문자로 변형되어서 나오는 거죠.

검증의 경우는 1 이라는 숫자 데이터가 "숫자 형태의 데이터만 지나갈 수 있는 파이프"를 거치면 1 이 나오지만, "1" 이라는 문자열이 들어가면 파이프에서 나오지 못합니다.

파이프는 이런식으로 데이터의 검증 및 변형에 사용되는 녀석입니다.

 

주로 사용자의 요청에 담긴 데이터(body같은 데이터 등)가 개발자가 원하는 형태로 입력되어있는지 확인하거나, 개발자가 원하는 형태로 바꿀 때 사용합니다.

예를들어 개발자가 사용자 나이를 받고 싶은데 숫자 형태로 받고 싶다고 합시다. 그런데 사용자는 요청에 실수로 "20"이라는 문자열로 보냈습니다. 그러면 개발자는 숫자가 들어올 거라고 생각하고 코드를 짰는데, 문자열이 들어와버리면 심각한 에러를 초래할 수 있습니다. 파이프는 이를 방지하기 위해 존재합니다.

 

Exception Filter

익셉션 필터입니다. Exception. 예외죠! 예외를 처리해주는 녀석입니다.

서버에서 에러가 발생하면, 해당 에러는 Exception Filter로 들어가게 됩니다. Exception Filter 내부에서 해당 에러가 어떤 에러인지에 따라 사용자에게 에러 메시지를 다르게 보낸다던가 하는 등의 일을 합니다.

예상치 못한 에러를 잡아서 서버가 다운되지 않게 하는 역할도 수행하구요.

개발자가 의도한 에러를 따로 잡아서 원하는 형태로 커스텀해서 사용자에게 보내주기도 합니다.

직관적으로, 예외를 걸러주는 녀석이라고 생각합시다 ㅎㅎ

 

 

자, 그러면 이제 Nest.js의 동작 구조에 대해서 배워봅시다.

여기서는 Nest.js에 요청이 들어오면, 어떤식으로 처리가 되는지에 대해서 알아갈 수 있을 겁니다.

 

일단, Request lifecycle을 보면 아래 그림과 같습니다.

이런식으로 Nest.js는 이미 각자 목적에 맞는 미들웨어(Guards, Interceptors, Pipes 등)를 구현하기 위한 기능들이 준비되어 있기 때문에 비교적 편하게 코드를 작성할 수 있습니다.

 

먼저, HTTP Request가 서버로 들어옵니다. 그 후, 다음과 같은 미들웨어들을 거치게 됩니다.

  1. 글로벌 바운드 미들웨어 : 가장 바깥에 존재하는 미들웨어라고 생각하면 됩니다. (가드도 어떻게 보면 여기에 해당된다고 할 수 있겠네요)
  2. Guard : 정확히 말하면 Global Guard이지만, 일단 여기서는 그림에 맞춰서 설명하겠습니다.
  3. Interceptor : 가드를 통과한 요청을 Interceptor가 가로채서 조작합니다.
  4. Pipe : Interceptor에서 나온 요청은 Controller에 들어가기 전(서비스 로직을 실행시키기 전)에 유효성 검증과 변형을 합니다. 그래야 예상치 못한 에러를 사전에 방지할 수 있으니까요. 요청이 Pipe를 통과하지 못한다면, 사용자에게 에러를 던집니다.
  5. Controller : 사용자의 요청을 받아서 어떤 서비스를 전달해줄지 정합니다. 그리고 해당 서비스를 실행한 결과를 사용자에게 응답으로 돌려줍니다.
  6. Interceptors : 서버의 응답을 다시 가로채서 조작합니다. 아까 말했죠? 요청-응답의 실행시간을 추가해주거나, 형태를 바꾸는 등 조작을 해서 사용자에게 전달합니다.
  7. Exception Filter : 익셉션 필터는 사실 번외입니다. 어디서 어떠한 에러가 발생하든 무조건 익셉션 필터를 거칩니다. 그림에서 익셉션 필터가 미들웨어 전체를 감싸듯이, 어떤 미들웨어에서라도 에러가 발생하면 바로 익셉션 필터로 가서 사용자에게 에러가 발생했다는 응답을 줍니다.

과제


이제 저희는 간단한 블로그를 만드는 프로젝트를 진행할 겁니다.

그를 위해 테이블을 설계할 필요가 있겠죠.

 

블로그에는 "유저", "게시글", "댓글"에 대한 기능들을 만들 예정입니다.

 

각자 나름대로 DB에 대한 설계를 해보세요!

 

정답!은 없습니다. 물론... 해당 시리즈를 계속 이어서 하려면 제가 짠 테이블 구조를 따라야겠지만... 나름대로 한 번 생각정도는 해보자구요!