모듈 시스템
최초의 모듈 시스템
최초에는 script 테그로 자바스크립트 파일을 삽입하면 브라우저에서 순서대로 로드 하는 방식 이었습니다.
<html>
<script src="/foo.js"></script>
<script src="/bar.js"></script>
</html>
문제점
- 모듈 간 스코프 구분이 되지 않습니다. 즉, foo 에 있는 변수명과 bar에 있는 변수명이 겹치면 잘못 동작하게 됩니다.
- 모듈 순서를 신경쓰면서 개발해야 합니다.
해결책
- 자바스크립트를 모듈화 하여 서로 간섭하지 않게 만들자
이에 따라 자바스크립트를 모듈화 하기 위한 새로운 기술 등장 했습니다.
CommonJS
module.exports = foo; // 모듈 export
const foo = require("./foo"); // 모듈 import
특징
- CommonJS 는 브라우저 외에도 사용가능한 범용적인 JS Module System 입니다. 즉, 브라우저에 초짐을 맞춘 기술이 아닙니다.
- 모든 디펜던시가 로컬 디스크에 존재하여 필요한 모듈을 바로 사용할 수 있는 환경을 전제로 하여, 동기적으로 모듈을 호출합니다.
- Node.js 에서도 모듈 시스템으로 CommonJS 를 기본적으로 제공하게 되었습니다.
문제점
- 비동기 방식보다 느립니다.
- 트리쉐이킹이 어렵습니다.
- 순환 참조에 취약합니다.
AMD(Asynchronous Module Definition)
define([ // 모듈 export
'jquery',
'underscore',
], function ($, _) {
return {
};
});
require([ // 모듈 import
...
], function (...) {
});
특징
- AMD는 비동기 상황에서도 JS 모듈을 쓰기 위해 CommonJS 에서 논의하다 합의점을 이루지 못하고 독립한 그룹입니다.
- 필요한 모듈을 네트워크로 비동기적으로 내려받아서 사용해야 하는 브라우저 환경에 초점을 맞춰서 만들어졌습니다.
- define() 함수를 이용해 모듈을 구현하므로 전역변수 문제가 없습니다.
문제점
- 코드가 복잡합니다.
그러나 CommonJS, AMD는 목적이 다르고 서로 통일되지 않은 규격으로 인해 호환성 문제가 생기게 됩니다.
결국 CommonJS 와 AMD 방식을 모두 호환할 수 있게 조건문으로 분기하고 팩토리 패턴으로 구현한 UMD가 나왔습니다.
UMD(Universal Module Definition)
(function (root, factory) {
if (typeof define === "function" && define.amd) { // AMD 방식
define(["jquery", "underscore"], factory);
} else if (typeof exports === "object") { // CommonJS 방식
module.exports = factory(require("jquery"), require("underscore"));
} else {
root.foo = factory(root.$, root._);
}
})(this, function ($, _) {
var foo = {
// ...
};
return foo;
});
UMD 는 CommonJS, AMD 의 호환성을 해결해주었습니다.
그러나 여전히 자바스크립트 언어 자체에서 모듈 시스템을 지원하는 것이 아니었기 때문에 그 필요성이 높아졌습니다.
ES6 Module
2015년 쯤 ES6(ECMAScript 6) 사양에서 자바스크립트 표준 모듈 시스템이 명세되었습니다.
import foo from "bar"; // 모듈 import
export default qux; // 모듈 export
특징
- 동기/비동기 로드 모두를 지원합니다.
- 문법도 간단합니다.
- CommonJS 와 달리 실제 객체/함수를 바인딩하기 때문에 순환 참조 관리도 편합니다.
- 정적 분석 (코드 실행하지 않아도 분석 가능)이 가능하여 트리 쉐이킹이 쉬워졌습니다.
문제점
- 최근 정의된 문법이라 IE 같은 구형 브라우저에서 제대로 동작하지 않습니다.
해결책
- ES6 문법을 구형 브라우저에서 사용할 수 있게 구형 브라우저에서 동작하는 JS 코드로 변환
트렌스파일러
바벨(Babel)
- ES6 문법을 구형 브라우저에서 사용할 수 있게 구형 브라우저에서 동작하는 JS 코드로 변환 해줍니다.
- 브라우저 호환성 걱정없이 최신 자바스크립트 문법을 사용할 수 있게 해줍니다.
모듈 번들러
앞에서 기나긴 역사를 살펴봤듯이 모듈 시스템은 결국 스코프를 구분하기 위해 만들어졌습니다.
스코프를 분리하여 서로의 간섭을 없애고 모듈을 조합하여 중복 코드를 줄일 수 있게 되었습니다.
그러나 이게 끝이 아닙니다.
이렇게 만들어놓은 빌드하기 위해서는 트렌스파일러, 전처리, 최적화, 트리쉐이킹, 호환성 처리, 테스팅 등 일련의 과정을 거쳐야 합니다.
그래서 Grunt, Gulp 와 같이 일련의 과정을 수행하기 위한 자동화 도구인 테스크 러너 가 사용되어야 했습니다.
빌드 과정 중에서도 번들 과정을 전문적으로 도와주는 모듈 번들러들이 등장하게 되었습니다.
특징
- 자바스크립트 모듈을 브라우저에서 실행할 수 있는 단일 자바스크립트로 번들링 해줍니다.
- 번들러 자체에서 여러 플러그인을 제공하여 별도 테스크 러너, 최적화 도구가 필요 없게 되었습니다. 즉, 번들러가 필요한 모든 과정을 처리해주는 만능 도구가 된 것입니다.
Webpack
특징
- 오래된 만큼 안정적인 강점이 있습니다.
- Sass, TS 등 사용시 번들러가 컴파일 과정에서 필요한 플러그인 추가하고 번들러 실행해줍니다.
- Code Splitting 을 통해 원하는 때 모듈 로딩할 수 있는 Dynamic Loading & Lazy Loading 가능합니다.
- 트리 쉐이킹 등 최적화 수행해줍니다. (별도 설정 필요)
- ES5가 호환되는 모든 브라우저를 지원합니다(IE8 이하는 지원되지 않습니다).
- CSS, Asset 등을 JS로 변환합니다. 이를 위한 플러그인, 로더 등이 필요합니다.
- 다양한 Boilerplate에서 쓰고 있어 자료가 많습니다. (CRA 등)
문제점
- 비교적 복잡한 configuration
- 번들 크기가 큽니다.
- 개발 모드 속도가 느립니다.
- ES6 모듈 형식으로 빌드 결과물을 출력할 수 없습니다.
Rollup
특징
- ES6 모듈 형식으로 빌드 결과물을 출력할 수 있습니다.
- Code Splitting 에 강점이 있습니다.
- 트리 쉐이킹에 특화되어 있습니다. 특히 진입점이 여러 개 있을때 두드러집니다. 진입점이 다르기 때문에 중복해서 번들될 수 있는 공통된 부분을 알아내고 독립된 모듈로 분리할 수 있습니다.
- 라이브러리 개발에 적합한 번들러 입니다.
Parcel
특징
- JS 엔트리포인트를 지정하는게 아니라 어플리케이션 진입을 위한 HTML 파일 자체를 읽기 때문에 별도 설정 없이도 동작합니다. (Zero Config 번들러 입니다.)
- HTML 파일을 순서대로 읽어가면서 JS, CSS, Asset을 직접 참조합니다.
- 트렌스파일러 설정이 간편합니다. JS 파일만 읽을 수 있는 일반적인 번들러와 달리 CSS, Asset 을 직접 참조하기 때문에 트렌스파일이 필요한 파일 유형을 일일이 설정해줄 필요 없이 .bablerc, .postcssrc, .posthtml 같은 설정 파일들을 루트 디렉토리에 만들기만 하면 자동으로 파일을 읽어와서 세팅 해줍니다.
- 모든 모듈에서 Babel을 사용하여 최신 JS를 브라우저에 지원하는 형식으로 컴파일합니다.
- 트리 쉐이킹에 강점이 있습니다. ES6, CommonJS 모듈 모두에 대해 트리 쉐이킹 지원합니다.
문제점
- 웹팩, 롤업에 비해 좁은 생태계, 안정성이 떨어집니다.
- 일반적인 케이스만 다루고 커스텀한 설정이 필요하면 결국 설정 파일을 다시 작성해야 합니다.
차세대 번들러
대부분의 번들러는 NodeJS 기반으로 돌아가며, 번들링 중 진행되는 트랜스파일링 등 역시 대부분 NodeJS로 돌아갑니다.
문제는 NodeJS는 싱글 스레드 구조다보니 이로 인한 처리 한계가 발생하여 번들링 동작들을 Native 영역에서 돌려 성능 높이려는 시도 나타났습니다.
그래서 esbuild, SWC 등의 차세대 번들러가 등장하게 되었습니다.
esbuild
특징
- Extreme speed without needing a cache
- ES6 and CommonJS modules
- Tree shaking of ES6 modules
- An API for JavaScript and Go
- TypeScript and JSX syntax
- Source maps
- Minification
- Plugins
- GO 로 작성되어 웹팩보다 100배 빠릅니다.
- JS는 인터프리터 언어라서 한줄한줄 기계어 변환합니다. 하지만 GO는 컴파일 단계에서 미리 소스 코드를 모두 기계어로 변환합니다. JS는 싱글 스레드 기반인 반면 GO는 멀티 스레드 기반으로 동작 가능함. 즉, 코드 파싱과 출력, 소스맵 생성을 모두 병렬로 처리하빈다.
문제점
- 단지 빌드 도구일 뿐이라서 타입 체킹, HMR 등의 기능이 없음
- 코드 분할 및 CSS 관련 처리가 아직은 미비함.
SWC
특징
- Rust 언어로 작성된 빌드 툴입니다. Rust 언어는 병렬 처리를 고려하여 설계된 언어로 자바스크립트에 비해 압도적으로 빠릅니다.
- 자바스크립트 프로젝트 컴파일, 번들링에 모두 사용될 수 있고 확장가능하게 설계되어 있어서 하나의 종합 플랫폼으로 볼 수 있습니다.
- 트렌스파일링을 지원하여 기존의 Babel 역할을 대체하며 성능 비교시 압도적으로 빠릅니다.
- 타입스크립트 컴파일을 지원하지만, 타입 체킹을 수행하지는 않기 때문에 tsc를 완전히 대체하지는 않습니다.
- Esbuild 와 비슷하거나 뛰어넘는 성능을 보여줍니다.
- terser 대체를 목표로 소스 압축 및 죽은 코드 제거 기능을 지원합니다.
문제점
- SWC는 크기가 큽니다. (58MB) Vite 에서 SWC 안쓰는 이유가 VIte 19MB, SWC 58MB 라서…
- 아직 지원되지 않는 플러그인들이 많습니다. 빌드 속도를 높이기 위해 기존에 사용하던 라이브러리를 교체해야할 수도 있습니다.
카카오 FE 기술 블로그의 말을 빌리면...
빌드 시 SWC를 사용하면 바벨과 Terser를 사용했을 때 보다 빌드 속도가 향상되며, 향상되는 정도는 프로젝트의 규모와 빌드 환경에 따라 달라진다고 합니다. (Next.js v12에서 테스트한 결과, 빌드가 5배는 아니지만 2배정도 빠름!)
https://fe-developers.kakaoent.com/2022/220217-learn-babel-terser-swc/
Vite
특징
- 디펜던시(개발 시 그 내용이 바뀌지 않을 일반적인(Plain) JavaScript 소스 코드. 보통 패키지 들을 뜻합니다.) 와 소스코드(개발 시 내용이 계속 바뀌는 코드) 를 분리하여 다룹니다.
- 개발 환경에서
- 사전번들링으로 Esbuild 를 사용합니다.
- 디펜던시를 esbuild로 미리 트랜스파일링 해놓은 뒤, 로컬에서 개발 서버를 띄우면, 소스 코드를 불러오면서 의존성이 있는 패키지만 가져옵니다. 한 번 빌드한 결과는 캐싱을 해두어 다음 개발 빌드 때 바로 뜨게 됩니다.
- 번들러가 아닌 브라우저의 Native ESM을 통한 HMR 을 지원합니다. 브라우저가 해당 모듈 요청하면 교체된 모듈을 전달합니다. 한마디로 브라우저가 번들러 역할을 한다고 볼 수 있습니다.
- Native ESM 는 브라우저가 현재 화면에 보여져야해서 필요한 Module 을 요청하면 그 Module 만 전달하기 때문에 빠르고 효율적입니다.
- HMR 는 앱을 종료하지 않고 갱신된 파일만을 교체하는(Replacement) 방식을 뜻합니다. 다만 앱 사이즈가 커질수록 선형적으로 갱신에 필요한 시간이 증가합니다.
- HTTP 헤더를 이용해 퍼포먼스를 높였습니다.
- 캐시를 활용하여 한 번의 요청이라도 덜 요청되게 개선하였습니다.
- 소스코드는 304 Not Modified로, 디펜던시는 Cache-Control: max-age=31536000,immutable
- 프로덕션 빌드시 사전번들링 및 Native ESM을 사용하지 않습니다. 내부적으로 Rollup 을 사용하여 번들링 합니다.
문제점
- 디펜던시가 CommonJS, UMD 등으로 작성되어있을 수 있기 때문에 ESM 으로 불러올 수 있게 변환 작업 해야 합니다.
- 개발 환경에서는 큰 강점이 있을 지 몰라도 프로덕션 환경으로 사용하기에 안정적인지는 아직 의문입니다.
Turbopack
특징
- 번들링 및 캐싱, 병렬화를 통해 최대 속도로 최소 작업을 수행합니다.
- 개발 서버에 번들을 제공 합니다.
- Rust로 작성되어 프로덕션에만 필요한 최적화 작업을 건너뛰기 때문에 빠릅니다.
- Turbo 엔진은 함수 호출을 위한 스케줄러처럼 작동하여 함수 호출을 사용 가능한 모든 코어에서 병렬화 합니다.
- 예약한 모든 기능의 결과를 캐시하므로 동일한 작업을 두 번 수행할 필요가 없습니다. 간단히 말해서 최대 속도로 최소 작업을 수행합니다 .
- Turbopack은 Vite 와 경쟁자 입니다. Vite 에서 사용하는 기술들의 문제를 아래와 같은 이유들로 사용하지 않고, 다른 방법으로 개선을 시도하였습니다.
- esbuild 를 사용하지 않습니다.
- esbuild 의 코드는 하나의 작업에 대해 매우 최적화되어 있어 빠른 번들링이 가능합니다. 그러나 HMR이 없습니다.
- 매우 빠른 번들러이지만 많은 캐싱을 수행하지 않습니다.
- 즉, 해당 작업이 기본 속도로 진행되더라도 동일한 작업을 반복해서 수행하게 됩니다 .
- 전체 모듈을 번들로 묶어서 전달하는 것이 아니라 요청한 페이지만 번들로 묶어서 전달합니다. 이는 Native ESM과 비슷하지만, Native ESM을 사용하지 않는 이유는 빠르지만, 대규모 APP 에서 확장 문제를 일으킬 수 있기 때문입니다.
- 브라우저에서 계단식 네트워크 요청이 너무 많으면 시작 시간이 느려질 수 있습니다.
- 대규모 어플리케이션에서 Native ESM 보다 빠릅니다.
- 딱 필요한 최소한의 코드만 번들로 묶어서 줘버려서 네트워크 요청 수를 줄이려고자 하는게 Turbopack 의 목표입니다.
- esbuild 를 사용하지 않습니다.
이렇게 전체적으로 자바스크립트 모듈 및 번들러 역사를 쭉 훑어보았습니다.
어떤 문제를 해결하기 위하여 여러 접근 방식으로 해결하는 기술들이 나오고,
또 그 기술들에서 나오는 문제를 해결하기 위한 새로운 기술들이 계속 나오고 있습니다. 참 재미있네요.
지금까지 공부한 것과 개인적으로 경험한 것으로 종합해보면 이렇게 정리가 됩니다.
- 안정적인 서비스 운영에는 Webpack
- 라이브러리는 Rollup
- 앞으로 안정화가 되고, 생태계가 커진다면 대단한 혁신을 가져올 것으로 기대되는 Vite, Turbopack 는 실험적으로 사용
'프론트엔드 개발 > React.js' 카테고리의 다른 글
Jotai 와 Zustand 는 무엇이 다른가요? (0) | 2023.02.05 |
---|---|
웹 프론트엔드 유용한 링크 모음 (0) | 2023.02.04 |
Vite 아직 사용하지 마세요!!! (1) | 2023.01.28 |
CRA를 Vite로 마이그레이션 하기 (0) | 2023.01.15 |
리액트에서 외부 링크 관리하기 (0) | 2023.01.15 |