영원 웹 개발과 일상

Node.js process.Tick() 이란

☕ nextTickQueue 와 microTaskQueue

nextTickQueue는 process.nextTick() API의 콜백들을 가지고 있으며, microTaskQueue는 Resolve된 프로미스의 콜백을 가지고 있습니다.

이 두 개의 큐는 이벤트루프에 포함되어있지 않으며 이벤트루프에 앞서 실행하기 위한 것을 목적으로한다.

따라서 libUV 라이브러리에 포함되어 있지 않고 Node.js 에 포함되어있다.

👓 process.nextTick()

tick 은 반영구적인 event loop 에서 하나의 loop 를 의미합니다.

process.nextTick() 은 비동기 API 이지만 위 다이어그램의 어떠한 이벤트루프에도 속해있지 않습니다.

process.nextTick() 의 목적은 사용자의 동기코드 이후, 이벤트루프가 진행되기 이전 의 시간을 목적으로 합니다.

다음과 같은 상황을 보면

let bar;

function someAsyncApiCall(callback) {
  process.nextTick(callback);
}

someAsyncApiCall(() => {
  console.log("bar", bar); // 1
});

bar = 1;

사용자의 동기코드인 bar = 1; 이 수행된 이후에 someAsycnApiCall 에 전달된 callback 이 수행됩니다. 만약 process.nextTick(callback)callback() 으로 바꾸게 된다면 bar = 1; 이 실행되기 이전에 callback 이 수행될 수 있습니다.

process.nextTick() 은 이벤트루프 이전에 수행되기 때문에 반복적으로 호출하게 되면 poll 단계가 수행되지 않을 수 있으며 이는 I/O 단계를 Starving 상태로 만들 수 있습니다.

😃 process.nextTick() 그리고 setImmediate()

사실 두 함수의 이름은 바뀌어야 합니다. process.nextTick()이 setImmediate()보다 더 즉시 실행되지만 이미 수많은 코드들이 작성되어 있기 때문에 두 이름을 바꾼다면 npm 패키지에 잠재적으로 많은 문제가 발생할 수 있기 때문에 이름을 바꾸지 않는다고 합니다.

🙄 그런데 왜 쓰는가?

process.nextTick() 을 쓰는 이유는 단순합니다.

사용자가 이벤트루프를 진행하기 전에 수행할 작업이 필요하기 때문입니다.

다음 예를 봅시다.

const server = net.createServer();
server.on("connection", (conn) => {});

server.listen(8080);
server.on("listening", () => {});

server.listen(8080) 은 이벤트루프 시작 부분에서 수행될 것 입니다. server.on 으로 등록한 listening 콜백은 setImmedate() 로 수행이 됩니다. 따라서 server.listen(8080) 보다 먼저 적용될 수 있습니다.


피드백은 항상 환영입니다


출처


Node.js Event loop 파헤치기

🙄 그래서 Event loop 가 뭘까?

이전 포스트에서 Event loopepoll_wait loop 로서 커널에 관심사를 알려주고 커널에서 알림이 blocking 상태를 해제하고 적절한 javascript API 로 변환하는 과정이라고 했습니다. 쉽게 말하자면 Node.js 가 None Blocking I/O 작업을 수행하도록 해주는 과정입니다.

Event loop 더 고수준에서 다이어그램으로 보자면 아래와 같습니다.

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

이벤트루프는 저수준 c 코드에서 일어나는 일이며 사실 더 복잡한 과정을 가지고 있습니다만 Node.js 공식 문서에서는 위 과정이 Event loop 의 가장 중요한 단계 를 의미한다고 합니다.

모든 단계는 특징을 가지고 있습니다.

  • 각 단계는 실행할 콜백의 FIFO 큐를 가집니다.
  • 각 단계는 각각 자신만의 특징을 가지고 있으며 해당 단계에 진입시 그 단계에서 행하는 동작을 수행하고 콜백을 실행합니다.
  • 각 단계에서는 큐를 모두 소진하거나 콜백의 최대 개수를 수행할 때까지 콜백을 실행합니다. 큐를 모두 소진하거나 콜백 제한에 이르면 이벤트 루프는 다음 단계로 이동합니다.

