저장소 #
저번의 첫 번째 뉴스레터 개선에 이어서 이번에 두번째 개선을 진행하였습니다. 가장 큰 변화는 Typescript 도입, Yarn PnP 도입, 커스텀 에러 정리를 진행하였습니다.
첫번째 리팩토링에서 밝혔듯이, 이 프로젝트는 npm을 사용해서 진행하고 있었습니다. 그렇지만 npm 대신에 다시 Yarn을 사용하게 되었습니다.
Yarn이 2.x 이상 버전으로 올라오면서(code명 berry) Plug'nPlay라는 기능이 추가되었습니다. # node_modules
라는 폴더를 만들어서 사용하던 기존 방식은 굉장히 비효율적인 관리 방식입니다. 의존성을 정리하는 과정에서 package.json
에 없는 패키지임에도 사용할 수 있게 되는 유령 의존성 같은 문제가 발생하기도 합니다. Yarn berry에서는 이러한 정보를 .yarn/cache
라는 폴더 안에 있는 .pnp.js
라는 파일을 통해서 기록합니다. (설치 정보를 관리하는 yarn.lock
은 여전히 사용합니다.) 또한 이렇게 설치된 패키지들은 zip
확장자로 압축되어서 .yarn/cache
에 저장됩니다. .pnp.js
에 나와있는 방식대로 순회하면 되기 때문에 속도도 빠르고, 용량도 적게 사용합니다. 또한 이 파일들을 저장소에 그대로 올려서 바로 사용이 가능한, Zero-install로 바로 스크립트를 실행시킬 수 있습니다.
제가 PnP를 적용한 이유도 Zero-install이었습니다. Github Actions에 쌓여있는 스크립트 실행시간을 본 결과, 의존성 패키지를 설치하는 데 시간이 걸리고, 학교 공지사항 목록에 접속하느데 시간이 걸렸습니다. 후자는 이 과정에서 필수적인 작업이라 줄일 수 없었지만, Zero-install을 이용하면 푸시만으로 패키지를 업데이트할 수 있습니다. PnP의 호환성 문제 같은 경우도, 의존 패키지가 워낙 적고, 스크립트 파일도 작아서 별 문제가 되지 않을 거라 예측했고 실제로도 그랬습니다.
먼저 Yarn 버전을 PnP가 지원되는 2.x 이상 버전대로 지정합니다.
yarn set version berry
이렇게 할 경우 Yarn 버전 지정에 필요한 각종 파일이 생깁니다. 여기서 우리가 건드려야 할 파일은 설정 파일인 .yarnrc.yml
입니다. 기존 프로젝트에서 berry를 사용할 경우, 패키지를 연결해주는 옵션인 nodeLinker
가 기존 방식인 node-modules
로 설정되어 있습니다. 이 부분을 지우면 PnP를 사용하게 됩니다.
# nodeLinker: node-modules
이후 node_modules
를 지우고 패키지를 설치해 줍니다.
yarn
# 또는
yarn install
패키지들이 보안 문제 때문에 업데이트를 해야 하는 상황이라 업데이트를 진행하였습니다. Yarn berry의 경우 기존 Yarn과 몇몇 명령어가 다릅니다. 업데이트 명령어도 그중 하나입니다. #
yarn up
이렇게 패키지 업데이트 한 뒤 Zero-install에 맞춰서 .gitignore
를 업데이트하였습니다.
.yarn/*
!.yarn/cache
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
이제 Zero-install이 적용되었습니다. 그 다음으로 한 일은 Github Actions Workflow를 Yarn Zero-install에 맞게 수정해 주었습니다.
- name: Yarn Install
run: yarn
- name: Run Script
run: yarn start
또한 사용하는 ide에게 현재 yarn에서 설치한 패키지 정보를 전달하기 위해, 다음 명령어를 입력합니다.
yarn dlx @yarnpkg/sdks vscode
이후 VS Code에서 작업 영역의 버전 사용을 입력하면 됩니다. 이 명령어는 패키지 설치할 때마다 해야 VS Code에서 설치된 패키지를 정확하게 인식하더라고요.
패키지 매니저 설정이 끝났으니 이 다음로 원래 목적이었던 typescript를 설치하였습니다. 그리고 마이그레이션을 조금씩 진행해나가면서 코드를 정리하기 시작햇습니다.
어쩌다 보니 최종 커밋 순서가 꼬이긴 했는데, 작성 후 컴파일 → 실행을 반복하기가 귀찮아서 ts-node를 적용하려 했습니다. 그러니깐 파일 포맷 관련해서 ts-node에서 여러 경고를 띄우더라고요. 귀찮았지만 잡는 게 좋을 것 같아서 eslint와 Prettier를 설치하고 세팅했습니다.
yarn add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-prettier eslint-plugin-prettier prettier
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
},
"env": {
"node": true
},
"extends": ["plugin:@typescript-eslint/recommended", "plugin:prettier/recommended", "prettier/@typescript-eslint"]
}
Prettier 세팅은 취향 문제라 생각해서 생략합니다.
eslint 플러그인 관련해서 PnP 관련 여러 문제를 겪었는데, 이번에는 별 문제가 없었습니다. 옛날에 yarn dlx @yarnpkg/sdks vscode
을 빼먹지 안 았는지 의심되긴 하더라고요.
@types/nodemalier
설치메일을 보내기 위해 사용하는 nodemailer
패키지는 타입스크립트용으로 타입 선언이 안 되어 있습니다. 다행히 @types/nodemailer
묘듈은 있더라고요. 이 패키지를 설치 했는데, nodemailer를 import
하는 과정에서 default import를 찾을 수 없다는 에러가 떴습니다. 이 이유는 @types/nodemailer
패키지에 default import가 없이 타입 정의만 되어 있기 때문에 생기는 문제였습니다. 2가지 해결법이 있습니다.
import * as nodemailer from 'nodemailer'
tsconfig.json
설정하기"allowSyntheticDefaultImports": true,
"esModuleInterop": true
원래 처음에는 1 처럼 했는데, 나중에 tsconfig.json
을 설정해야되어 가지고 이를 설정했습니다.
tsconfig.json
사용 및 tslib 설치 원래 만들기 귀찮았는데, Microsoft Edge Tools for VS Code에서 계속 경고도 아니고 에러를 띄우길래, 너무 거슬려서 설치했습니다. tslib
같은 경우 typescript에 사용되는 여러 가지 helper 함수들을 모아놓은 패키지입니다.
다만 애네를 설치해도, 이 확장 기능에서 Yarn PnP를 인식을 못하는지 계속 에러 띄우길래 포기하고 그냥 확장 기능을 비활성화했습니다.
ts-node
적용 및 본격적인 typescript 마이그레이션앞에 과정들은 빌드 후 실행하기 귀찮다고 ts-node
를 적용하기 위해 노력하는 과정에서 있었던 여러 삽질이었습니다. 이 과정에서 하나 알게 된 것이 있는데, 타입스크립트에서 산술 연산자의 경우 숫자가 아닌 다른 타입이 들어가면 오류를 띄웁니다. JS 상에서는 별 문제없는 문법이라 실행은 되지만, 명시적으로 숫자로 쓰인다고 기록하는 게 좋습니다. 제일 쉬운 방법은 +를 붙이는 것입니다.
return (+now - +article.date) / (1000 * 60 * 60 * 24) < 3;
더 명시적인 방법은 Date.prototype.valueOf()
를 해서 밀리 세컨드로 변환된 값을 받아 비교하는 것입니다.
return (
(now.valueOf() - article.date.valueOf()) / (1000 * 60 * 60 * 24) < 3
);
이제 기본적인 마이그레이션은 끝났고, 보다 더 타입스크립트 적인 코드를 쓰기 위해 strict
옵션을 지정하고 타입을 정리해줬습니다.
target
수정예전에는 메일이 없는 상황에 대해 그냥 아무 코드도 실행되지 않게 했는데, 그것보다는 class Error
를 상속 받아서 예외를 구현하여 처리하는 게 더 깔끔할 것이라 생각해서 다음과 같이 구현했습니다.
class NoNoticeException extends Error {
constructor() {
super('Nothing to Send');
this.name = 'Nothing to Send';
}
}
그런데 throw new NoNoticeException()
를 했을 때, 제대로 핸들링이 되지 않았습니다. 원인은 instanceof
였습니다.
if (error instanceof NoNoticeException) {
console.log('Nothing to Send');
} else {
console.error(error);
}
instanceof
가 타입스크립트 설정에서 target
이 es5
이하일 경우, 제대로 프로토타입 지정이 안되는 문제가 있어서 제대로 작동하지 않는 것이었습니다. 수동으로 프로토타입을 덧 씌워도 되지만, 좋은 방식은 아니라 생각해서 tsconfig.json
에서 target
을 es6로 바꿔줬습니다. 타겟을 es6 이상으로 할 경우, 경로 관련 오류가 생기는 데, 이는 tsconfig.json
에서 "moduleResolution": "node"
로 하면 해결됩니다.
swc는 rust로 쓰여진, 표준 타입스크립트 트랜스파일러인 tsc보다 더 빠르게 작동하는 트랜스파일러입니다. # Express.js와 같은 유명 프로젝트에서도 사용하고 있습니다. ts-node에서도 이미 관련 옵션을 제공합니다.
저도 사용해보려고 했으나, 아마 Github Actions와 제 컴퓨터 OS 차이로 인해 install을 다시 해야 했고, 무엇보다도 파일이 너무 짧아서 실행시간에 큰 차이가 없길래 그냥 적용하지 않았습니다.
가장 마지막에 한 작업은 이메일에 스타일을 넣는 것이었습니다. 덕분에 오랫만에 <style>
태그 안에서 스타일 시트를 작성했습니다.
이렇게 타입스크립트 마이그레이션을 포함해 여러 개선 작업을 했습니다. 간단한 프로젝트였지만 마이그레이션이 단순하지는 않았는데요. 앞으로 마이그레이션이 필요한 몇몇 프로젝트에서는 좀 더 쉬우면 좋겠네요.