Node.js + Typescript 시작하기

cozzin·2023년 3월 4일
0
post-thumbnail

만들고 싶은 프로젝트가 생겼는데 요즘 관심이 생긴 node와 typescript를 사용해보기로 했다. js도 잘 모르긴 하지만 강의나 책을 다 보고 시작하기에는 절대적인 시간이 부족하다는 걸 깨달았다. 만들고 싶은걸 바로 만들어보자.

https://www.digitalocean.com/community/tutorials/setting-up-a-node-project-with-typescript 이 글을 따라서 해보자.

언제나 그렇듯 오류가 있다. npm 명령어가 안된다.

 ernest.hong@Ernest  ~/project/interquest  npm init -y
dyld[15558]: Library not loaded: '/opt/homebrew/opt/icu4c/lib/libicui18n.71.dylib'
  Referenced from: '/opt/homebrew/Cellar/node/19.4.0/bin/node'
  Reason: tried: '/opt/homebrew/opt/icu4c/lib/libicui18n.71.dylib' (no such file), '/usr/local/lib/libicui18n.71.dylib' (no such file), '/usr/lib/libicui18n.71.dylib' (no such file), '/opt/homebrew/Cellar/icu4c/72.1/lib/libicui18n.71.dylib' (no such file), '/usr/local/lib/libicui18n.71.dylib' (no such file), '/usr/lib/libicui18n.71.dylib' (no such file)
[1]    15558 abort      npm init -y

나는 homebrew로 설치했었기 때문에 homebre uninstall 하고 다시 설치했다. 나의 경우 brew postinstall 까지 안해도 문제가 해결되었다. https://stackoverflow.com/a/32788187

$ brew update
$ brew uninstall node
$ brew install node
$ brew postinstall 

package.json을 만들었다. -y 옵션은 프롬프트 질문 안받고 기본값으로 package.json 파일을 만들게 해준다.

 ernest.hong@Ernest  ~/project/interquest  npm init -y
Wrote to /Users/ernest.hong/project/interquest/package.json:

{
  "name": "interquest",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

typescript를 설치하자. --save 옵션을 쓰면 package.json 파일에 package와 version을 저장하고, --save-dev 옵션은 devDependencies에만 package와 version을 저장해준다. production에 배포되지 않아도 되는 패키지를 이렇게 지정하면 편리하다고 한다. 그렇다면 여기서 --save 옵션을 써야할 것 같은데, 일단 예제에서는 --save-dev로 되어 있으니 지금은 이대로 가보자. https://stackoverflow.com/a/24739835

 ernest.hong@Ernest  ~/project/interquest  npm install typescript --save-dev

added 1 package, and audited 2 packages in 5s

found 0 vulnerabilities
npm notice
npm notice New minor version of npm available! 9.5.0 -> 9.6.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v9.6.0
npm notice Run npm install -g npm@9.6.0 to update!
npm notice

typescript 설치가 되면 이렇게 npx 명령어를 실행할 수 있게 된다. npx는 Node Package eXecute 라고 한다. project-specific dependency로 TypeScript를 설치할 수 있다고 한다.

 ernest.hong@Ernest  ~/project/interquest  npx tsc --version
Version 4.9.5

--global 옵션을 주면 프로젝트 단위가 아니라 글로벌 하게 설정이 가능하다.

 ernest.hong@Ernest  ~/project/interquest  npm install typescript --global

added 1 package in 739ms
 ernest.hong@Ernest  ~/project/interquest  tsc --version
Version 4.9.5

이제 tsconfig.json 파일이 필요하다. 이 config 파일은 어떤 파일들이, 어떤 옵션으로 컴파일 되어야 하는지 알려준다. https://www.typescriptlang.org/docs/handbook/tsconfig-json.html

 ernest.hong@Ernest  ~/project/interquest  touch tsconfig.json

기본 형태는 이렇게 생겼다고 한다. 여기서 node16을 예로 들고 있는데 LTS 버전이라서 이걸 보통 예로 드는 것 같다. tsconfig.json 파일에 아래 내용을 작성한다.

{
  "extends": "@tsconfig/node16/tsconfig.json",
  "compilerOptions": {},
  "include": ["src"],
  "exclude": ["node_modules"]
}

내 환경의 global에는 node v19.7.0 이 설치되어 있긴하다. 일단 계속 진행해보자.

 ernest.hong@Ernest  ~/project/interquest  node --version
v19.7.0
 ernest.hong@Ernest  ~/project/interquest  npm install @tsconfig/node16 --save-dev

added 1 package, and audited 3 packages in 2s

found 0 vulnerabilities
 ernest.hong@Ernest  ~/project/interquest  cat package.json
{
  "name": "interquest",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@tsconfig/node16": "^1.0.3",
    "typescript": "^4.9.5"
  }
}