👓각 단계 파헤치기.

각 단계의 개요

  • timers: 이 단계는 setTimeout()과 setInterval()로 스케줄링한 콜백을 실행합니다.
  • pending callbacks: 다음 루프 반복으로 연기된 I/O 콜백들을 실행합니다.
  • idle, prepare: 내부용으로만 사용합니다.
  • poll: 새로운 I/O 이벤트를 가져옵니다. I/O와 연관된 콜백(클로즈 콜백, 타이머로 스케줄링된 콜백, setImmediate()를 제외한 거의 모든 콜백)을 실행합니다. 적절한 시기에 node는 여기서 블록 합니다.
  • check: setImmediate() 콜백은 여기서 호출됩니다.
  • close callbacks: 일부 close 콜백들, 예를 들어 socket.on(‘close’, …).

Timers

타이머는 제공된 콜백이 일정 시간 후 실행되어야 하는 시간을 지정합니다. 이 시간은 운영체제 스케쥴링이나 다른 콜백 실행 때문에 지연될 수 있으므로 정확한 시간이 아닙니다.

예를 들어 100ms 이후 실행되도록 시간을 지정하고 95ms 가 걸리는 파읽 읽기를 비동기로 수행한다고 가정합니다.

const fs = require("fs");

function someAsyncOperation(callback) {
  // 이 작업이 완료되는데 95ms가 걸린다고 가정합니다.
  fs.readFile("/path/to/file", callback);
}

const timeoutScheduled = Date.now();

setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;

  console.log(`${delay}ms have passed since I was scheduled`);
}, 100);

// 완료하는데 95ms가 걸리는 someAsyncOperation를 실행합니다.
someAsyncOperation(() => {
  const startCallback = Date.now();

  // 10ms가 걸릴 어떤 작업을 합니다.
  while (Date.now() - startCallback < 10) {
    // 아무것도 하지 않습니다.
  }
});

someAsyncOperation 에서 95ms 이 걸리는 fs.readFile 을 수행하고 10ms 를 소요하는 콜백을 다시 수행합니다.

해당 과정은 poll 과정에서 일어나고, poll 임계점에 다다르지 않았다고 가정합니다.

콜백이 완료되면 105ms 를 소요하게 되고 poll 큐에 아무것도 없기 때문에 poll 단계는 종료됩니다.

이제 이벤트루프는 check -> close callback 단계를 거쳐 다시 timers 단계로 돌아가며 100ms 로 지정한 콜백은 105ms 이후 실행되었기에 timers 의 시간은 정확한 시간이 아니라 콜백을 실항할 때 까지 걸리는 최소시간 이라는 것을 알 수 있습니다.


Pending 콜백

이름에서 알 수 있다시피 콜백의 완전히 완료하지 못하고 연기된 콜백을 실행하는 단계 입니다.

poll

다음 두 가지 기능을 합니다.

  • I/O 를 얼마나 오래 block 하고 polling 해야하는지 계산합니다.
  • poll 큐에 있는 이벤트를 처리합니다.

poll 단계에서는

  • poll 큐가 비어있지 않다면 콜백의 큐를 모두 소진하거나 임계점에 도달할 때 까지 동기로 콜백을 수행합니다.
  • poll 큐가 비어있다면 다음 단계로 넘어가거나 콜백이 poll 큐에 추가되길 기다린 후 즉시 실행합니다.

poll 큐가 다음 단계로 간다는 말은 하나 이상의 timers 가 준비되어있거나 setImmediate() 가 스케쥴링 되어 있다는 뜻 입니다.

check

poll 단계가 수행된 후에 콜백을 수행하기 위한 단계입니다. setImmediate() 를 통해 이 단계를 수행할 수 있습니다.

setImmediate() 는 setTimeout() 와 다른 단계에서 수행이 되며 동일한 I/O 주기 내에서 둘을 같이 호출 한다면 setImmidate() 가 항상 먼저 실행됩니다.

