견고한 node.js 프로젝트 설계하기

hopsprings2·2020년 1월 21일
159
post-thumbnail

본 글은 Sam Quinn의 “Bulletproof node.js project architecture” 글을 번역한 것입니다.
Bulletproof node.js project architecture 🛡️
Express.js is great frameworks for making a node.js REST APIs however it doesn't give you any clue on how to organizing…softwareontheroad.com

Introduction

Express.js는 node.js REST API를 만드는데 좋은 프레임워크지만, 어떻게 node.js 프로젝트 구조를 잡아야 할지 알려주지 않습니다.

우습게 들릴 수도 있지만, 이건 매우 큰 문제입니다.

올바른 node.js 프로젝트 구조는 코드의 중복을 피해주고 안정성을 높여주며, 당신의 서비스를 확장하는데 도움이 될 것입니다.

이 포스트는 다년간의 부족했던 설계와 나쁜 패턴, 그리고 수없이 많은 코드 리팩토링 경험을 통해 쓰여진 하나의 리서치입니다.

목차

  • 폴더 구조

  • 3 계층 설계 (3 Layer Architecture)

  • Service 계층

  • Pub/Sub 계층

  • 의존성 주입 (Dependency Injection)

  • Unit Testing

  • 스케줄링 및 반복 작업 (Cron Jobs and Recurring Task)

  • 설정 및 시크릿 파일 (Configurations and secrets)

  • Loaders

  • 예제

폴더 구조 🏢

앞으로 설명할 node.js의 프로젝트 구조는 다음과 같습니다.

개발하는 모든 REST API 서비스에서 다음과 같은 구조를 유지합니다. 이제 각각의 컴포넌트가 어떤 역할을 하는지 자세히 살펴보도록 하겠습니다.

src
│   app.js          # App entry point
└───api             # Express route controllers for all the endpoints of the app
└───config          # Environment variables and configuration related stuff
└───jobs            # Jobs definitions for agenda.js
└───loaders         # Split the startup process into modules
└───models          # Database models
└───services        # All the business logic is here
└───subscribers     # Event handlers for async task
└───types           # Type declaration files (d.ts) for Typescript

이는 단순히 파일들을 정렬한 것이 아닙니다.

3 계층 설계 (3 Layer Architecture) 🥪

관심사 분리 원칙(principle of separation of concerns)를 적용하기 위해 비지니스 로직을 node.js의 API Routes와 분리해줍니다.

언젠가는 반복되는 작업을 하다보면 CLI 도구를 통해 비지니스 로직을 사용하고 싶어할 것이기 때문입니다.

그리고 node.js server에서 API 호출을 하는 것은 좋은 생각은 아닙니다.

☠️ 비지니스 로직을 controller에 넣지 마십시오!! ☠️

아마 express.js controllers에 바로 애플리케이션의 비지니스 로직을 구현하고 싶을 수 있습니다. 하지만 이렇게 코드를 작성하면 스파케티 코드가 되기 마련인데요, 유닛 테스트를 작성하다 보면 수많은 express.js의 reqres 오브젝트를 다루게 될 것이기 때문입니다.

언제 클라이언트로 response를 보내야 할지, 그리고 언제 프로세스를 백그라운드에서 계속 실행해야 할지 구분하는 것은 매우 어렵습니다. 클라이언트로 response를 보낸 후에 프로세스 작업을 계속 하기로 했다고 가정 해봅시다.

다음은 하지 말아야 할 예시입니다.

