일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- Firebase
- 포랩
- JavaScript
- 플레이스토어
- LineRenderer
- 고등학생
- 유니티
- MNIST
- 개발
- 고등학교
- 1등급사과
- 수학가형
- 모의고사
- 생명과학1
- 생명과학
- PoLAB
- ios
- 바른생수
- xcode
- Unity
- Android
- 개발일지
- 내신
- kotlin
- customdialog
- 코틀린
- 딥러닝
- 수능
- 수학가형21번
- 과탐
- Today
- Total
수학적 접근
[Node.js/Typescript] Node.js + Typescript + PM2 프로젝트 구성하기 본문
이 글에서는 Typescript를 이용한 Node.js 환경 구성,
개발 환경을 위한 nodemon 연동,
그리고 프로덕션 환경을 위한 PM2까지 연동해 볼 것입니다.
* 작업하는 환경에 Node.js와 npm이 설치되어 있다고 가정하고 시작합니다.
들어가며
현재 제가 맡고 있는 프로젝트에서는, Javascript 만으로 Node.js 백엔드 API 서버 코드를 작성하고 있었습니다.
그런데 코드 양이 늘 수록 프로젝트 관리에 한계를 느끼게 되어, Typescript를 도입하기로 결정하였습니다.
Typescript로 코드를 짤 수 있으면서, 기존에 사용하던 환경을 동일하게 구성하기 위해, 여러 자료를 샅샅이 찾아가며
(+ChatGPT 까지도..) 각종 오류를 겪었습니다.
그 끝에 찾아낸 솔루션을 여기에 정리해 볼까 합니다.
Background
Node.js에서 Typescript 코드를 실행하는 방법은 아래와 같이 두 가지가 있습니다.
1. Typescript 코드 자체를 Node.js에서 실행
2. Typescript 코드를 Javascript 코드로 변환하여, Javascript 코드를 Node.js에서 실행
이 글에서는
- 개발 환경에서는 1번 방식
- 프로덕션 환경에서는 2번 방식
을 이용하는 방법을 소개할 것입니다.
* 왜 개발 환경에서는 1번 방식을 사용하는가?
만약 개발 환경에서 2번 방식을 사용한다면, `nodemon`은 변환 결과 생성된 Javascript 파일의 변경을 감지하도록 해야 합니다. 그러려면 Typescript로 코드를 작성한 다음, 코드가 잘 동작하는지 확인하려고 할 때마다 Typescript 코드를 Javascript 코드로 변환을 직접 해주어야 하는 번거로움이 발생합니다. 따라서 Typescript 코드의 변경만으로 코드의 동작을 바로 확인할 수 있도록, Typescript 코드 자체를 실행하도록 하는 것이 필요합니다. |
* 왜 프로덕션 환경에서는 2번 방식을 사용하는가?
사실 Typescript 코드 자체를 바로 실행하는 방식을 사용한다고 해서 Typescript 코드가 곧바로 실행되는 것이 아닙니다. 실제로는 내부적으로 트랜스파일링 과정을 거쳐 Javascript 코드로 변경된 다음 실행됩니다. (Node.js에서는 나중에 볼 `ts-node`라는 패키지가 그 역할을 수행합니다.) 그런데, pm2에서 매번 트랜스파일링 과정이 일어나면 성능의 저하가 발생할 것입니다. 실제로, pm2 공식문서에서도, pm2에서 트랜스파일러를 사용하는 것은 권장하지 않는다고 합니다. "We highly don’t recommend to use this in production as it slows down your app. In that case, your app must be bundled i.e. transpiled from the source to get a pre-processed version of your app." https://pm2.io/docs/runtime/integration/transpilers/ |
위 내용을 이해한 다음, 계속 진행해봅시다.
기초 작업
이 글에서 구성해 볼 프로젝트의 디렉토리 구조는 아래와 같습니다.
. #프로젝트 루트
├── build/ # 빌드(트랜스파일)된 js 파일이 저장될 디렉토리
├── code/ # Typescript 코드 디렉토리
│ └── server.ts # Node.js entry 파일
├── node_modules/
├── ecosystem.config.json # pm2 설정 파일
├── nodemon.json # nodemon 설정 파일
├── package-lock.json
├── package.json
└── tsconfig.json # Typescript 설정 파일
프로젝트 루트 디렉토리를 생성하고, 해당 디렉토리로 이동 후, 해당 프로젝트를 초기화합니다.
mkdir my-project
cd my-project
npm init -y
그리고 해당 프로젝트 내에, 작성할 코드, 빌드된 코드를 담아 둘 디렉토리 두 개(build/, code/)를 미리 생성해둡니다.
mkdir build code
Typescript 설치 및 구성
Typescript 설치
npm i typescript
npm i -D ts-node # Typescript 코드 자체를 실행할 수 있도록 해 주는 패키지
Typescipt 설정 파일인 tsconfig.json 파일 생성
npx tsc --init
* `npx` 를 사용하지 않고, `tsc`를 직접 글로벌하게 설치하여 사용해도 됩니다.
tsconfig.json 파일은 대략 아래와 같이 작성해 주면 됩니다.
{
"compilerOptions": {
/* Language and Environment */
"target": "ESNext",
"lib": [
"ESNext"
],
/* Modules */
"module": "ESNext",
"rootDir": "code",
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"@types": [
"./node_modules/@types",
]
},
"resolveJsonModule": true,
/* JavaScript Support */
"allowJs": true,
/* Emit */
"outDir": "build",
/* Interop Constraints */
"verbatimModuleSyntax": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitAny": true,
/* Completeness */
"skipLibCheck": true
},
"ts-node": {
"esm": true,
"experimentalSpecifierResolution": "node"
}
}
ESNext: Javascript 버전 중 하나이며, 코드 작성 시 Javascript 버전에 따라 기능/문법이 작동하거나 작동하지 않을 수 있고, Node.js 버전에 따라 해당 Javascript 버전이 호환이 될 수도 안 될 수도 있습니다. 프로젝트를 진행하다가 필요에 따라 변경하면 됩니다.
rootDir: Typescript 코드 파일을 저장하는 디렉토리를 `code`로 지정한 것에 의한 것으로, 필요에 따라 변경이 가능합니다.
outDir: 변환된 js 파일을 저장하는 디렉토리를 `build`로 지정한 것에 의한 것으로, 필요에 따라 변경이 가능합니다.
나머지 설정도 기존 tsconfig.json 파일에 적혀 있는 주석 등을 참고하여, 필요에 따라 변경하시면 되겠습니다.
개발 환경 구성
nodemon 설치
npm i -D nodemon
`nodemon.json` 파일 작성 (파일들의 위치는 상단의 디렉토리 구조 참고)
`nodemon.json`
{
"watch": ["."],
"ext": ".ts,.js",
"ignore": [],
"exec": "node --loader ts-node/esm ./code/server.ts"
}
"exec" 부분의 명령어는 원래 "ts-node ./code/server.ts" 가 되는 것이 일반적이지만, node 20 버전에서 이 명령어가 TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for ... 와 같은 오류를 띄우면서 동작하지 않고 있습니다. 20보다 한 단계 낮은 LTS 버전인 18을 이용하면 "ts-node ..."를 사용할 수 있지만, 20을 쓰고자 한다면 "node --loader ts-node/esm ..."을 사용해야 합니다(23/06/14 기준). "node --loader ts-node/esm ..." 명령어는 ts-node 공식 홈페이지에 대안으로 사용할 수 있는 명령어로 나와 있습니다. https://typestrong.org/ts-node/docs/usage ts-node 프로젝트에서 해당 이슈에 대해 논의 중입니다. https://github.com/TypeStrong/ts-node/issues/1997 |
`server.ts` 파일은 우선 테스트용으로 아래와 같이 간단하게 작성해줍니다.
`server.ts`
'use strict';
import express, { Request, Response } from 'express';
const app = express();
const port = 3000;
app.get('/', (req: Request, res: Response) => {
res.type('text/plain');
res.send(`Hello, Typescript!`);
});
app.listen(port, () => {
console.log(`Server is listening at http://localhost:${port}`);
});
* `server.ts` 예시 파일에 `express` 패키지가 사용되었기 때문에, `express` 패키지를 설치해줍니다.
npm i express
npm i -D @types/express
** `@types/패키지명` 을 별도로 설치해야 하는지 아닌지는 https://npmjs.com 에서 해당 패키지를 확인해 보면 알 수 있습니다.
와 같이 패키지명 옆에 DT 로 표시된 패키지들은 `@types/패키지명` 과 같이 별도 설치가 필요합니다.
와 같이 패키지명 옆에 TS 로 표시된 패키지들은 `@types/패키지명` 을 별도 설치하지 않아도 됩니다.
둘 중 어느 것도 적히지 않은 경우도 있는데, 이럴 때는 별도로 type 선언이 필요합니다. 이 경우는 여기서 다루지 않겠습니다.
잘 동작하는지 확인해보기 전에, node.js 설정 하나를 추가합니다.
`package.json` 에서 "type": "module" 부분을 추가해줍니다.
이 설정을 하지 않으면, SyntaxError: Cannot use import statement outside a module 와 같은 오류가 뜰 것입니다.
`package.json`
{
. . .
"devDependencies": {
. . .
},
"dependencies": {
. . .
},
"type": "module"
}
이제 아래 명령어를 실행해서 잘 동작하는지 확인합니다.
nodemon
콘솔창에 Server is listening at http://localhost:3000 와 같이 표시되면 성공이며,
http://localhost:3000 에 접속하여 Hello, Typescript! 가 출력되는지 확인합니다.
프로덕션 환경 구성
rimraf 및 pm2 설치
npm i -g rimraf pm2
`rimraf` 패키지는 빌드 전 원래 있던 build 파일을 지우기 위한 용도로 설치하는 패키지로, 굳이 설치하지 않고 쉘 명령어를 직접 사용해도 됩니다(rm -rf 등).
`pm2`의 configuration 파일인 `ecosystem.config.json` 파일을 작성합니다 (파일 위치는 상단 디렉토리 구조 참고)
`ecosystem.config.json`
{
"name": "my-project",
"script": "./build/server.js",
"cwd": ".",
"watch": true,
"ignore_watch": [
"node_modules",
"logs"
],
"exec_mode": "cluster",
"instances": "max",
"env": {
"NODE_ENV": "production"
},
"output": "~/logs/pm2/console.log",
"error": "~/logs/pm2/consoleError.log"
}
"script": "./build/server.js" : build 파일 내에 생성될 server.js 를 가리킵니다.
cwd: 파일 경로들의 기준점을 나타냅니다. "." 으로 입력하면 프로젝트의 루트가 됩니다.
watch: cwd 디렉토리 이하에 존재하는 파일의 변경을 감지하여, hot-reloading을 할 것인지 여부를 결정합니다.
Javascript 빌드(트랜스파일) 명령어 등록
`package.json`
{
. . .
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "rimraf ./build && tsc",
"dev": "nodemon"
},
. . .
"type": "module"
}
scripts 내부에 위와 같이 build 부분을 추가해줍니다. 필요하면 dev와 같은 것도 추가해줄 수 있습니다.
빌드(파일 변환)
npm run build
pm2 실행
pm2 start ecosystem.config.json
잘 실행되는지는 http://localhost:3000 에 접속해서 확인해 보면 됩니다.
마치며
실무 프로덕션 환경에서는 nginx 등 웹서버를 이용한 reverse proxy를 사용하며, 여기에 docker 등을 사용하기도 합니다.
nodejs+typescript+pm2 에 이어 reverse proxy+docker 까지 통합한다면, 실무 백엔드 API 서버 환경을 완전하게 구성할 수 있습니다.