close

소켓 또는 핸들이 닫힌 경우와 같이 close 이벤트를 처리하기 위한 단계입니다.


😁그림으로

Bert Belder 의 강연 에서는 다음과 같은 그림을 통해 설명하고 있습니다.

이벤트루프


Node.js 는 이벤트루프를 libUV 라이브러리를 통해 수행하고 있습니다. libUV 에 대해 더 알고 싶다면 libUV Doc 를 참고해주세요.

피드백은 환영입니다.


출처


Node.js Event loop 저수준에서 파헤치기

시작하기 앞서 이 포스트는 Node.js 유튜브 영상 을 참조하고 만든 것을 알려드립니다.!!

기존의 방법들

Node.js 의 Event Loop 가 Scale 측면에서 우수하다 라는 말을 알기 위해서는 기존에는 요청을 어떻게 처리 했는지를 이해하면 도움이 많이 됩니다.

TCP Connection 과정을 기존에 어떻게 처리 했는지 봅시다.

    int server = socket();
    bind(server, 80)
    listen(server)

    while(int connection = accept(server)) {
        do_something(connection);
    }

위의 의사코드 작동을 보면

  • server 의 소켓을 생성하고 포트바인드를 한다.
  • 그리고 Listen 상태로서 요청이 오는지 확인하며 대기한다.
  • 요청이 오면 요청을 처리한다.

TCP connection 요청을 받고 상태는 accept connection 됩니다. accept connection 은 system call 이고 system call 은 프로그램을 block 할 수 있습니다. 즉 do_something 이 끝날 때까지 다른 무언가를 할 수 없는 상태가 됩니다. 예를 들어 10 초가 걸리는 요청이 있다면 그 요청을 끝내기 전까지는 다른 요청을 처리하지 못하게 될 수 있습니다.

멀티 쓰레드

자 이제 Multi Thread 개념을 생각해봅시다. 만약 새로운 Connection 마다 새로운 Thread 를 생성해서 할당하고 그 Thread 에게 요청을 맡기고 요청이 끝나면 다시 Thread 를 회수한다고 생각해봅시다.

Main Thread 에서는 Connection 을 기다리고 Connection 요청이 오면 새로운 Thread 를 생성하고 요청을 할당한 후 다시 Connection 을 기다린다면 요청이 끝나기 전 다른 요청을 받을 수 있습니다.

    int server = socket();
    bind(server, 80)
    listen(server)

    while(int connection = accept(server)) {
        pthread_create(echo, connection);
    }
    void echo(int connection) {
        char buf[4096];
        while(int size = read(connection, buffer, sizeof buf)) {
            write(conection, buf, size);
        }
    }

Multi Thread 환경에서도 문제가 있습니다. Thread 를 생성하고 할당하는 과정은 우리가 다룰 데이터에 비하면 상대적으로 상당히 무거운 과정입니다.

예를 들어 정말 간단한 작업을 요청을 하더라도 Thread 를 생성해야합니다. Thread 를 관리하기 위한 메모리 할당, Thread 간 Context switching 과정 등에서 들어갈 작업 등..을 생각해보면 비효율적일 수도 있습니다. 임계 Thread 양보다 많은 요청이 들어온다면 요청을 처리할 수도 없게 됩니다.

Epoll

Scale 관점의 문제를 해결하기 위해서 Epoll 이라는 것을 알아봅시다. Epoll 은 I/O 통지 모델로서 커널 수준에서 file descriptor를 관리하게 됩니다.

epoll 이 하는 역할은 epoll descriptor 를 만들고 커널에 우리가 어떤 이벤트에 관심이 있는지 말해줄게!! 그 이벤트가 발생하면 나에게 알려줘!! 하는 역할을 하게 됩니다. 다시 말해 커널에게 작업을 맡기겠다는 말입니다. 커널에 정보를 알려주고 다시 요청을 받을 준비를 하게 됩니다. Epoll 을 이용하면 cpu 자원 또한 아끼게 됩니다.