route.post('/', async (req, res, next) => {

  // This should be a middleware or should be handled by a library like Joi.
  const userDTO = req.body;
  const isUserValid = validators.user(userDTO)
  if(!isUserValid) {
    return res.status(400).end();
  }

  // Lot of business logic here...
  const userRecord = await UserModel.create(userDTO);
  delete userRecord.password;
  delete userRecord.salt;
  const companyRecord = await CompanyModel.create(userRecord);
  const companyDashboard = await CompanyDashboard.create(userRecord, companyRecord);

  ...whatever...


  // And here is the 'optimization' that mess up everything.
  // The response is sent to client...
  res.json({ user: userRecord, company: companyRecord });

  // But code execution continues :(
  const salaryRecord = await SalaryModel.create(userRecord, companyRecord);
  eventTracker.track('user_signup',userRecord,companyRecord,salaryRecord);
  intercom.createUser(userRecord);
  gaAnalytics.event('user_signup',userRecord);
  await EmailService.startSignupSequence(userRecord)
});

Service 계층에 비지니스 로직을 넣으십시오 💼

비지니스 로직은 service 계층에 있어야 합니다.

이는 분명한 목적이 있는 클래스들의 집합이며, SOLID 원칙을 node.js에 적용한 것입니다.

이 레이어에는 ‘SQL query’ 형태의 코드가 있어서는 안됩니다. 그것은 data access layer에서 사용해야 합니다.

  • 코드를 express.js router에서 분리하십시오.

  • service 레이어에는 req와 res 객체를 전달하지 마십시오.

  • 상태 코드 또는 헤더와 같은 HTTP 전송 계층과 관련된 것들은 반환하지 마십시오.

Example

route.post('/', 
  validators.userSignup, // this middleware take care of validation
  async (req, res, next) => {
    // The actual responsability of the route layer.
    const userDTO = req.body;

    // Call to service layer.
    // Abstraction on how to access the data layer and the business logic.
    const { user, company } = await UserService.Signup(userDTO);

    // Return a response to client.
    return res.json({ user, company });
  });

다음은 service가 작동하는 방법입니다.

import UserModel from '../models/user';
import CompanyModel from '../models/company';

export default class UserService() {

  async Signup(user) {
    const userRecord = await UserModel.create(user);
    const companyRecord = await CompanyModel.create(userRecord); // needs userRecord to have the database id 
    const salaryRecord = await SalaryModel.create(userRecord, companyRecord); // depends on user and company to be created
    
    ...whatever
    
    await EmailService.startSignupSequence(userRecord)

    ...do more stuff

    return { user: userRecord, company: companyRecord };
  }
}

클릭하시면 예제 repository를 보실 수 있습니다.

Pub/Sub 계층도 사용하십시오 🎙️

pub/sub 패턴을 전형적인 3계층 구조 범위를 넘어서지만 매우 유용합니다.

간단한 node.js API 앤드포인트에서 사용자를 생성한 뒤, third-party 서비스를 호출하거나, 서비스 분석을 시도하거나, 이메일 전송과 같은 작업을 하고 싶을 수 있습니다.

금세 간단한 “create” 작업이 여러 가지 일을 하기 시작할 것이며, 하나의 함수 안에 1000줄이 넘어가는 코드가 생길 것입니다.

이는 단일 책임 원칙(principle of single responsibility)를 위배합니다.

시작부터 책임들을 분리하여 간결하게 코드를 유지 관리 할 수 있습니다.

import UserModel from '../models/user';
import CompanyModel from '../models/company';
import SalaryModel from '../models/salary';

export default class UserService() {

  async Signup(user) {
    const userRecord = await UserModel.create(user);
    const companyRecord = await CompanyModel.create(user);
    const salaryRecord = await SalaryModel.create(user, salary);

    eventTracker.track(
      'user_signup',
      userRecord,
      companyRecord,
      salaryRecord
    );

    intercom.createUser(
      userRecord
    );

    gaAnalytics.event(
      'user_signup',
      userRecord
    );
    
    await EmailService.startSignupSequence(userRecord)

    ...more stuff

    return { user: userRecord, company: companyRecord };
  }

}

독립적인 서비스들을 직접적으로 호출하는 것이 최선의 방법은 아닙니다.

더 좋은 접근법은 이벤트를 발생시키는 것입니다.

이렇게 한다면 이제 리스너들이 그들의 역할을 책임지게 됩니다.