@tsconfig/node16package.json에 반영되었다. 자 이제 코딩을 해보자.

src 디렉토리에 main.ts를 만들어보자. 파일의 내용은 다음과 같이 채운다.

function sayMyName(name: string): void {
  if (name === "Ernest") {
    console.log("You're right 👍");
  } else {
    console.log("You're wrong 👎");
  }
}
 
sayMyName("Ernest");
 ernest.hong@Ernest  ~/project/interquest  mkdir src
 ernest.hong@Ernest  ~/project/interquest  cd src
 ernest.hong@Ernest  ~/project/interquest/src  touch main.ts
 ernest.hong@Ernest  ~/project/interquest/src  vim main.ts
 ernest.hong@Ernest  ~/project/interquest/src  cat main.ts
function sayMyName(name: string): void {
  if (name === "Ernest") {
    console.log("You're right 👍");
  } else {
    console.log("You're wrong 👎");
  }
}

sayMyName("Ernest");

 ernest.hong@Ernest  ~/project/interquest/src  ll
total 8
-rw-r--r--  1 ernest.hong  staff   193B  3  4 12:10 main.ts
 ernest.hong@Ernest  ~/project/interquest/src  cd ..
 ernest.hong@Ernest  ~/project/interquest  ll
total 24
drwxr-xr-x  7 ernest.hong  staff   224B  3  4 12:08 node_modules
-rw-r--r--  1 ernest.hong  staff   1.0K  3  4 12:06 package-lock.json
-rw-r--r--  1 ernest.hong  staff   313B  3  4 12:06 package.json
drwxr-xr-x  3 ernest.hong  staff    96B  3  4 12:10 src
-rw-r--r--  1 ernest.hong  staff   129B  3  4 12:06 tsconfig.json

npx tsc를 터미널에 입력하면 컴파일 에러가 발생한다!

 ernest.hong@Ernest  ~/project/interquest  npx tsc
src/main.ts:3:5 - error TS2584: Cannot find name 'console'. Do you need to change your target library? Try changing the 'lib' compiler option to include 'dom'.

3     console.log("You're right 👍");
      ~~~~~~~

src/main.ts:5:5 - error TS2584: Cannot find name 'console'. Do you need to change your target library? Try changing the 'lib' compiler option to include 'dom'.

5     console.log("You're wrong 👎");
      ~~~~~~~


Found 2 errors in the same file, starting at: src/main.ts:3

Node.js v16에 기본으로 dom option이 포함되어 있지 않기 때문에 console도 사용할 수 없다고 한다. 잘 몰랐지만 브라우저에서 디버깅하는 환경에서는 dom이 설치되어 있다고 생각해볼 수 있다.

@types/node에는 node를 위한 type define이 되어 있다. 이것을 추가로 설치하면 console도 컴파일러가 인식할 수 있게된다.

Global values: AbortController, AbortSignal, dirname, filename, console, exports, gc, global, module, process, require, structuredClone

~/project/interquest  npm install @types/node --save-dev

added 1 package, and audited 4 packages in 2s

found 0 vulnerabilities
 ~/project/interquest  cat package.json
{
  "name": "interquest",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@tsconfig/node16": "^1.0.3",
    "@types/node": "^18.14.6",
    "typescript": "^4.9.5"
  }
}

이제 npx tsc를 입력해도 컴파일 에러가 없다. src/main.js 파일이 생성되었다. ts -> js로 생성해준다는 것을 알 수 있다.

"use strict";
function sayMyName(name) {
    if (name === "Ernest") {
        console.log("You're right 👍");
    }
    else {
        console.log("You're wrong 👎");
    }
}
sayMyName("Ernest");
 ~/project/interquest  node src/main.js
You're right 👍

ts 파일과 js 파일이 하나의 폴더에 생기는게 꼴보기 싫다고 생각했는데 그 다음에 js 폴더를 나눠서 지정하는 옵션이 소개되어 있다.
tsconfig.json 파일의 "compilerOptions""outDir": "dist"를 추가하자.

tsconfig.json

{
  "extends": "@tsconfig/node16/tsconfig.json",
  "compilerOptions": {
    "outDir": "dist"
  },
  "include": ["src"],
  "exclude": ["node_modules"]
}