Epoll 에 대해서는 다음 블로그를 참고해주세요 . Epoll 알아보기

이를 Event loop 라 하며 각각의 루프를 tick 이라 합니다. 아래와 같은 식이라 생각하면 됩니다. (사실 더 복잡하지만 간략화 했습니다.)

struct epoll_event_events[10];

while((int max = epoll_wait(eventfd, events, 10))) {
    for(n = 0; n < max; n++) {
        if(events[n].data.fd.fd == server) {
            //Server socket has connection!!
            int connection = accept(server);
            ev.events = EPOLLIN;
            ev.data.fd = connection;
            epoll_ctl(eventfd,EPOLL_CTL_ADD, connection, &ev);
        } else {
            //Connection socket has data
            char buf[4096];
            int size = read(connection, buffer, sizeof buf);
            write(connection, buffer, size);
        }
    }
}

Event Loop 의 정체

이 반영구적인 loop 가 event loop 의 정체입니다. 커널에 관심사를 알려주고 관심사가 일어날 때까지 blocking 하며 대기상태로 진입한 후 관심사가 일어나면 Node.js 가 그 관심사를 자바스크립트 api 로 변환하게 되는 겁니다.

이 loop 는 더이상 event 를 기다려도 되지 않을 때 까지 반복됩니다.

Node.js 도 Multi Thread 를 이용한다.

충격적이겠지만 사실 Node.js 에도 Thread poll 이라는 것이 존재합니다. Epoll 을 통해 관리할 수 없는 유형의 작업은 이 Thread poll 의 Thread 에 할당하여 처리하게 됩니다.

Pollable and None Pollable

  • File System : fs.* 의 모든 것은 불가능합니다. 해당 유형의 요청은 Thread 로 넘겨지게 되며 Thread 는 blocking 됩니다.
  • Dns.Lookup calls : request(“hostname”)… 의 요청인 경우 Dns 를 찾아야하기 때문에 None Pollable 합니다. 다만 Ip 를 통한 직접적인 요청인 경우는 Pollable 합니다.
  • crypto : crypto.randomBytes(), crypto.pbkdf2() 등 일부 유형에서 None Pollable 합니다.
  • any c++ addons that use it : Not Pollable 합니다.

이외에도 여러 유형이 있습니다. 매우 cpu intensive 한 작업에서 None pollable 한 경우가 많았습니다. 만약 None Pollable 한 작업을 많이 요구하는 Node.js 어플리케이션의 경우는 Thread poll size 를 늘리는 것도 생각해야합니다.

또한 Event Loop Time 시간을 잘 고려해서 내 어플리케이션이 어디서 Blocking 이 많이 되는가를 모니터링 해야 한다는군요.


Node.js 의 이벤트 루프를 공부하다가 발견한 영상을 토대로 정리했습니다. 미흡한 정보는 이후 포스트를 업데이트 하며 추가/수정 하겠습니다. 피드백은 항상 환영입니다.

이후에는 고수준에서 Event loop 를 살펴보겠습니다.


출처


Redux 기본서!! (feat. react context hook)

🔨 Redux 는 이렇게 작동합니다.

기본 Redux 는 간단하게 작용합니다. 중앙화 된 상태저장소가 있고 각각의 컴포넌트들이 그 상태저장소에 접근해서 원하는 것을 가져올 수 있습니다. 자식들에게 상태(state)와 자산(props)을 물려줄 필요는 없습니다.

Redux 를 구성하는 3가지 요소가 있습니다.

  • Actions
  • Store
  • Reducers

🙂 Actions

간단하게 Actions 는 이벤트 데이터 입니다. 데이터는 심플한 Javascript Object 입니다. 다음처럼요.

