개발자 경험 개선하기 (2) - ts-loader를 esbuild-loader로 마이그레이션해보자!

보투게더·2023년 10월 3일
7

들어가며

보투게더 팀의 프론트엔드에서 최초로 선택한 loader는 ts-loader 였습니다.
해당 loader를 선택한 가장 큰 이유는, 프로젝트의 파일들이 거의 모두 TypeScript로 작성되기 때문이었습니다.

타입의 안정성을 보장하며 타입의 확장이나 재활용면에서 편리한 TypeScript를 선택했으므로, 이로 작성된 파일들을 로드하면서 타입 체킹을 해주는 ts-loader를 이용하는 것은 자연스러운 흐름이었습니다.

그런데 loader 가 뭐지?🤔

파일들을 로드해주기 위해 loader 라는 것이 필요한 걸까, 추측만 하고 넘어갔었는데.. 이번 기회에 loader란 무엇인지 정확히 짚고 넘어가보려 합니다.

loader란, Webpack과 같은 모듈 번들러를 통해 웹 애플리케이션을 빌드할 때, 특정 파일 형식을 다른 형식으로 변환하거나 처리해주는 도구입니다.

엄밀히 말하면, babel-loader의 loader와, ts-loader의 loader는 완전히 동일한 의미는 아닙니다.

  1. Babel Loader (babel-loader):
    Babel은 JavaScript 컴파일러로, 최신 버전의 JavaScript 문법을 이전 버전의 JavaScript로 변환하는 데 사용됩니다. (예를 들어, ES6+ 문법을 ES5로 변환하거나, JSX를 일반 JavaScript로 변환하는 데 주로 사용됩니다.) Babel Loader는 Webpack과 함께 사용되며, JavaScript 파일을 로드할 때 Babel을 사용하여 트랜스파일링을 수행합니다. 이로써 모든 브라우저에서 호환되는 JavaScript 코드로 변환된 파일을 생성할 수 있습니다. babel로 빌드 시 ts 또는 tsx 파일의 타입 체킹은 되지 않습니다.

  2. TypeScript Loader (ts-loader):
    TypeScript는 정적 타입 언어로, JavaScript의 상위 집합이며 타입 검사를 수행하는 컴파일러입니다. TypeScript를 사용하면 코드의 안정성과 가독성을 향상시킬 수 있습니다. ts-loader는 Webpack과 함께 사용되며, TypeScript 파일(.ts 또는 .tsx)을 JavaScript로 변환하기 위해 사용됩니다. 이로써 TypeScript 코드를 일반 JavaScript로 변환하고 웹 애플리케이션 번들에 포함시킬 수 있습니다. 특히 tsconfig.json의 compilerOptions로 트랜스파일을 진행합니다.

잠깐! 트랜스파일.. 컴파일.. 같은 건지?🤔

  1. 트랜스파일(Transpile): 주로 하나의 프로그래밍 언어에서 다른 버전 또는 하위 집합으로 코드를 변환하는 과정입니다. 이 변환은 주로 소스 코드 수준에서 이루어지며, 목적은 코드를 다른 런타임 환경에서 실행할 수 있도록 하는 것입니다. 일반적으로 트랜스파일은 더 낮은 버전의 언어로 코드를 변환하는 데 사용됩니다. (가장 대표적인 예시로 JavaScript의 ES6+ 코드를 ES5로 변환하는 Babel과 같은 도구가 있습니다. TypeScript 또한 TypeScript 코드를 JavaScript로 트랜스파일하는 예입니다.)

  2. 컴파일(Compile): 주로 고수준 언어에서 저수준 언어로 코드를 변환하는 과정입니다. 이 변환은 일반적으로 컴파일러라고 불리는 프로그램에 의해 수행되며, 소스 코드를 목적 코드로 변환합니다. 컴파일은 주로 프로그래밍 언어에서 기계어 또는 중간 언어로 변환하는 데 사용됩니다. C, C++, Java와 같은 언어는 일반적으로 소스 코드를 컴파일하여 실행 가능한 바이너리 파일을 생성합니다. 이러한 바이너리 파일은 특정 플랫폼 또는 운영 체제에서 직접 실행됩니다.