import UserModel from '../models/user';
import CompanyModel from '../models/company';
import SalaryModel from '../models/salary';

export default class UserService() {

  async Signup(user) {
    const userRecord = await this.userModel.create(user);
    const companyRecord = await this.companyModel.create(user);
    this.eventEmitter.emit('user_signup', { user: userRecord, company: companyRecord })
    return userRecord
  }
}

이제 이벤트 핸들러/리스너들은 여러 파일로 나눌 수 있습니다.

eventEmitter.on('user_signup', ({ user, company }) => {

  eventTracker.track(
    'user_signup',
    user,
    company,
  );

  intercom.createUser(
    user
  );

  gaAnalytics.event(
    'user_signup',
    user
  );
})
eventEmitter.on('user_signup', async ({ user, company }) => {
  const salaryRecord = await SalaryModel.create(user, company);
})
eventEmitter.on('user_signup', async ({ user, company }) => {
  await EmailService.startSignupSequence(user)
})

await 구문을 try-catch block으로 감싸거나 ‘unhandledPromise’ 를 process.on(‘unhandledRejection’, cb)로 처리해 줄 수 있습니다.

의존성 주입 💉

의존성 주입(D.I), 또는 제어 역전(IoC)은 코드를 구조화하는데 많이 사용하는 패턴인데요, 생성자를 통해 클래스와 함수의 의존성을 전달해주는 방식입니다.

이를 통해 ‘호환 가능한 의존성(compatible dependency)'을 주입함으로써 유연하게 코드를 유지할 수 있습니다. 이는 service에 대한 유닛 테스트를 작성하거나, 다른 context에서 코드를 사용할 때 도움이 됩니다.

의존성 주입을 사용하지 않을 때

import UserModel from '../models/user';
import CompanyModel from '../models/company';
import SalaryModel from '../models/salary';  
class UserService {
  constructor(){}
  Sigup(){
    // Caling UserMode, CompanyModel, etc
    ...
  }
}

의존성 주입을 사용할 때

export default class UserService {
  constructor(userModel, companyModel, salaryModel){
    this.userModel = userModel;
    this.companyModel = companyModel;
    this.salaryModel = salaryModel;
  }
  getMyUser(userId){
    // models available throug 'this'
    const user = this.userModel.findById(userId);
    return user;
  }
}

다음과 같이 직접 의존성을 주입해서 사용할 수 있습니다.

import UserService from '../services/user';
import UserModel from '../models/user';
import CompanyModel from '../models/company';
const salaryModelMock = {
  calculateNetSalary(){
    return 42;
  }
}
const userServiceInstance = new UserService(userModel, companyModel, salaryModelMock);
const user = await userServiceInstance.getMyUser('12346');

하지만 서비스가 가질 수있는 종속성의 양은 무한하며 새 인스턴스를 추가 할 때 서비스의 모든 인스턴스화를 리팩터링하는 것은 지루하고 오류가 발생하기 쉬운 작업입니다.

이 때문에 의존성 주입 프레임워크가 생기게 되었습니다.

단지 필요한 의존성만을 사용하는 사람이 직접 클래스에 선언하면 되고, 해당 클래스의 인스턴스가 필요할 때면 ‘Service Locator’를 호출하기만 하면 됩니다.

node.js에 의존성을 사용할 수 있게 해주는 npm 라이브러리 typedi의 예시를 살펴봅시다.

공식 문서에서 typedi 사용법을 읽어보실 수 있습니다.

WARNING typescript example

import { Service } from 'typedi';
@Service()
export default class UserService {
  constructor(
    private userModel,
    private companyModel, 
    private salaryModel
  ){}

  getMyUser(userId){
    const user = this.userModel.findById(userId);
    return user;
  }
}

services/user.ts

이제 typedi는 UserService에 필요한 모든 종속성을 해결해줍니다.

잘못된 service locator 호출은 좋지 않은 패턴(anti-pattern)입니다.