{
  type: REQUEST_ROBOTS_SUCCESS,
  isPending : false,
  payload: {
      robots : [some api call results]
}

이 Actions 를 dispatch 라는 함수를 통해 Redux Store 로 보내게 됩니다.

dispatch({
  type: REQUEST_ROBOTS_SUCCESS,
  isPending : false,
  payload: {
      robots : [some api call results]
  }
});

🙂 Reducers

Reducers 는 순수함수입니다. 이전 상태 값을 받아 새로운 상태 값을 리턴하는 함수입니다.

순수함수란 값을 받아서 어떤 값을 다시 리턴 해주는 함수이며, 이때 동일한 값을 받으면 그 값에 대해서 항상 같은 값을 리턴해준다면 이를 순수함수라고 합니다..

dispatch 를 통해 action 을 받은 Reducer 는 action 의 타입에 따라 다른 행동을 합니다.

reqeustBots 라는 Reducer 를 예를 들면,

export const requestRobots = (state=initialStateRobots, action={}) => {
  switch (action.type) {
    case REQUEST_ROBOTS_PENDING:
      return Object.assign({}, state, {isPending: true})
    case REQUEST_ROBOTS_SUCCESS:
      return Object.assign({}, state, {robots: action.payload, isPending: false})
    case REQUEST_ROBOTS_FAILED:
      return Object.assign({}, state, {error: action.payload})
    default:
      return state
  }

이처럼 생겼으며 순수함수 이므로 state 를 변화시킨다기 보다는 새로운 state 를 만들어 리턴하게 됩니다.

🙂 Store

store 는 앱의 state 를 가지고 있는 저장소입니다.

const store = createStore(Reducers);

이 store 를 Redux 의 Provider 를 통해 앱으로 전달하게 됩니다.

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

App 컴포넌트에서는 이 store 에 접근하기 위해 react-redux 라이브러리의 connect 함수를 통해 연결하게 됩니다.

const mapStateToProps = (state) => {
  return {
    searchField: state.searchRobots.searchField,
    robots: state.requestRobots.robots,
    isPending: state.requestRobots.isPending,
  };
};

// dispatch the DOM changes to call an action. note mapStateToProps returns object, mapDispatchToProps returns function
// the function returns an object then uses connect to change the data from redecers.
const mapDispatchToProps = (dispatch) => {
  return {
    onSearchChange: (event) => dispatch(setSearchField(event.target.value)),
    onRequestRobots: () => dispatch(requestRobots()),
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(App);
  • mapStateToProps : 우리가 이 컴포넌트에서 사용할 state.
  • mapDispatchToProps : 우리가 이 컴포넌트에서 사용할 actions 들을 dispatch 할 정보를 담은 함수.

🙂 Redux Middleware

Redux 에서는 Action 이 dispatch 되기 전 이를 가로채 가공할 수 있는 middleware 기능이 있습니다.

다음과 같이 사용 가능합니다.

import thunkMiddleware from "redux-thunk";
import { createLogger } from "redux-logger";
const store = createStore(
  rootReducers,
  applyMiddleware(thunkMiddleware, logger)
);

redux-thunk 는 비동기 수행에, redux-logger 는 로그를 남기기 위해 사용하는 middleware 입니다.

🙄 React Context hook

React 의 Context hook 또한 상태 관리를 위한 목적을 가지고 있습니다. Redux 에서와 비슷하게 Provider / Consumer 패턴으로 state 를 제공/ 소비를 할 수 있습니다.

Redux 와 비슷하게 state 를 관리할 수 있습니다. 그럼 왜 Redux 를 아직 사용할까요??

Redux 의 장점

  • Debugging 과 Testing 하는데 편리합니다. Redux DevTools 또는 logger 등을 이용한다면 우리 앱에서 컴포넌트의 전환과정 사이에 어떤 일이 일어나는지 알 수 있습니다.
  • Redux 는 어떻게 코드가 작성되어야 하는지 강요하는 구조입니다. 큰 규모의 앱에서 관리하기 수월합니다.
  • Redux 내부적으로 최적화가 되어있고, 필요한 컴포넌트만 Rerender 할 수 있기 때문에 성능상에서 약간의 이득을 볼 수 있습니다.
  • 로컬 스토리지에 상태를 영속적으로 저장하고 불러오는데 뛰어납니다.
  • 초기 상태를 서버에 전송하여 필요한 컴포넌트를 render 할 수 있습니다. 따라서 SSR 에 강점이 있습니다.

위의 장점이 필요 없다면 React hooks 만 써도 됩니다. 필요 이상의 일을 할 필요가 없어지는 것 입니다.

그래서 모든 개발자가 React hooks 가 Redux 를 대체할 수 있나요? 라는 질문에 “아니요” 라고 대답하는 것 같습니다.

다만 React hooks 또는 React 관련 기능이 더 성장한다면 Redux 를 대체할 수도 있지 않을까 생각합니다.


참고 :

참고 예제 :


NestJS 블로그 만들기 - 리팩토링과 e2e 테스트!!

2. NestJS 블로그 만들기 - 리팩토링과 e2e 테스트!!

이번에는 이전 포스트에서 다뤘던 내용에 대해서 Refactoring 을 하고 테스트도 해보겠습니다.

🔨 Refactoring !!

└─modules
    └─user
        │  user.controller.ts
        │  user.module.ts
        │  user.repository.ts
        │  user.service.ts
        │  user.spec.ts
        │
        ├─dto
        │      create-user.dto.ts
        │      index.ts
        │      update-user.dto.ts
        │
        ├─entities
        │      user.entity.ts
        │
        ├─exceptions
            email-already-exist-exception.ts
            user-not-found.exception.ts
            username-already-exist-exception.ts

계속 리팩토링 하다보니 위와 같은 구조가 만들어졌습니다. 파일 이름이 바뀐 것도 있고, 내부 구현사항이 바뀐 것도 있습니다. 리팩토링 하면서 설계의 중요성을 다시 느낍니다.

user.exceptions

Custom Exception 을 만들기 위해 user 폴더 안에 exceptions 폴더를 만듭시다!

이메일과 유저네임이 중복 되었을 때 그리고 유저를 찾지 못했을 때 발생 시킬 exception 을 각각 만들어 줍시다.

  • username-already-exist-exception.ts
import { BadRequestException } from "@nestjs/common";

export class UsernameAlreadyExistException extends BadRequestException {
  constructor(error?: string) {
    super("username already exist", error);
  }
}
  • email-already-exist-exception.ts
import { BadRequestException } from "@nestjs/common";

export class EmailAlreadyExistException extends BadRequestException {
  constructor(error?: string) {
    super("email already exist", error);
  }
}
  • user-not-found.exception.ts
import { BadRequestException } from "@nestjs/common";

export class UserNotFoundException extends BadRequestException {
  constructor(error?: string) {
    super("user not found", error);
  }
}

각각의 클래스는 BadRequestException 을 상속받고 있습니다. 해당 Exception 을 발생시키면 Status Code 는 400(BadRequest) 이 발생됩니다.

매우 간단하게 Custom Exception 을 만들었습니다!!

user.service

create 메소드 안에서 이전에 만들었던 QueryBuilder 부분을 삭제하고 바로 위에서 만들었던 Custom Exceptions 들을 사용하여 간단하게 만들어줍니다.

  • UserService.create
const thisUser = await this.userRepository.findOne({ username: username });
if (thisUser) {
  const error = "UserName is already exist";
  throw new UsernameAlreadyExistException(error);
}
const thisEmail = await this.userRepository.findOne({ email: email });
if (thisEmail) {
  const error = "Email is already exist";
  throw new EmailAlreadyExistException(error);
}

그리고 유저를 저장하여 반환할 때 단순 number 값이 아니라 Object 를 반환해줍시다.

const userId = await this.userRepository.save(newUser).then((v) => v.id);
return { userId: userId };

테스트를 위해 remove 메소드 안의 내용을 다음과 같이 만들어 줍시다.

이후 Auth 관련 기능을 만들면서 remove 기능은 사라질겁니다…

  • UserService.remove
async remove(email: string): Promise<DeleteResult> {
    return await this.userRepository.delete({ email: email });
}

user.controller

remove 와 관련된 controller 부분을 다음으로 변경해주세요!

  • UsersController.remove
  @Delete('')
  remove(@Body('email') email: string) {
    return this.usersService.remove(email);
  }

@Body annotation 은 response body 안에서 해당하는 필드를 가져와 변수에 입력해주는 기능을 합니다!!

🧡🧡새해 첫 테스트 두근두근

먼저 유닛테스트가 아닌, e2e 테스트를 진행하기 위한 코드임을 알려드립니다. 테스트 관련 기술이 많이 부족해서 계속 공부중입니다. ㅜㅜ 더 알아가면서 포스트를 업데이트 하도록 하겠습니다!!

테스트는 해당 url 로 요청 수행시 적절한 응답이 오는 확인합니다.

e2e 테스트 방법에 정석이 있겠지만 진행 도중 많은 오류가 발생하여.. 일단 야매로 진행하겠습니다.

서버실행 -> url로 요청

요청을 수행하기 위해서 supertest 라이브러리를 먼저 설치합시댜!!

$ npm install --save-dev supertest

supertest 관한 내용은 다음링크에서 더 확인 가능합니다!! npm supertest

그리고 테스트 하기 위한 nest 라이브러리도 설치합시다 !!

$ npm i --save-dev @nestjs/testing

이제 user.spec.ts 라는 파일을 user 폴더 안에 생성하여 다음과 같은 코드를 작성합시다!

import * as request from "supertest";
const app = "http://localhost:3000";
describe("User create 테스트", () => {
  beforeEach(async () => {
    await request(app).delete("/users").send({ email: "test1@example.com" });
    await request(app).delete("/users").send({ email: "test2@example.com" });
  });

  it("email 중복 확인", async () => {
    await request(app)
      .post("/users")
      .send({
        email: "test1@example.com",
        username: "testuser",
        password: "12345",
      })
      .expect(201);

    const res = await request(app)
      .post("/users")
      .send({
        email: "test1@example.com",
        username: "abcd",
        password: "12345",
      })
      .expect(400);
    expect(res.body.error).toBe("Email is already exist");
  });

  it("username 중복 확인", async () => {
    await request(app)
      .post("/users")
      .send({
        email: "test2@example.com",
        username: "testuser",
        password: "12345",
      })
      .expect(201);

    const res = await request(app)
      .post("/users")
      .send({
        email: "test1@example.com",
        username: "testuser",
        password: "12345",
      })
      .expect(400);
    expect(res.body.error).toBe("UserName is already exist");
  });
});
  • describe 메소드로 테스트 할 놈들을 그룹화 해줍니다.
  • beforeEach 메소드를 통해 각 테스트를 수행하기 전 테스트 유저를 삭제해줍니다.
  • it 메소드를 통해 각각 테스트를 수행합니다. ittest 메소드와 같은 기능을 하며 별칭입니다.

자 이제 테스트를 시작해봅시다!!

먼저 서버실행!!

$ npm run start

그리고 테스트도 실행 !!

$ npm run test
> nestjs-practice@0.0.1 test C:\Users\jiyoung\nestjs-practice
> jest

PASS  src/modules/user/user.spec.ts
User create 테스트
    √ email 중복 확인 (173 ms)
    √ username 중복 확인 (24 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.392 s, estimated 5 s
Ran all test suites.

다음과 같이 통과하면 성공입니다!!

package.json 에서 jest 관련 설정을 spec 또는 test 가 붙은 ts 파일로 설정했기 때문에 해당하는 파일은 모두 테스트하게 됩니다!!


테스트 관련해서는 아직 공부하고 있습니다 !! e2e 테스트를 먼저 해보기 위해 시도는 많이 했으나 그만큼 오류가 많이 나더군요 ㅜㅜ 위와 같은 야매 방법을 통해 테스트를 했으나, 이후에 해결방법을 알아와서 포스트를 업데이트 하겠습니다!!

또한 이후에는 Unit Test 관련 기능도 알아보겠습니다!

다음에는 post 모듈을 만들어봅시다!! 감사합니다.

피드백은 항상 환영입니다.