2024. 6. 8. 01:20ㆍ카테고리 없음
들어가며
이슈트래커는 webpack으로 구성돼있었다. CRA의 너무나도 느린 dev server 구동 속도를 개선하고자, 또 번들러를 학습하려는 목적으로 직접 webpack으로 개발 환경을 구성했었다.
webpack으로 구성된 환경은 CRA와 비교했을 때 어느정도 차이는 있었다. 약 12초 정도 걸리던 dev server 구동 시간을 9초대로 줄일 수 있었다.
하지만 요즘 CRA는 새로운 프로젝트에서 아무도 사용하지 않고, 다른 프로젝트에서 직접 webpack을 구축하는 경우는 적었다. Next가 아닌 이상 대부분 vite를 이용해 프로젝트 환경 설정을 했다.
다른 프로젝트에서 vite의 개발 서버 시작 시간은 굉장히 짧다. webpack과 비교해서 짧은 수준이 아닌 10배 정도의 차이를 보인다. 그럼 왜 그렇게 vite는 빠를 수 있을까?
ESM
뜬금 없이 웬 ESM인가? 라고 할 수도 있지만 vite의 공식문서에서도 native ESM based dev server라는 표현이 나온다.
이 글에서 가장 중요하게 다루는 ESM의 특징은 디펜던시 그래프가 정적이라는 것이다. 즉, 모듈간의 의존성이 코드가 실행되기 전에 결정된다는 것을 의미한다. 이로 인해 ESM은 보다 예측 가능하고 최적화가 용이한 구조를 갖는다. 반면 CJS는 디펜던시 그래프가 정적이지 않다.
동작원리를 살펴보자. 더 자세한 ESM의 동작원리는 이 글에 자세히 설명되어 있다. 정말 간략히만 다시 살펴보자.
의존성 그래프를 그릴 때 ESM은 다음의 세 단계로 진행된다. ESM과 CJS의 또 다른 차이는 ESM에서 각 단계는 독립적으로, 비동기로 동작한다는 것이다. CJS는 세 단계가 동기적으로 진행된다.
1. Construction
엔트리 포인트(HTML의 script 태그) 부터 import 문을 따라 디펜던시 그래프를 생성한다. 이후 이를 모듈 레코드로 파싱한다.
2. Intantiation
인스턴스화는 간단히 말하면 메모리 공간을 확보하는 과정이다. 모듈 레코드의 변수들을 모듈 환경 레코드에 할당하고 모듈 환경 레코드는 각 export와 관련된 메모리를 추적한다. 상위 depth의 모듈 환경 레코드에서 import와 관련된 메모리를 추적한다. 즉 같은 메모리를 가리킨다. 이를 live-binding이라 부른다.
(참고로 CJS는 export 객체를 복사한 사본을 내보낸다. 즉 메모리가 다르다.)
3. Evaluation
평가는 간단히 말하면 메모리에 실제로 값을 할당하는 과정이다. ESM은 live-binding이기 때문에 순환 참조 시에도 문제가 발생하지 않는다.
(참고로 CJS는 동기식이라는 점과, export 객체를 복사한다는 점, 즉 메모리가 다르다는 점 때문에 순환 참조 시 문제가 된다.)
Tree Shaking
잠깐 이 글의 주제에서 벗어나, Tree shaking에 대해 알아보자.
위에서 CJS는 디펜던시 그래프가 정적이지 않다는 말을 했다. CJS는 기본적으로 require/exports를 동적으로 하는 것에 아무런 제약이 없다. 분기문에서 require/exports를 할 수도 있고, impot path에 변수를 사용할 수도 있다. 즉 CJS는 코드를 모두 실행해야 디펜던시 그래프를 파악할 수 있다.
하지만 ESM은 디펜던시 그래프가 정적이다. import path는 항상 정적인 값이어야 하고, export는 항상 top level에서만 사용해야 한다. 코드를 실행하지 않고도, 디펜던시 그래프를 알 수 있다. 따라서 ESM은 Tree Shaking을 더욱 쉽게 할 수 있다.
다시 Vite로
그럼 이제 Vite 공식문서에 나와있는 두 이미지를 다시 살펴보자.
Bundle based dev server 즉, webpack과 같은 번들러는 dev server를 구동하기 전에 모든 모듈의 번들을 마쳐야만 한다.
하지만 vite는 일부 역할을 브라우저에게 위임했다. 일단 dev server를 구동시키고, 엔트리 포인트로부터 필요한 모듈들만 브라우저가 요청하면 트랜스파일링 한 후 보내주면 된다. 바로 이게 vite의 dev server 구동 시간이 굉장히 빠른 이유다.
(추가적으로 소스 코드가 아닌 디펜던시의 경우 Go로 작성된 esbuild를 이용하는 것도 dev server 구동 시간을 높이는 이유중 하나다. Go 언어는 컴파일 언어고 병렬 처리에 특화되어 있다. 반면 다른 번들러는 JS로 작성되어있다.)
마치며
이런 이론적 배경을 뒤로 하고, 실제로 Vite로 전환하는 과정은 꽤 수월했다. 애초에 webpack으로 개발 환경을 구성할 때 vite를 많이 참조했다. 폴더 구조나 .env 등이 거의 유사했기에 단순히 webpack을 지우고 vite를 설치한 후 몇 가지의 설정만 바꿔주면 됐기에 이는 어렵지 않았다.
그럼 성능이 얼마나 개선 됐을까?
CRA
개발 서버 구동: 12.32s
프로덕션 빌드: 20.45s
hmr: 0.49s
Vite
개발 서버 구동: 0.35s
프로덕션 빌드: 3.7s
hmr: 0.003s
hmr의 경우 100배가 넘게 차이가 났고, 개발 서버 구동 시간은 약 40배 가량의 차이가 났다.
문득 이렇게 다른 개발자를 편하게 해주는 개발자들이 참 대단하다는 생각이 든다. 다른 개발자를 편하게 해주는 그런 개발자가 되고 싶다.