(Typescript) test와 lint 시간 줄여보기
안녕하세요. 똑닥의 접수/예약 서비스 개발을 담당하고 있는 백엔드팀 이준형입니다.
뭐가 문제인 거야?
비브로스 백엔드팀에서는 자신이 작성한 코드를 외부에 개방하는 "커밋", "푸시" 시점에 자신이 작성한 코드의 문법 혹은 컨벤션 오류 확인(lint), 코드의 견고함, 안정성 확인(test)을 자동으로 시행하고 있습니다. 이는 소스 저장소(이하 리모트), 자신의 개발 PC(이하 로컬)에서 회귀 테스트처럼 항상 시행하고 있습니다.
이러한 lint 및 test는 월급 루팡질을 하는 게 아닌 이상 자주 시행하기 때문에 시간이 길어지면 길어질수록 퍼포먼스가 하락하게 됩니다.
이렇게 한 번 돌렸는데 8분이나 걸리면 아주 답답합니다. 매일 컴파일 한 번 돌리고 커피 한 잔 땡기러 가시는 분들이 보기에는 당연한 이야기로 무슨 호들갑이냐 싶겠지만 인터프리터 언어를 사용하는 저희 입장에서는 이게 쌓이고 쌓이다 보면 퇴근 시간이 늦어지고 가정이 무너지고 사회가 무너지게 됩니다.
그래서 저희가 어떤 접근과 해결을 통해 퇴근 시간을 지켰는지 소개하고자 합니다.
그래서 뭘 해본 거야?
gts vs tsoa 자강두천의 맞대결, 문제는 prettier야!
gts는 google의 typescript라는 의미로 구글이 사용하는 코드 컨벤션 룰 설정과 지원 CLI 프로그램들이 담겨있는 프로젝트입니다.
tsoa는 인터페이스를 정의하고 컨트롤러 코드를 작성하는 것으로 swagger 문서도 자동으로 만들어주고 express 라우터 파일도 만들어주고 유효성 검사도 해주는 프로젝트입니다.
그런데 이렇게 개발할 때 잡생각 안 나게 해주는 고마운 툴들이 도대체 무슨 문제를 일으켰길래 우리들의 퇴근 시간을 잡아먹은 걸까요?
Before
$ DEBUG=* gts check src/routes.ts
...
eslint:file-enumerator Complete iterating files: ["src/routes.ts"] +5m
eslint:cli-engine Linting complete in: 277161ms +5m
278.37 real 271.56 user 2.84 sys
위와 같이 tsoa가 만들어낸 동적 파일을 돌렸을 경우 다른 파일에 비해 터무니없는 시간을 소요하는 것을 알 수 있었습니다.
이 문제를 저희가 어떻게 해결했을까요? 그렇습니다. 피했습니다.
문제가 되는 부분은 prettier 룰이었는데요. prettier는 띄어쓰기, 뉴라인 등 코드의 가독성을 책임지는 좋은 툴이지만 tsoa를 통해 자동으로 생성된 파일은 이 규칙을 따를 필요도 없고 따르지도 않습니다.
그렇기 때문에 검사하지 않아도 문제없을 것이므로 .prettierignore
파일에 예외 대상을 추가하였습니다. 하는 김에 배포 결과물(**/dist, **/node_modules)도 같이 예외 대상으로 추가하였습니다.
After
$ DEBUG=* gts check src/routes.ts
...
eslint:file-enumerator Complete iterating files: ["src/routes.ts"] +5s
eslint:cli-engine Linting complete in: 5152ms +5s
6.19 real 4.31 user 0.59 sys
그 결과 위와 같이 줄일 수 있었습니다.
이렇게 npm run lint
문제를 해결할 수 있었습니다. 이렇게 문제가 끝나나 싶었습니다.
테스트 코드가 발목을 잡기 시작했다
하지만 코드의 양이 점점 많아지다 보니 테스트를 돌리는 시간이 점점 길어지기 시작했습니다. 그로 인해 PR 시간이 지연되고 배포 시간이 지연되고 우리의 퇴근 시간도 지연되기 시작했습니다. 이놈들 따위에게 우리의 워라 밸을 뺏길 수 없다는 마음에 다시 튜닝 작업을 시작했습니다.
Before
$ time npm run test
...
real 2m20.154s
user 2m38.692s
sys 0m5.259s
2분이면 얼마 안 되는 거 아닌가? 싶겠습니다만 이게 쌓이고 쌓이면 역시나 퇴근 시간이 늦어지고 삶의 질이 떨어집니다.
당시 제 PC가 아닌 특정 PC에서 4분까지 올라갔었으나 재현이 안 돼서 본문에는 부득이하게 적지 않았습니다.
이를 해결하기 위해 구글링을 하던 중 ts-node의 버전을 업그레이드
하라는 글을 확인해볼 수 있었습니다. ts-node는 node.js REPL 환경에서 typescript를 돌릴 수 있게 해주는 툴입니다. REPL 환경은 그냥 로컬 환경(개발 환경)이라고 생각하시면 됩니다.
그 조언을 받아서 8.8 -> 9.0으로 버전을 업데이트 한 결과 1분까지 내려간 것을 보았습니다.
After
$ time npm run test
...
real 1m14.590s
user 1m29.170s
sys 0m5.071s
문제는 모듈 로드 캐시
이렇게 6분이 걸리던 작업을 1분으로 줄여서 기분이 매우 뿌듯했습니다. 하지만 릴리즈 노트를 봐도 도대체 왜 빨라졌는지 모르는 상황이었기 때문에 다시 롤백(...)하고 디버그 명령을 이용하여 하나하나 분석해보기 시작했습니다.
먼저 단일 파일로 비교했을 때 둘의 성능은 비슷했고 오히려 9.0 버전이 더 느린 상황이 발생하기까지 했습니다. 하지만 한 번에 테스트하려는 파일이 많으면 많아질수록 성능 차이가 나타나기 시작했습니다.
이를 통해 다수의 파일을 불러올 때 모듈을 불러오는 방법의 차이로 인해 그 차이가 점점 벌어진다는 사실을 알았습니다.
잠깐! ESM Loader?
ESM Loader는 ECMAScript Loader로 한 모듈에서 다른 모듈을 불러오는 방법의 하나입니다. 간단하게 코드로 비유하면 아래와 같습니다.
// CommonJS
const fs = require('fs');
// ESM
import * as fs from 'fs';
Typescript에서는 둘 다 지원하지만 대부분 ESM을 사용합니다. ts-node의 버전이 올라가면서 이 방식을 통해 모듈을 불러오는 성능이 향상되었기 때문에 다수의 파일을 불러오는 npm run test
의 성능이 향상되었다는 것을 알 수 있습니다.
만약 ts-node의 버전이 옛날 버전이라면 최신 버전으로 한 번 올려보시기 바랍니다. 여기서 끝났으면 좋았겠지만….
transpile-only?
Transpile의 의미는 다양하지만 여기서는 Typescript를 Javascript로 변경하는 과정을 뜻합니다. 그런데 왜 밑도 끝도 없이 이 말이 나왔을까요?
mocha에서 **/*.spec.ts
이런 식으로 다수의 파일을 불러들일 경우 일단 관련 파일들 목 록을 추린 다음 이 파일들을 하나씩 하나씩 순차적으로 불러오게 됩니다. 이로 인해 ts-node의 버전을 올려서 esm 로더의 성능을 아무리 향상시킨다 한들 파일 하나하나당 그 파일과 관련된 인터페이스의 타입 체킹을 순차적으로 진행하게 되므로 타입 체킹 했던 파일을 또 타입 체킹 하는 등 불필요한 연산이 반복되게 됩니다.
심지어 단일 파일에 대해서는 타입 체킹을 통과했는데 더 많은 파일과 타입 체킹을 하면 충돌이 나서 터지는 대참사가 발생하기도 합니다. 모든 테스트에 성공해서 기분 좋다고 컴파일 돌렸는데 터지면 그날 기분이 많이 더러워지죠.(...)
그래서 역할을 아예 나누는 게 좋지 않겠냐는 Typescript 전문가의 의견을 받아 테스트 절차를 두 개로 쪼개게 되었습니다.
// package.json
...
"scripts": {
...
"pretest": "tsc --noEmit", // --noEmit 플래그를 넣으면 output이 생기지 않습니다, 스크립트 앞에 pre를 붙이면 그 이전에 실행하게 됩니다.
"test": "mocha ./src/**/spec/*.spec.ts",
...
},
...
// .mocharc.json
{
...
"require": ["ts-node/register/transpile-only", "tsconfig-paths/register"], // transpile-only가 여기서 나옵니다
...
}
실제로 위와 같이 작업한 결과 절대적인 시간이 줄지는 않았습니다.(1분대) 하지만 테스트 코드가 많아지면 많아질수록 더 큰 효과가 나올 것으로 예상됩니다.
마참내!
이렇게 저희는 엄청나게 긴 테스트 시간을 줄이는 데 성공하였습니다. 우리는 이 시간들을 모아 우리들은 정시 퇴근을 하지는 못하고 남는 시간만큼 더 많 은 야근을 하는데 성공하였다는 해피엔딩으로 이 지루한 이야기의 막을 내리도록 하겠습니다.
그래서 결론이 뭐야?
위에서 주저리주저리 쓴 내용을 간단하게 요약해 보자면 아래와 같습니다.
- tsoa 등 generator 툴을 이용하여 생성된 파일은 prettier에서 제외하자.
- ts-node 버전이 옛날 버전이라면 최신 버전(9.0 이상, 현재 10 넘어감)으로 업데이트해 보자.
- 테스트 코드는 transpile-only 플래그를 넣고 돌리고 엄격한 타입 체크는 별도로 하자.
이상 길고 쓸데없는(...) 글을 읽어주신 것에 대해 감사의 말씀 올리면서 이만 줄여보도록 하겠습니다.