본문 바로가기

Front-End

Vite 가 Webpack 보다 빠르다고 느끼는 이유는 무엇일까

최근들어 프론트엔드 개발자들 사이에서 많이 사용되는, 스스로 '차세대 프론트엔드 개발 툴, Vite' 라는 문구를 사용하는 vite 를 사용해 보신적이 있다면, 속도도 빠르고 상당히 개발자 경험이 좋은것을 느낄수 있을거라고 생각합니다 ( 아니라면 웹팩 같은 기존 툴을 사용하시겠지만요 😅 )

이번에는 주로 비교되는 Vite 와 Webpack 의 차이점과, Vite 는 어떤점이 좋은지에 대해 주로 알아 보았습니다. 비교점을 알아보는 글 이라기 보다는 사실상 Vite 영업에 가까운것 같습니다. 

Vite Webpack 보다 빠른 이유 ?


자주 사용되는 두 도구, Webpack 과 Vite 는 어떤 차이점이 있을까요?

아래에 차이점을 먼저 비교해보았습니다.

- Webpack : 자바스크립트 기반, Bundle Based
- Vite : Esbuild (Go), Rollup (JS), Natvie ESModule

Vite 는 타 번들러와 조금 다릅니다. 특히 개발서버 구동에서 그 차이점이 많이 있습니다.
또한, Vite 는 '번들러' 라기 보단 '번들러를 포함한 개발 환경 및 빌드 도구' 에 가깝습니다.

 

Pre-Bundling 과 Native ESModule 를 이용한 개발 서버 구동방식

먼저 아래는 Vite 공식문서에서 소개하는, Vite 가 어떤 문제를 해결하기 위해 나왔는지에 대한 소개입니다.

이런 문제점이 있었어요

...(생략)

하지만 애플리케이션이 점점 더 발전함에 따라 처리해야 하는 JavaScript 모듈의 개수도 극적으로 증가하고 있습니다. 심지어 수천 개의 모듈이 존재하는 것도 대규모 프로젝트에서는 그리 드문 일이 아닙니다. 이러한 상황에서 JavaScript 기반의 도구는 성능 병목 현상이 발생되었고, 종종 개발 서버를 가동하는 데 비합리적으로 오랜 시간을 기다려야 한다거나 HMR을 사용하더라도 변경된 파일이 적용될 때 까지 수 초 이상 소요되곤 했습니다. 이와 같은 느린 피드백 루프는 개발자의 생산성과 행복에 적지 않은 영향을 줄 수 있습니다.

Vite 이러한 것에 초점을 맞춰, 브라우저에서 지원하는 ES Modules(ESM) 네이티브 언어로 작성된 JavaScript 도구 등을 활용해 문제를 해결하고자 합니다.

 

Vite 에서 모듈을 다루는 방법

vite 는 모듈을 dependenciessource doce 두 가지 카테고리로 나누어 개발서버 시작 시간을 개선합니다.

- Dependencies
개발 시 내용이 바뀌지 않을 Plain JavaScript 소스 입니다. 컴포넌트 라이브러리와 같은 일반적인 node_modules 패키지와 같은 것들을 의미합니다.
이러한 dependencies Esbuild 를 사용하여 Pre-Build 됩니다. Go 언어로 작성된 Esbuild 는 Javascript 기반 번들러 대비 10~100배 빠른 속도를 제공합니다.

- Source Code
jsx, css, 또는 Vue/Svelte 컴포넌트와 같이 컴파일링이 필요하고, 수정이 매우 잦은 Non-plain JavaScript 소스 입니다.

Vite 는 Native ESM (ESModule) 을 사용해 소스코드를 제공합니다. 이는 브라우저가 번들러 작업의 일부를 가지도록 합니다. Vite 는 브라우저가 요청하는 대로 소스코드를 변환하고 제공하기만 하면 됩니다. 조건부 동적 import 이후의 코드는 현재 화면에서 실제로 사용되는 경우에만 처리됩니다.

이는, 개발서버 에서도 자동으로 code splitting 처리를 지원한다는 이야기입니다.