Node.js의 Express.js에서 의존성 주입 사용하기

의존성 주입을 express.js에서 사용하는 것이 node.js 프로젝트 설계의 마지막 관문입니다.

Routing layer

route.post('/', 
  async (req, res, next) => {
    const userDTO = req.body;

    const userServiceInstance = Container.get(UserService) // Service locator

    const { user, company } = userServiceInstance.Signup(userDTO);

    return res.json({ user, company });
  });

흘륭합니다. 너무나 잘 구조화가 되어 빨리 코딩하고 싶어집니다.

유닛 테스트 예제 🕵🏻

이런 구조를 유지하고 의존성 주입을 사용하게 되면, 유닛 테스트는 정말 간단해집니다.

res/res 객체들과 require 호출들을 할 필요가 없습니다.

Example: Unit test for signup user method

tests/unit/services/user.js

import UserService from '../../../src/services/user';

describe('User service unit tests', () => {
  describe('Signup', () => {
    test('Should create user record and emit user_signup event', async () => {
      const eventEmitterService = {
        emit: jest.fn(),
      };

      const userModel = {
        create: (user) => {
          return {
            ...user,
            _id: 'mock-user-id'
          }
        },
      };

      const companyModel = {
        create: (user) => {
          return {
            owner: user._id,
            companyTaxId: '12345',
          }
        },
      };

      const userInput= {
        fullname: 'User Unit Test',
        email: 'test@example.com',
      };

      const userService = new UserService(userModel, companyModel, eventEmitterService);
      const userRecord = await userService.SignUp(teamId.toHexString(), userInput);

      expect(userRecord).toBeDefined();
      expect(userRecord._id).toBeDefined();
      expect(eventEmitterService.emit).toBeCalled();
    });
  })
})

스케줄링 및 반복 작업 ⚡

비지니스 로직이 service layer에 캡슐화 되었습니다. 이는 스케줄링 작업(cron jobs)들을 더 하기 쉽게 해줍니다.

코드 실행을 지연시키는 작업들을 할 때는 절대로 node.js의 setTimeout* *이나 다른 원시적인 방법(primitive way)을 사용하면 안 됩니다. 대신에 데이터베이스에서 작업을 유지하고 실행하는 프레임워크를 사용해야 합니다.

이 방식을 통해 실패한 작업을 제어하고, 성공한 작업들로부터 피드백을 받을 수 있습니다. 이 부분에 대해서는 다른 글로 작성해두었는데요, node.js의 태스크 매니저인 agenda.js를 사용하는 가이드를 읽어보십시오.

설정 및 시크릿 파일 🤫

Twelve-Factor App의 battle-tested 개념에 따라 node.js에서 API Key와 데이터베이스 연결과 관련된 설정들을 저장하는 가장 좋은 방법은 dotenv를 사용하는 것입니다.

.env 파일을 만들되 절대 커밋하지 마십시오. (하지만 repository에 기본 값들로 채워져 있어야 하긴 합니다.) 이후 npm 패키지인 dotenv 는 .env 파일을 로드하여 안에 있는 값들을 node.js의 process.env 객체에 대입할 것입니다.

이것으로도 충분하지만, 몇 가지 추가적인 단계를 소개하고자 합니다. config/index.ts 파일에서 npm 패키지 dotenv가 .env 파일을 로드하고, 객체를 사용하여 변수들을 저장합니다. 이를 통해 코드 구조화를 할 수 있고 자동 완성을 사용할 수 있습니다.

config/index.js

const dotenv = require('dotenv');
// config() will read your .env file, parse the contents, assign it to process.env.
dotenv.config();

export default {
  port: process.env.PORT,
  databaseURL: process.env.DATABASE_URI,
  paypal: {
    publicKey: process.env.PAYPAL_PUBLIC_KEY,
    secretKey: process.env.PAYPAL_SECRET_KEY,
  },
  paypal: {
    publicKey: process.env.PAYPAL_PUBLIC_KEY,
    secretKey: process.env.PAYPAL_SECRET_KEY,
  },
  mailchimp: {
    apiKey: process.env.MAILCHIMP_API_KEY,
    sender: process.env.MAILCHIMP_SENDER,
  }
}

