Typescript + Express

2021. 7. 3. 17:27Typescript

기존에는 타입스크립트 없이 Express를 사용하다가 최근 회사의 모든 프로젝트가 타입스크립트로 넘어가는 시기였습니다.

타입스크립트의 서버 프레임워크로는 Nest.js를 채택해 사용을 했고 레거시를 옮기는 작업을 하는 도중 기존 운영 중인 레거시 서버에 기능을 추가해야하는 일이 발생했습니다.

바로 Nest.js로 서버를 만들어서 운영을 해도 되었으나 기존에 레거시에서 사용중인 데이터베이스 ORM인 Sequelize에 과도한 hooks가 몰려있어서 이 부분을 전부다 옮기지 않는 한 도입하기 쉽지 않았습니다.

해당 기능은 최대한 ASAP으로 추가를 해야만 했고 기존 Sequelize를 옮기기엔 시간이 부족하다고 생각이 되어서 이번 기능은 Express + Typescript로 추가하고 추후에 레거시 이전이 끝나면 해당 서버에 붙이기로 합의가 되었습니다.

이번 포스트는 Express와 타입스크립트를 같이 적용해서 사용하는 기초 세팅에 대한 글을 다루겠습니다.

목차는 다음과 같이 진행됩니다.

  • tsconfig.json 세팅
  • express 기초 세팅

1. tsconfig를 세팅해보자.

타입스크립트를 사용하기 위해서는 타입스크립트를 전역으로 설치할 필요가 있습니다.

안해도 되지만 하면 좋습니다. 명령어를 전역에서 사용할 수 있습니다.

npm install -g typescript

다음으로 프로젝트 폴더를 만든 후 해당 프로젝트 터미널에서 다음과 같은 명령어를 입력 해 보겠습니다.

tsc --init

해당 폴더에 tsconfig.json이라는 파일이 생성되었을 것입니다.

tsconfig.json의 역할은 타입스크립트로 작성 된 파일을 자바스크립트로 컴파일할 때 필요한 옵션들을 설정하는 파일입니다.

우리 타입스크립트는 자바스크립트를 타입이 있는 정적언어로 사용할 수 있게 해주는 언어입니다. 따라서 다시 자바스크립트로 컴파일 하는 과정을 거치는 것인데 이 과정에서 우리가 필요한 설정을 지정 할 수 있는 옵션을 지정해 줄수 있는 것이 tsconfig.json입니다.

다시 tsconfig.json파일을 보면 굉장히 많은 옵션들이 존재하고 대부분의 기능은 주석처리가 되어있는 것을 볼수 있습니다.

각 옵션들에 대한 자세한 설명은 타입스크립트 핸드북을 참고해주세요.

간단하게 이번 프로젝트에서 사용 할 옵션들만 남겨두 지우도록 하겠습니다.