이렇게 변경하고 npx tsc 명령어를 쳐보자.

 ~/project/interquest  npx tsc
 ~/project/interquest  tree -L 2
.
├── dist
│   └── main.js
├── node_modules
│   ├── @tsconfig
│   ├── @types
│   └── typescript
├── package-lock.json
├── package.json
├── src
│   ├── main.js
│   └── main.ts
└── tsconfig.json

6 directories, 6 files

이번에는 dist/main.js 이렇게 생성된 것을 볼 수 있다. src/main.js에 이미 생성된건 없애주지는 않나보다. 그냥 수동으로 없애줬다.

 ~/project/interquest  tree -L 2
.
├── dist
│   └── main.js
├── node_modules
│   ├── @tsconfig
│   ├── @types
│   └── typescript
├── package-lock.json
├── package.json
├── src
│   └── main.ts
└── tsconfig.json
 ~/project/interquest  node dist/main.js
You're right 👍

ts -> js 변환하고, js 파일 실행시키는게 귀찮으니까 ts 파일을 지정해서 바로 실행하는 방법이 있다. ts-node를 설치하자.

 ~/project/interquest  npm install ts-node --save-dev

added 16 packages, and audited 20 packages in 4s

found 0 vulnerabilities
 ~/project/interquest  npx ts-node src/main.ts
You're right 👍

ts -> js 변환하고, js를 실행해주는걸 이 ts-node가 다 해준다. 그리고 require 대신 import syntax를 쓸 수 있게 해준다는데(CommonJS), 이건 나중에 경험이 쌓이고 알아보자. https://github.com/TypeStrong/ts-node#commonjs-vs-native-ecmascript-modules

Node.js Third-party package 사용할 때 추가 설정이 필요할 수 있다고 한다. npm install express 통해서 express가 설치된 상황이라고 가정한다고 하니, 일단 설치해보자. -> 나중에 npm install express --save-dev로 해줬다.

그리고 다음과 같이 src/main.ts 파일을 수정하자.

import express from "express";
const app = express();
 
app.get("/", function (req, res) {
  res.send("Hello World");
});
 
app.listen(3000);
 ~/project/interquest  npm install express

added 57 packages, and audited 77 packages in 3s

7 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
 ~/project/interquest   main ±  vim src/main.ts
 ~/project/interquest   main ±  npx ts-node src/main.ts
/Users/ernest.hong/project/interquest/node_modules/ts-node/src/index.ts:859
    return new TSError(diagnosticText, diagnosticCodes, diagnostics);
           ^
TSError: ⨯ Unable to compile TypeScript:
src/main.ts:1:21 - error TS7016: Could not find a declaration file for module 'express'. '/Users/ernest.hong/project/interquest/node_modules/express/index.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/express` if it exists or add a new declaration (.d.ts) file containing `declare module 'express';`

1 import express from "express";
                      ~~~~~~~~~
src/main.ts:4:24 - error TS7006: Parameter 'req' implicitly has an 'any' type.