이렇게 함으로써 process.env.MY_RANDOM_VAR 명령어들이 난무해 지는 것을 막을 수 있으며, 코드 자동 완성을 이용해 env 변수명들의 이름을 다시 확인하지 않아도 됩니다.

Loaders 🏗️

저는 이 패턴을 W3Tech microframework에서 가져왔지만, 이 패키지를 사용하지는 않습니다.

아이디어는 node.js 서비스의 시작 프로세스를 테스트 가능한 모듈로 나누는 것입니다.

전형적인 express.js app 시작 부분을 보시죠.

const mongoose = require('mongoose');
const express = require('express');
const bodyParser = require('body-parser');
const session = require('express-session');
const cors = require('cors');
const errorhandler = require('errorhandler');
const app = express();

app.get('/status', (req, res) => { res.status(200).end(); });
app.head('/status', (req, res) => { res.status(200).end(); });
app.use(cors());
app.use(require('morgan')('dev'));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json(setupForStripeWebhooks));
app.use(require('method-override')());
app.use(express.static(__dirname + '/public'));
app.use(session({ secret: process.env.SECRET, cookie: { maxAge: 60000 }, resave: false, saveUninitialized: false }));
mongoose.connect(process.env.DATABASE_URL, { useNewUrlParser: true });

require('./config/passport');
require('./models/user');
require('./models/company');
app.use(require('./routes'));
app.use((req, res, next) => {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});
app.use((err, req, res) => {
  res.status(err.status || 500);
  res.json({'errors': {
    message: err.message,
    error: {}
  }});
});


... more stuff 

... maybe start up Redis

... maybe add more middlewares

async function startServer() {    
  app.listen(process.env.PORT, err => {
    if (err) {
      console.log(err);
      return;
    }
    console.log(`Your server is ready !`);
  });
}

// Run the async function to start our server
startServer();

보시는 바와 같이, 이 부분은 매우 지저분합니다.

이를 다루는 효과적인 방법은 다음과 같습니다.

const loaders = require('./loaders');
const express = require('express');

async function startServer() {

  const app = express();

  await loaders.init({ expressApp: app });

  app.listen(process.env.PORT, err => {
    if (err) {
      console.log(err);
      return;
    }
    console.log(`Your server is ready !`);
  });
}

startServer();

loaders는 간단한 목적이 있는 작은 파일들입니다.

loaders/index.js

import expressLoader from './express';
import mongooseLoader from './mongoose';

export default async ({ expressApp }) => {
  const mongoConnection = await mongooseLoader();
  console.log('MongoDB Intialized');
  await expressLoader({ app: expressApp });
  console.log('Express Intialized');

  // ... more loaders can be here

  // ... Initialize agenda
  // ... or Redis, or whatever you want
}

다음은 express loader입니다.

loaders/express.js

import * as express from 'express';
import * as bodyParser from 'body-parser';
import * as cors from 'cors';

export default async ({ app }: { app: express.Application }) => {

  app.get('/status', (req, res) => { res.status(200).end(); });
  app.head('/status', (req, res) => { res.status(200).end(); });
  app.enable('trust proxy');

  app.use(cors());
  app.use(require('morgan')('dev'));
  app.use(bodyParser.urlencoded({ extended: false }));

  // ...More middlewares

  // Return the express app
  return app;
})

다음은 mongo loader입니다.

loaders/mongoose.js

import * as mongoose from 'mongoose'
export default async (): Promise<any> => {
  const connection = await mongoose.connect(process.env.DATABASE_URL, { useNewUrlParser: true });
  return connection.connection.db;
}

Conclusion

