Vite 프로젝트 빌드 및 배포 과정

#vite

디자인 패턴 자습서에서 모듈 챕터의 .mjs 확장자를 이해하려다 Vite에서 자바스크립트 코드가 어떻게 빌드되고 배포되는지까지 확인하게되었다.

Vite 프로젝트 빌드 및 배포 과정

Vite의 여러 설정이 있겠지만, 나는 이 중 타입스크립트 리액트 프로젝트를 다뤄보았다.

1. 개발 환경 설정

가장 먼저 Vite의 명령어를 이용해 개발 환경을 설정한다(react-ts 템플릿 선택). Vite 명령어를 이용해 개발 환경을 설정하면, 자동으로 필요한 다른 도구들이 함께 설치된다.

$ npm create vite@latest

2. 코드 작성

다음으로 코드를 작성한다. 코드는 하나의 index.html 파일과 여러 개의 .ts, .tsx, .css 등의 파일로 이루어진다. 로직에 따라 원하는 단위(ex. 컴포넌트)로 나누어 파일에 코드를 작성한다.

이 때, 코드 스플리팅을 원하면 정적 import 방식이 아닌, 동적 import 방식(ex. React.lazy())를 사용해서 모듈을 import 한다. 코드 스플리팅은 이후 번들링을 하며 파일을 하나로 합칠 때, 너무 하나의 큰 덩어리가 되지 않도록 작은 덩어리(chunk)로 나누는 기준을 만들어주는 작업이다.

3. 개발 서버

코드를 작성한 후 배포 전 개발 중간에 확인할 때는 개발 서버를 사용한다.

$ npm run dev

Webpack과 Vite 개발 서버 비교

Vite는 웹팩 데브 서버의 장점인 메모리 입출력을 그대로 가져가면서, **번들링 과정을 생략(Native ESM)**함으로써 속도를 더 높였다.

Vite 개발 서버의 메모리 입출력 방식

  1. 메모리 기반 서빙: Vite 개발 서버는 esbuild를 통해 트랜스파일링된 결과물을 하드디스크가 아닌 메모리에 저장한다.(파일 시스템 읽기/쓰기 과정이 생략되어 매우 빠름)
  2. Native ESM의 효율성: 웹팩은 메모리에 ‘전체 번들 파일’을 올려두고 서비스하지만, Vite는 브라우저가 import 문을 만날 때마다 요청하는 개별 모듈만 메모리에서 즉시 변환하여 전달한다.
  3. 브라우저 캐싱 활용: 메모리 입출력 외에도 Vite는 변하지 않는 라이브러리(node_modules)를 브라우저에 강력하게 캐싱하여, 네트워크 요청을 최소화한다.

개발 서버를 시작하는 명령어를 쓰면 tsc와 esbuild가 파일들을 트랜스파일링하고, Vite가 내부적으로 실행한 로컬 개발 웹서버가 브라우저로 보낸다. Vite는 개발 서버를 사용할 때 번들링을 하지않고, 브라우저가 요청한 파일만 트랜스파일링 해서 전달한다(Native ESM 방식). Webpack과 달리 번들링 과정을 아예 뛰어넘기 때문에 서버시작속도가 즉각적이다.

4. 빌드

코드를 완성한 후에는 빌드를 한다.

$ npm run build

빌드를 하면 프로젝트는 트랜스파일링->번들링->압축 및 최적화 과정을 거친다. Vite 타입스크립트 리액트 프로젝트에서는 트랜스파일링은 tsc와 esbuild, 번들링은 Rollup, 압축 및 최적화는 esbuild가 각각 맡는다.

5. 트랜스파일링

트랜스파일링이란 한 언어로 작성된 소스 코드를 비슷한 수준의 추상화를 가진 다른 언어로 변환하는 것을 의미한다. (TS → JS, JSX → JS, 최신 JS(ES6) → 구형 JS(ES5)) 프로그래밍 언어를 기계어로 바꾸는 컴파일과는 비슷하지만 다르다. 트랜스파일링을 하여 구형 브라우저에서도 신형 문법이 동작하게 코드를 변환하고, jsx 등 브라우저가 알 수 없는 문법을 js 문법으로 바꾼다.

Vite 프로젝트에서는 트랜스파일링에 tsc와 esbuild가 사용된다. tsc는 트랜스파일링 전 타입스크립트 코드의 타입을 검사하는 도구이다. tsc로 타입 검사가 완료되면 esbuild타입스크립트 코드를 자바스크립트 코드로 변환한다.

트랜스파일링시 신형 문법을 구형 문법으로 곧바로 대체할 수 없는 경우(ex. Promise 객체), 내부에서 해당 부분을 체크 해 둔다. 체크해 둔 부분은 후에 폴리필(Polyfill)을 진행한다. 폴리필이란 구형 문법에 없는 라이브러리나 로직을 새로 만들어서 추가하는 것을 말한다.

Vite 프로젝트에서도 Webpack처럼 트랜스파일링에 바벨(Babel)을 사용할 수 있지만, 기본적으로는 바벨 대신 esbuild를 사용한다. esbuild는 Go 언어로 작성되어 자바스크립트로 작성된 바벨보다 훨씬 빠르기 때문이다. 하지만 아주 오래된 브라우저를 위한 폴리필이 필요할 때는 플러그인을 이용해 바벨을 사용할 수 있다.

6. 번들링

트랜스파일링이 완료되면 Rollup이 번들링을 시작한다. 번들링이란 각각 쪼개진 .js 함수들을 하나의 코드로 합치는 작업을 말한다. 이 작업으로 브라우저가 서버에 코드 요청을 하는 횟수를 줄일 수 있다. 번들링을 하면서 코드 스플리팅을 했던 부분은 다른 덩어리(chunk)로 나누고, 트랜스파일링 때 체크했던 폴리필이 필요한 부분을 작업한다.

참고로 esbuild도 번들링은 가능하지만, Rollup이 좀 더 구형 브라우저에 대한 안정성이 높아 Vite에서는 번들링을 할 때 Rollup을 채택했다고 한다.

7. 압축 및 최적화

번들링이 완료되면 esbuild가 압축 및 최적화를 시작한다. 압축 및 최적화는 번들링한 코드에서 필요없는 부분(ex. 주석, 들여쓰기)을 모두 제거하고, 긴 변수명이나 함수명을 간단한 문자로 바꾸는 작업을 말한다. 트리쉐이킹도 진행되는데, 나무를 흔들어 죽은 잎을 떨어뜨리듯, 실제로 사용하지 않는 코드(Dead Code)를 제거하는 과정이다. (ES6의 import/export 문법을 정적으로 분석하여, 불러오기만 하고 쓰지 않는 함수나 변수를 최종 번들 파일에서 제외, CJS는 동적으로 변화하여 분석 불가) 이를 통해 덩어리로 합친 코드의 용량을 더욱 더 작게 줄일 수 있다.

8. 배포

이렇게 빌드가 완료되었다. 완료된 빌드 파일은 ./dist 폴더에 존재한다. 개발자는 이 폴더를 웹서버에 올려 배포한다.

사용자가 웹브라우저를 통해 웹서버에 요청을 보내면, 웹서버는 먼저 index.html을 전송한다. 웹브라우저가 index.html을 읽다가 main.js를 만나면, 웹서버에 파일을 요청하고, 웹서버는 번들링된 main.js 파일을 전송한다. 이 때 코드 스플리팅된 파일은 사용자가 요청하기 전까지는 보내지 않는다.