4 app.get("/", function (req, res) {
                         ~~~
src/main.ts:4:29 - error TS7006: Parameter 'res' implicitly has an 'any' type.

4 app.get("/", function (req, res) {
                              ~~~

    at createTSError (/Users/ernest.hong/project/interquest/node_modules/ts-node/src/index.ts:859:12)
    at reportTSError (/Users/ernest.hong/project/interquest/node_modules/ts-node/src/index.ts:863:19)
    at getOutput (/Users/ernest.hong/project/interquest/node_modules/ts-node/src/index.ts:1077:36)
    at Object.compile (/Users/ernest.hong/project/interquest/node_modules/ts-node/src/index.ts:1433:41)
    at Module.m._compile (/Users/ernest.hong/project/interquest/node_modules/ts-node/src/index.ts:1617:30)
    at Module._extensions..js (node:internal/modules/cjs/loader:1329:10)
    at Object.require.extensions.<computed> [as .ts] (/Users/ernest.hong/project/interquest/node_modules/ts-node/src/index.ts:1621:12)
    at Module.load (node:internal/modules/cjs/loader:1133:32)
    at Function.Module._load (node:internal/modules/cjs/loader:972:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:83:12) {
  diagnosticCodes: [ 7016, 7006, 7006 ]
}

컴파일 에러가 발생하는데, 원인을 살펴보자.

Could not find a declaration file for module 'express'. '/Users/ernest.hong/project/interquest/node_modules/express/index.js' implicitly has an 'any' type.

express의 타입을 정의하는 파일이 필요하다고 한다. type을 모르면 any로 반환된다. tsconfig.json가 기본적으로는 strict: true로 되어 있다. any로 타입을 추론하기 보다는 에러를 발생시키는 것이다.

타입 정의를 해주면 된다. type definition이 키워드인 것 같다. https://github.com/DefinitelyTyped/DefinitelyTyped 에서 찾아보자. express를 위한 type definition을 사용하려면 다음과 같이 설치하면 된다.

 ~/project/interquest   main ±  npm install @types/express --save-dev

added 8 packages, and audited 85 packages in 955ms

7 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

npx ts-node로 main.ts 실행해보면 에러 없이 실행된다.

 ~/project/interquest   main ±  npx ts-node src/main.ts

네트워크 연결을 허용해주고 localhost:3000 주소로 API가 작동하는지 확인해보자. 내 환경에서는 httpie를 설치해둔 상태라서 터미널에서 확인했다. 브라우저나 postman으로 확인해봐도 된다.

 ~  http localhost:3000
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 11
Content-Type: text/html; charset=utf-8
Date: Sat, 04 Mar 2023 04:00:35 GMT
ETag: W/"b-Ck1VqNd45QIvq3AZd8XYQLvEhtA"
Keep-Alive: timeout=5
X-Powered-By: Express

Hello World

잘 작동한다 🥳🥳🥳

ESLint

린트도 추가하자. 그리고 .eslintrc.js 파일을 프로젝트 루트에 추가해서 ESlint를 위한 설정을 해주면 된다. 예전에 Swift를 처음 쓰기 시작했을 때 SwiftLint를 통해서 코딩 스타일을 잡는데 도움을 받았었다. 때마침 블로그 글에서 소개하고 있으니 켜두면 좋을 것 같다.

$ npm install eslint --save-dev

몇 개를 더 설치하라고 한다. eslint는 애초에 js용으로 만들어진거라 추가 플러그인이 필요한 것 같다.

.eslintrc.js 파일도 작성해보자.

module.exports = {
  parser: "@typescript-eslint/parser",
  parserOptions: {
    ecmaVersion: "latest", // Allows the use of modern ECMAScript features
    sourceType: "module", // Allows for the use of imports
  },
  extends: ["plugin:@typescript-eslint/recommended"], // Uses the linting rules from @typescript-eslint/eslint-plugin
  env: {
    node: true, // Enable Node.js global variables
  },
};

린트 실행법

$ npx eslint . --fix

package.json에 lint commadn를 추가할 수 도 있다. 이걸 주로 쓸 것 같다.

{
  "scripts": {
    . . .
    "lint": "eslint . --fix"
  }
}
 ~/project/interquest   main ±  npm run lint


> interquest@1.0.0 lint
> eslint . --fix

간편하다! 그런데 이렇게 직접 실행하는거 말고 VS Code에서 실시간으로 알려주는 것도 있을 것 같다.

역시나 있다! 다음에 적용해보자

특정 파일은 lint 안하기: .eslintignore에 지정해두면 된다. node_modules.으로 시작하는 파일은 알아서 걸러진다.

VS Code에서 TS 사용하기

프로젝트 루트에 .vscode/launch.json 파일을 생성한다.

{
  "version": "0.1.0",
  "configurations": [
    {
      "name": "Debug main.ts",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceRoot}",
      "runtimeArgs": ["-r", "ts-node/register"],
      "args": ["${workspaceRoot}/src/main.ts"]
    }
  ]
}

이렇게 해두고 VS Code로 열어서 F5 누르면 main.js 가 실행된다. 디버그 가능하다니까 break point를 한번 걸어봤다.

$ http localhost:3000

debug console에서 res로 어떤 값이 들어왔는지 살펴볼 수 있게 되었다! 빨간점이 왜 res 뒤에 찍히는지는 모르겠다. (혹시 아는 분 있으면 알려주세요 🙏)

알게된 것들

  • Node 프로젝트 생성 했다.
  • TypeScript를 통해서 JavaScript 파일을 생성해봤다.
  • package.json 생성해봤고, package 관리를 위해서 필요하다는 것을 알게 되었다.
  • express로 hello world API 열어봤다.
  • eslint 연결해봤다. 스타일에 맞지 않았을 때 어떤 식으로 알려주는건지는 잘 모르겠다. 이상하게 해봤는데 경고해주는건 없었다.
  • VS Code에서 디버깅할 수 있게 되었다.
profile
Software Engineer

0개의 댓글