프론트엔드 팀원들 모두 각각의 loader를 사용해본 경험이 있었는데, ts-loader를 선택한 이유는 총 2가지입니다.

  1. babel-loader와 달리 ts-loader는 ts(tsx) 파일들에 대해 강력한 타입 체킹이 됩니다.
  2. babel-loader는 폴리핑이 가능하여 구 브라우저도 지원 가능하다는 이점이 있으나, 현재 IE와 같은 구 브라우저의 전세계 사용률은 1%가 채 되지 않는다는 현황을 발견하였습니다. 뿐만 아니라 구형 브라우저의 경우 보안 공격에 취약하기에, 구 브라우저로 접속 시 다른 최신 브라우저로 접속하도록 사용자에게 알려주기로 하였습니다.
  • 구 브라우저로 접속 시 아래와 같은 alert를 하고 있습니다.
import React from 'react';
import ReactGA from 'react-ga4';

import ReactDOM from 'react-dom/client';

import App from './App';

if (
  navigator.userAgent.search('Trident') !== -1 ||
  navigator.userAgent.toLowerCase().indexOf('msie') !== -1
) {
  alert('이 브라우저는 지원 중단 되었습니다. 최적의 환경을 위해 브라우저를 업데이트 하세요.');
}

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

위와 같은 이유로 ts-loader를 이용하여 개발하던 중... esbuild-loader라는 것을 접했고, esbuild-loader에 대해 알아보니 이를 사용하지 않을 이유가 없을 만큼 성능적으로 굉장하다고 느꼈습니다.

Esbuild Loader 란?
esbuild 는 Go 라는 언어로 작성된 JavaScript 번들러입니다. esbuild는 ESNext와 TypeScript 트랜스파일링과 더불어, JS minification 기능도 매우 빠르게 지원하고 있습니다.

esbuild-loader를 사용하면 빌드 시간이 3배 이상이 빨라진다기술 블로그를 읽고, 빌드 시간으로 인해 디버깅에 불편함을 겪던 필자ts-loaderesbuild-loader의 차이를 직접 비교해보기로 했습니다.😀

ts-loaderesbuild-loader 비교해보기

두 loader 간에 비교해본 항목을 하나씩 살펴봅시다.

1) 컴파일 및 로드 시간 비교

보투게더 프로젝트는 Webpack 5 버전을 사용했으며, 빌드 및 컴파일 시간을 측정한 환경은 Window10 입니다.

ts-loader

먼저 ts-loader의 경우, 컴파일 완료 시간은 최초에 60051 ms 이었습니다. (아래 이미지 참고) 약 1분(60초) 정도네요..!

그리고 두번째로 빌드했을 때는 38210 ms(38초)가 소요되었습니다.

한 번 빌드할 때마다 최소 30초 이상이라니... 디버깅할 때마다 시간이 꽤나 소요되네요.

speed-measure-plugin 이라는 플러그인을 설치하여, 빌드 프로세스에서 플러그인 및 loader가 차지하는 시간을 정확히 알아봅시다.
아래 이미지를 보면, ts-loader 는 파일 로드에 약 24초를 소요하고 있네요. (ts-loader took 23.96 secs)

esbuild-loader

이제 esbuild-loader의 컴파일 시간을 알아볼까요? 8355 ms, 8초 가량 소요되네요!
ts-loader의 컴파일 시간이 3~40초 정도였으니... 실제로 4~5배는 빨라진 것을 확인할 수 있습니다.

SMP 플러그인으로도 측정해본 결과, esbuild-loader의 파일 로드는 약 5초 정도 소요되었습니다. (ts-loader보다 5배 이상 빠르네요!)

2) 프로덕션 빌드 시간 비교 (Jenkins)

보투게더 팀은 프로젝트의 CI/CD 툴로 Jenkins를 사용하고 있습니다.

ts-loader


프로덕션(운영)의 파일들을 빌드하는데 소요된 시간을 Jenkins 사이트에서 확인해보면, 2분 20초 정도 소요되는 것을 알 수 있습니다. (frontend 폴더의 변경 사항이 없다면 약 54초가 소요됩니다.)