Vite 에서 개발서버가 동작하는 방식

1. Vite 는 개발서버 가 최초 실행시에는 모든 Dependencies를 Esbuild 를 통해 Pre-Build 를 진행합니다. 이러한 Dependencies 는 Cache-Control: max-age=31536000,immutable을 이용해 브라우저에 캐시됩니다.

2. Source Code 를 ESM 으로 제공합니다. Dependencies 도 동일합니다. Pre-Build 이후에는 모든 모듈이 ESM 을 사용하여 브라우저가 번들러 작업의 일부를 가지도록 합니다. 현재 화면 또는 페이지에서 필요하지 않은 모듈들은 모두 코드 스플리팅되어 제공되지 않습니다.

3. Source Code 가 업데이트되면, HMR 을 통해 변경 사항을 업데이트 합니다. 차이점은 번들러가 아닌 ESM을 이용하는 것입니다. 어떤 모듈이 수정되면 vite는 그저 수정된 모듈과 관련된 부분만을 교체할 뿐이고, 브라우저에서 해당 모듈을 요청하면 교체된 모듈을 전달할 뿐입니다. 전 과정에서 완벽하게 ESM을 이용하기에, 앱 사이즈가 커져도 HMR을 포함한 갱신 시간에는 영향을 끼치지 않습니다. 필요에 따라 소스 코드는 304 Not Modified 로 제공하여 변경되지 않은 소스 코드를 추가요청 하지 않도록 처리합니다.

3번 항목의 HMR 동작 방식이 다른 번들러와의 차이점 입니다. 일부 기존 번들러는 메모리에서 작업을 수행하여 실제로 갱신에 영향을 받는 파일들만을 새롭게 번들링 하도록 했지만, 결국 처음에는 모든 파일에 대한 번들링을 수행해야 했습니다. 모든 파일을 번들링하고, 이를 다시 웹페이지 에서 불러와야 했으므로, 상당히 비효율적 이었습니다.

여기서, Vite 에서 다루는 Pre-Build 와 Esbuild 에 대해 좀더 자세히 알아보겠습니다.

 

사전 번들링 된 디펜던시 (Pre-Build)

처음 vite 를 실행할때, vite는 로컬 개발 서버에 사이트를 불러오기전에 프로젝트의 디펜던시를 Pre-Build 합니다.
이 처음 실행시 불러오는 과정을 콜드 스타트 ( 아무것도 캐시되지않은, 맨 처음 실행과정 ) 라고 부릅니다.

1. 모든 모듈을 ESM 으로 변환하여 가져오기

vite 의 개발서버는 모든 코드를 Native ESM 으로 가져오게 됩니다. 따라서, vite 는 반드시 모든 CommonJS 및 UMD 파일들을 ESM 으로 불러올수 있게 변환 작업을 진행해야 합니다.

2. 퍼포먼스

vite 는 여러 디펜던시가 존재하는 ESM 모듈을 하나의 모듈로 변환하여 페이지 로드에 대한 퍼포먼스를 향상시킵니다. 주로 언급되는 lodash 라이브러리와 같은 매우 많은 모듈을 필요로 하는 경우, 그 모듈 수 만큼 HTTP 요청을 통해 가져오게 되고, 이는 페이지 로드 퍼포먼스를 떨어트립니다.

이러한 모듈을 하나의 모듈로 번들링 하게 된다면, 브라우저는 단지 하나의 HTTP 요청만을 전송하게 됩니다.

참고로, 이 Pre-Bundling 은 개발 모드에서만 적용되고 프로덕션 빌드의 경우엔 rollup 을 통해 번들링을 진행합니다.

 

Esbuild 가 빠른 이유

그렇다면 Esbuild 가 다른 자바스크립트 기반 번들러보다 빠른 이유는 무엇일까요?

1. Go 언어를 기반으로 작성되었고, 네이티브 코드 방식으로 컴파일 합니다.