지금까지 프로덕션 테스트를 거친 node.js 프로젝트 구조에 대해 자세히 살펴보았습니다. 다음은 요약 된 팁입니다.

  • 3 계층 구조를 사용하십시오. (3 layer architecture)

  • 비지니스 로직을 express.js의 controller에 넣지 마십시오.

  • 백그라운드 작업을 할 때는 PubSub 패턴을 사용하고 이벤트를 발생 시키십시오.

  • 마음의 평화를 위해 의존성 주입을 사용하십시오.

  • 비밀번호, secrets와 API key들을 절대 누출하지 말고 configuration manager를 사용하십시오.

  • node.js 서버 설정파일을 작은 모듈들로 분리하여 독립적으로 로드할 수 있게 하십시오.

클릭하시면 예제 repository를 보실 수 있습니다.

profile
hopsprings2ternal@gmail.com

40개의 댓글

comment-user-thumbnail
2020년 2월 2일

덕분에 글 잘 읽었습니다!
다음에 한번 시도해봐야겠네요

1개의 답글
comment-user-thumbnail
2020년 2월 5일

좋은 글 감사합니다.
간단하게 작성했던 REST API 서버가 계속 커지면서 코드가 계속 중복되고 있었는데
좋은 아키텍처를 알게됬네요 ㅎㅎ

1개의 답글
comment-user-thumbnail
2020년 2월 5일

재밌게 잘봤습니다! 덕분에 구조에 대해 좀더 생각해볼 기회가 생긴거같아요. :)

1개의 답글
comment-user-thumbnail
2020년 2월 5일

글 잘 읽고 갑니당ㅎㅎㅎ
번역하시느라 고생하셨어요!

1개의 답글
comment-user-thumbnail
2020년 2월 6일

번역 감사합니다! 덕분에 좋은 글 읽고 갑니다.

1개의 답글
comment-user-thumbnail
2020년 2월 10일

정말 훌륭한 글 번역해주셔서 감사합니다!
말씀해주신 모듈 바탕으로 저도 boilerplate 만들고 있었는데 https://github.com/Q00/api_server_boilerplate 구경해주시고 피드백 주시면 감사하겠습니다 :)

1개의 답글
comment-user-thumbnail
2020년 2월 11일

좋은 글 보고 갑니다!
감사합니다.
비즈니스 로직쪽이 감명깊네요 ㅎㅎ

1개의 답글
comment-user-thumbnail
2020년 2월 14일

잘읽었습니다!!!

1개의 답글
comment-user-thumbnail
2020년 2월 16일

좋은 글 감사합니다

1개의 답글
comment-user-thumbnail
2020년 2월 17일

좋은글 감사합니다.

1개의 답글
comment-user-thumbnail
2020년 2월 17일

좋은글 감사합니다.

답글 달기
comment-user-thumbnail
2020년 2월 18일

좋은 글 감사합니다. ㅎㅎ node 구조를 한번 배우고 싶었는데 덕분에 많이 배우고 갑니다ㅎㅎ
두고두고 계속 볼 것 같아요 ㅎ

1개의 답글
comment-user-thumbnail
2020년 2월 20일

좋은 글 잘 읽었습니다

1개의 답글
comment-user-thumbnail
2020년 2월 23일

질문이 있습니다. supertest에서 api 테스트를 하려고 하는데요. app.ts에서 app을 리턴 받아야 supertest에 app을 넣을 수 있을 것 같은데요. await require('./loaders').default({ expressApp: app }); 이 부분을 변수에 담아서 리턴 받느면 테스트를 돌릴 수 있을까요?

1개의 답글
comment-user-thumbnail
2020년 2월 23일

질문이 있습니다. supertest에서 api 테스트를 하려고 하는데요. app.ts에서 app을 리턴 받아야 supertest에 app을 넣을 수 있을 것 같은데요. await require('./loaders').default({ expressApp: app }); 이 부분을 변수에 담아서 리턴 받느면 테스트를 돌릴 수 있을까요?

답글 달기
comment-user-thumbnail
2020년 2월 23일