esbuild-loader


esbuild-loader로 마이그레이션한 이후, 총 빌드 시간은 1분 28초로, ts-loader의 경우보다 약 1분이 감소했습니다.

3) 패키지 크기 비교

이전의 패키지에서 다른 패키지로 마이그레이션할 때 중요한 요소 중 하나는, 패키지 크기라고 생각합니다. 성능은 좋지만 패키지 크기가 크다면, 번들링 결과물(예를 들어 bundle.js)의 크기도 커질 수 있기 때문입니다.

esbuild-loader의 패키지 크기는 어떨까요?

npm 공식 사이트에서 두 loader의 Unpacked size를 확인해봤습니다.

ts-loader

Unpacked Size: 255kB

webpack-bundle-analyzer 플러그인을 설치하여 빌드한 번들 결과물의 크기를 알아봅시다.
아래 이미지를 참고하면, bundle.js의 크기는 2.4 MB 입니다.

esbuild-loader

33.7kB 으로, ts-loader보다는 패키지 크기자 작습니다.)

webpack-bundle-analyzer 으로 알아본 bundle.js의 크기는 2.41 MB 이었습니다. (ts-loader와 거의 유사하네요!)

4) 타입 체킹

esbuild-loader의 유일한 한계가 있다면, 바로 타입 체킹을 하지 않는다는 것입니다.

esbuild-loader는 타입스크립트 파싱을 지원하지만, 타입 annotation 을 제외합니다. 즉, esbild는 타입 체킹을 전혀 하지 않는다는 의미이기도 하죠.

참고로 타입 annotation 이란, const a: number = 1 과 같이 : 기호를 이용하여 타입을 정의하는 표기법입니다.

그러나 fork-ts-checker-webpack-plugin 플러그인을 사용한다면, 이 한계가 사라집니다!

해당 플러그인 설치 후 실제로 빌드해보면, 터미널에 아래와 같은 문구가 뜹니다.

Type-checking in progress...
타입 에러가 없다면 No errors found 라고 뜨네요!

궁금한 점..?🤔

esbuild-loader 대신 ts-loader의 빌드 시간을 단축시키는 방법은 없을까?

라는 의문이 갑자기 들더라구요.

사실 ts-loader가 빌드 시간이 더 소요되는 이유는, 타입 체킹 때문입니다. 타입 에러는 없는지, 프로젝트를 빌드할 때마다 모든 파일을 검사합니다.

여러 자료를 탐색해본 결과, 별도의 플러그인(fork-ts-checker-webpack-plugin)을 설치하여 타입 체킹을 별도의 프로세스를 분리한다면 빌드 시간을 단축할 수 있다고 하네요!

참고 자료

마치며

ts-loader에서 esbuild-loader로 마이그레이션한 결과, 빌드 시간(정확히는 파일 로드 시간)이 상당히 단축됨을 알 수 있었습니다. 덧붙여 esbuild-loader github의 README 문서를 읽어보니, Webpack 플러그인을 별도로 설치하지 않아도 minification 등의 빌드 결과물에 대한 최적화도 지원한다고 하더군요!👍

마이그레이션 결과, Window 환경인 필자는 디버깅 시간을 대폭 줄었고 esbuild-loader의 파격적인 성능을 체감할 수 있었습니다.

esbuild-loader... 안 쓸 이유가 없는 loader 라고 생각합니다.

개발자 경험(DX) 향상에 크게 기여하는 esbuild-loader, 다들 한번씩은 경험하기를 바라며... 이번 포스팅 마치겠습니다.


📚참고 자료

profile
Fun from Choice! 오늘도 즐거운 한 표

3개의 댓글

comment-user-thumbnail
2023년 10월 4일

유익한 정보 글 정리해주셔서 복습할 수 있었어요 잘 읽었습니다 - 웃슷 -

답글 달기
comment-user-thumbnail
2023년 10월 5일

정리를 아주 깔끔하게 잘 해주셨군요 넘 유용하네요 따봉👍

답글 달기
comment-user-thumbnail
2023년 10월 10일

👍👍

답글 달기