대부분의 번들러는 JIT 컴파일 방식인 자바스크립트로 작성되었습니다. 이러한 방식은 command-line 응용 프로그램(기존의 번들러와 같은)에서는 최악의 성능을 보입니다. 번들러를 실행할 때마다 자바스크립트 가상 머신이 최적화된 힌트가 하나도 없는채로 번들러의 코드를 수행하기 때문입니다.

Go 는 병렬처리를 위해 핵심부터 설계되었지만, 자바스크립트는 그렇지 않습니다. Go 는 스레드간 메모리를 공유하지만, 자바스크립트는 애초에 싱글 스레드 언어이고 멀티 스레드로 구현한다고 하더라도 스레드간의 데이터를 직렬화 해야합니다.

Go 와 자바스크립트는 병렬적인 가비지 컬렉터를 가지고 있지만, Go 의 heap 영역은 모든 스레드가 공유하고 자바스크립트의 heap 영역은 각각의 스레드마다 독립적으로 가집니다. 그래서 자바스크립트는 추측이지만 CPU 코어의 절반이 나머지 절반의 가비지 컬렉터를 위해 작동하느라 worker 스레드들의 병렬 처리량이 반으로 줄어들 것 입니다.

2. 병렬처리가 많이 사용됩니다.

esbuild 내부 알고리즘을 살펴보면 가능한 많은 CPU 코어들을 최대한으로 사용하도록 세밀하게 설계되어 있습니다. 여기서 수행하는 작업들은 크게 파싱, 연결(linking), 코드 생성 단계로 나눠집니다. 그중 파싱과 코드 생성 단계가 대부분의 작업을 차지하며 병렬 처리의 대부분 입니다.

여기서 병렬처리가 중요한 이유모든 스레드들이 메모리를 공유하기 때문에, 번들링 작업을 쉽게 공유할수 있기 때문입니다.

예를 들어, 각각의 CPU 들이 서로 다른 진입점 (Endpoint) 들을 번들링 할때, 이미 작업했던 동일한 라이브러리를 import 해야할 경우 공유된 번들링된 작업을 참조하기만 하면 됩니다. 

자바스크립트의 worker 환경은 별도의 스레드로 운영되어 멀티 스레드 방식으로 작업을 진행할수 있게 도와주기는 하지만, 메모리 역시 따로 가지기 때문에 서로 참조가 불가능하고, 이러한 동일한 라이브러리 역시 따로 import 해야하므로 작업 시간과 메모리 공간의 낭비, 가비지 컬렉팅 수행의 낭비가 일어나게 되는 것 입니다.

3. esbuild 는 모든 코드를 직접 작성하였습니다.

서드파티 라이브러리들을 사용하는 대신 모든것을 직접 작성하면 성능상 많은 이점이 있습니다. 직접 작성한다면 시작부터 성능을 염두에 두고 일관적인 데이터 구조를 사용하고 값 비싼 변환 과정들을 피할 수 있습니다. 필요할때마다 광범위한 아키텍처를 변경할 수 있습니다.

예를 들어, 많은 번들러들이 공식 TypeScript 컴파일러를 파서로 사용합니다. 하지만 TypeScript는 TypeScript 컴파일러 팀의 목표를 위해 만들어졌고 성능을 최고의 우선순위로 두지 않습니다. 그들의 코드는 메가모르픽 객체 형태와 동적 속성 접근을 상당히 많이 사용합니다.(두 방식 모두 자바스크립트의 속도를 저하시키는 것으로 잘 알려져 있습니다.)

그리고 TypeScript 파서는 타입 체커가 비활성화 되어 있어도 계속 타입 체커를 실행하는 것으로 나타납니다. esbuild 커스텀한 Typescript 파서는 이러한 문제들이 없습니다.

4. 메모리가 효율적으로 사용됩니다.

컴파일러들의 이상적인 복잡도는 입력 길이 내에서 O(n)입니다. 따라서 많은 데이터를 처리하는 경우에, 메모리 접근 속도가 성능에 큰 영향을 미칠 수 있습니다. 데이터를 넘기는 데 필요한 pass들이 적을수록(또는 데이터를 변환하는데 필요한 다른 표현들이 적을수록) 컴파일러의 속도는 더 빨라질 것입니다.