질문이 있습니다. supertest에서 api 테스트를 하려고 하는데요. app.ts에서 app을 리턴 받아야 supertest에 app을 넣을 수 있을 것 같은데요. await require('./loaders').default({ expressApp: app }); 이 부분을 변수에 담아서 리턴 받느면 테스트를 돌릴 수 있을까요?

답글 달기
comment-user-thumbnail
2020년 2월 23일

질문이 있습니다. supertest에서 api 테스트를 하려고 하는데요. app.ts에서 app을 리턴 받아야 supertest에 app을 넣을 수 있을 것 같은데요. await require('./loaders').default({ expressApp: app }); 이 부분을 변수에 담아서 리턴 받느면 테스트를 돌릴 수 있을까요?

답글 달기
comment-user-thumbnail
2020년 2월 23일

질문이 있습니다. supertest에서 api 테스트를 하려고 하는데요. app.ts에서 app을 리턴 받아야 supertest에 app을 넣을 수 있을 것 같은데요. await require('./loaders').default({ expressApp: app }); 이 부분을 변수에 담아서 리턴 받느면 테스트를 돌릴 수 있을까요?

답글 달기
comment-user-thumbnail
2020년 2월 23일

질문이 있습니다. supertest에서 api 테스트를 하려고 하는데요. app.ts에서 app을 리턴 받아야 supertest에 app을 넣을 수 있을 것 같은데요. await require('./loaders').default({ expressApp: app }); 이 부분을 변수에 담아서 리턴 받느면 테스트를 돌릴 수 있을까요?

답글 달기
comment-user-thumbnail
2020년 2월 23일

질문이 있습니다. supertest에서 api 테스트를 하려고 하는데요. app.ts에서 app을 리턴 받아야 supertest에 app을 넣을 수 있을 것 같은데요. await require('./loaders').default({ expressApp: app }); 이 부분을 변수에 담아서 리턴 받느면 테스트를 돌릴 수 있을까요?

답글 달기
comment-user-thumbnail
2020년 2월 23일

질문이 있습니다. supertest에서 api 테스트를 하려고 하는데요. app.ts에서 app을 리턴 받아야 supertest에 app을 넣을 수 있을 것 같은데요. await require('./loaders').default({ expressApp: app }); 이 부분을 변수에 담아서 리턴 받느면 테스트를 돌릴 수 있을까요?

답글 달기
comment-user-thumbnail
2020년 2월 23일

질문이 있습니다. supertest에서 api 테스트를 하려고 하는데요. app.ts에서 app을 리턴 받아야 supertest에 app을 넣을 수 있을 것 같은데요. await require('./loaders').default({ expressApp: app }); 이 부분을 변수에 담아서 리턴 받느면 테스트를 돌릴 수 있을까요?

답글 달기
comment-user-thumbnail
2020년 2월 23일

질문이 있습니다. supertest에서 api 테스트를 하려고 하는데요. app.ts에서 app을 리턴 받아야 supertest에 app을 넣을 수 있을 것 같은데요. await require('./loaders').default({ expressApp: app }); 이 부분을 변수에 담아서 리턴 받느면 테스트를 돌릴 수 있을까요?

답글 달기
comment-user-thumbnail
2020년 2월 23일

질문이 있습니다. supertest에서 api 테스트를 하려고 하는데요. app.ts에서 app을 리턴 받아야 supertest에 app을 넣을 수 있을 것 같은데요. await require('./loaders').default({ expressApp: app }); 이 부분을 변수에 담아서 리턴 받느면 테스트를 돌릴 수 있을까요?

답글 달기
comment-user-thumbnail
2020년 2월 25일

잘읽었습니다!!!

답글 달기
comment-user-thumbnail
2020년 9월 13일

즐겨찾기에 추가해놔야겠네요.
너무잘보고 갑니다.
고생하셨습니다.

답글 달기
comment-user-thumbnail
2020년 9월 22일

좋은 정보 감사합니다👍

답글 달기