// .tsconfig.json
{
  "compilerOptions": {
    "incremental": true,                  
    "target": "es2020",                  
    "module": "commonjs",                 
    "allowJs": true,                      
    "checkJs": true,                      
    "declaration": true,                  
    "sourceMap": true,                    
    "outDir": "./dist",                   
    "removeComments": true,               
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,       
    "emitDecoratorMetadata": true,        
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"]
}

이제 package.json을 만들어 설정하도록 하겠습니다.

간단하게 다음 명령어로 생성하도록 하겠습니다.

npm init -y

2. express를 세팅하자.

express 세팅을 위한 라이브러리를 받도록 하겠습니다.

npm install --save express

npm install --save -D @types/express ts-node typescript eslint @typescript-eslint/parser @typescript-eslit/eslint-plugin

우선 간단하게 eslint부터 설정하도록 하겠습니다. 다음 설정은 제가 Nest.js에서 자주 쓰는 설정입니다.

// .eslintrc.js
const path = require("path");

module.exports = {
    parser: "@typescript-eslint/parser",
    parserOptions: {
        project: path.join(__dirname, "tsconfig.json"),
        tsconfigRootDir: __dirname,
        sourceType: "module"
    },
    plugins: ["@typescript-eslint/eslint-plugin"],
    extends: ["plugin:@typescript-eslint/recommended"],
    root: true,
    env: {
        node: true,
        jest: true
    },
    ignorePatterns: [".eslintrc.js"],
    rules: {
        "@typescript-eslint/interface-name-prefix": "off",
        "@typescript-eslint/explicit-function-return-type": "off",
        "@typescript-eslint/explicit-module-boundary-types": "off",
        "@typescript-eslint/no-explicit-any": "off"
    }
};

이제 본격적인 express 세팅을 해보도록 하겠습니다.

우선 루트폴더에 src폴더를 생성하고 그 안에 index.ts파일을 생성하도록 하겠습니다.

.
├── .eslintrc.js
├── package-lock.json
├── package.json
├── src
│   └── index.ts
└── tsconfig.json

typescript에서는 라이브러리를 import하는 방법은 두가지가 존재합니다.

import * as express from 'express';
import express = require('express');

이제 간단한 express서버를 작성해보도록 하겠습니다.

// src/index.ts
import * as express from "express";

const app = express();

app.use("/", (req, res) => {
    res.send("OK");
});

app.listen(3000, () => {
    console.log("Server Start");
});

package.json에서 간단한 스크립트 2개를 추가하고 서버를 실행해보록 하겠습니다.

...
"scripts": {
   "prestart": "tsc",
   "start": "node dist/index.js"
 },
...
npm run start

이제 localhost:3000으로 접속을 해보면 OK라는 텍스트가 보인다면 설정이 끝났습니다. 이제 타입스크립트로 express를 더 강력하게 개발 할 수 있습니다.

이제 조금 더 구체적인 서버세팅을 해보도록 하겠습니다.

npm install --save cookie-parser helmet morgan 

다시 src폴더 밑에 routes폴더를 만들고 test.ts라는 파일을 만들고 다음과 같은 코드를 작성해보겠습니다.

import * as express from "express";

const router = express.Router();

router.get("/test", (req, res) => {
    res.send("ok");
});

export default router;

이제 다시 src/index.ts로 돌아와 서버세팅을 바꿔보도록 하겠습니다.

import * as express from "express";
import * as cookieParser from "cookie-parser";
import * as morgan from "morgan";
import * as helmet from "helmet";

import testRouter from "./routes/test";

const app = express();

/**
 * Set Default Header
 */
app.disable("x-powerd-by");
app.disable("etag");

/**
 * JSON Prettier - Only use development
 */
app.set("json replacer", 0);
app.set("json spaces", 4);

/**
 * Graceful shutdown time
 */
const WAIT_BEFORE_SERVER_CLOSE = parseInt(process.env.WAIT_BEFORE_SERVER_CLOSE) || process.env.production ? 10 : 3;

/**
 * 헤더 , Cookie , Body , CORS
 */
app.use((req, res, next) => {
    res.setHeader("Content-Language", "ko-KR");

    next();
});

app.use((req, res, next) => {
    res.set("Cache-Control", "no-store");
    if (req.headers.origin || req.method == "OPTIONS") {
        res.set("Access-Control-Allow-Credentials", "true");
        res.set("Access-Control-Allow-Methods", "POST,GET,PUT,PATCH,DELETE,OPTIONS");
        res.set("Access-Control-Allow-Headers", "Content-Type");

        if (req.headers.origin) {
            res.set("Access-Control-Allow-Origin", req.headers.origin);
        }

        if (req.method == "OPTIONS") {
            res.send();
            return;
        }
    }
    next();
});

app.use(express.urlencoded({ extended: true }));
app.use(express.json());

app.use(cookieParser());

app.use(morgan("dev"));
app.use(helmet());

/**
 * Router
 */
const BASE_PATH = "/v1/api";

app.use(BASE_PATH, testRouter);

const appServer = app.listen(process.env.production ? 80 : 3000, () => {
    console.log(`✓ HTTP Server listening start on port ${process.env.production ? 80 : 3000}`);
    console.log("----------------------------------------------------------------");
});

/**
 * Graceful Shutdown
 */

const handle = (signal: string) => {
    console.log("------------------------");
    console.log(`Shutdown Signal: ${signal}`);
    console.log(`waiting for ${WAIT_BEFORE_SERVER_CLOSE} sec to close server`);

    setTimeout(() => {
        console.log("Close Server");

        appServer.close(() => {
            process.exit(0);
        });
    }, WAIT_BEFORE_SERVER_CLOSE * 1000);
};

process.on("SIGINT", handle);
process.on("SIGTERM", handle);
npm run start

이제 다시 http://localhost:3000/v1/api/test로 접속했을때 여전히 'OK'가 나온다면 서버세팅이 끝났습니다.