예를 들어, esbuild는 자바스크립트 AST(Abstract Syntax Tree)를 오직 3번만 사용합니다.

  1. symbol들을 선언, scope 설정, 파싱, 어휘 분석을 위한 pass
  2. 바인딩 symbol들, 구문 최소화, JSX,TS 문법들을 JS로 변환, ESNEXT 문법을 ES2015로 변환하기 위한 pass
  3. 식별자 최소화, 공백 최소화, 코드 생성 및 소스 맵 생성을 위한 pass

AST에 대한 설명 링크입니다. https://gyujincho.github.io/2018-06-19/AST-for-JS-devlopers

따라서 CPU 캐시의 사용량이 많은 동안 AST 데이터의 재사용이 극대화 됩니다. 다른 번들러들은 이러한 단계를 끼워서 수행하지 않고 별도의 pass 들로 수행합니다. 또한 이러한 번들러들은 데이터 표현들 간에 여러 라이브러리들을 붙여서 함께 변환할 수 있기 때문에 메모리를 더 많이 사용하고 작업 속도가 느려집니다.
(예를 들어, string -> TS -> JS -> string 으로 변환 후에, string -> JS -> 오래된 JS -> string 으로 변환 하고, string -> JS -> 압축된 JS -> string 로 변환)

Go의 또다른 장점은 메모리에 데이터를 밀도있게 저장할 수 있다는 것인데, 이 점은 메모리를 적게 사용하고 CPU 캐시에 더 많이 적재할 수 있게 해줍니다. 모든 객체 영역들은 타입을 가지며 영역들이 타이트하게 채워져 있습니다. 예를 들어 몇몇 boolean flag 들은 각각 1바이트만 사용합니다.

Go 또한 가치 의미론을 가지고 있으며 객체를 다른 객체에 직접적으로 내장 있으므로 다른 할당 없이비용 없이제공됩니다. 자바스크립트는 이러한 기능이 없으며 JIT 오버헤드(예를 들어, 숨겨진 클래스 슬롯) 비효율적인 표현들(예를 들어, 정수가 아닌 number들은 영역에 포인터로 할당 되는 것들) 같은 단점들이 존재합니다.

위 내용을 요약 하여 Esbuild 가 타 번들러에 비해 빠른이유는 아래와 같습니다

- 자바스크립트는 자바스크립트 가상 머신이 최적화된 힌트가 하나도 없는채로 번들러의 코드를 수행
- Go 언어는 병렬처리에 능하고, 스레드간 메모리를 공유함
- 서드파티 라이브러리 없이 직접 작성된 코드를 사용
- 메모리를 효율적으로 사용하고, 불필요한 데이터 변환작업을 최소화함.

 

결론

Vite 는 개발서버 환경에 초첨을 맞추어, 개발자 경험을 상당히 향상시켜줄수 있는 도구입니다.
개발서버 구동간에는 ESBuild 와 연계하여 ESM 방식 모듈을 제공하고, 프로덕션 빌드시에는 Rollup 을 사용하여 번들링을 진행합니다.
이는 현재 Vite 팀에서 확장성과 속도간 트레이드 오프에서 확장성이 뛰어난 번들러를 채택하기로 판단한 이유 때문이라고 하는데요, 추후에는 프로덕션 빌드도 ESBuild 로 넘어갈수 있다고 하니, 그때가 되면 Vite 는 더더욱 대중적인 도구로 자리 잡을수 있을것 같습니다.
항상 이런 문제점 인식에서 출발하여 큰 개발자 경험 향상을 가져오도록 하는 도구들은 개발자들의 많은 관심을 받을수 있는것 같습니다.

 

참고한 아티클 : 

 

Vite

Vite, 차세대 프런트엔드 개발 툴

ko.vitejs.dev

 

 

Vite

Vite, 차세대 프런트엔드 개발 툴

ko.vitejs.dev

 

 

esbuild는 왜 빠른가?(번역)

esbuild 공식문서에서 설명하는 esbuild가 빠른 4가지의 주요한 이유

gusrb3164.github.io