안녕하세요. 정지현 입니다 :)
많은 JavaScript 프로젝트에서 모듈화를 위해 다양한 번들러를 사용하고 있습니다.
CRA를 사용하다 보면 굳이 웹팩 설정까지 건들 일이 없었는데, 프로젝트가 고도화될수록 번들러 설정을 구성해야 하는 상황을 마주치게 되더라구요.
프론트엔드 개발자로서 모듈과 번들러를 이해하고 빌드 최적화까지 고려할 수 있어야 더 똑똑하게 개발할 수 있겠쬬 ?
오늘은 왜 우리가 번들러를 사용해야 하는지부터 번들러의 프로세스, 번들러의 종류들을 살펴볼게요.
배경지식
자바스크립트의 모듈화
번들러가 뭐야 ?
번들러의 기능
갑자기 분위기 Babel (트랜스파일러)
번들러 더 자세하게 !
번들러 프로세스 정리
webpack, Rollup, Parcel, Vite 특징 알아보기 👉 [Bundler] JavaScript 번들러 그리고 Webpack , Parcel , Rollup , Vite... (2)
정리
개발하는 애플리케이션의 크기가 커지면 파일을 여러개로 분리하게 됩니다.
이때 분리된 파일 각각을 '모듈(module)'이라고 부릅니다.
ES5, ES6의 [ export (name,default) | import ] , commonJS의 [ module.exports | require ] 문법을 사용하여 쉽게 모듈을 만들 수 있어요.
플러그인 파일이나 잘개 쪼개진 자바스크립트 코드 조각을 재사용하기 위해서 각각의 파일을 등록하고, 등록된 파일을 자바스크립트에서 불러와서 사용할 수 있게 됩니다.
그렇다면 왜 모듈화가 필요할까요 ?
모듈 재사용으로 개발과 유지보수가 용이해집니다.
예를 들어 공통된 기능이나 UI를 사용하는 부분이 있다면 우리는 이를 공통 컴포넌트로 분리하여 어디서든 사용할 수 있습니다.
디버깅, 테스트시 용이성을 제공합니다.
특히 단위테스트에 용이합니다. 단위 테스트란 하나의 모듈을 기준으로 독립적으로 진행되는 가장 작은 단위의 테스트입니다. 해당 부분만 독립적으로 테스트하기 때문에 어떤 코드를 리팩토링하더라도 빠르게 문제 여부를 파악할 수 있습니다.
사실 자바스크립트는 원래 웹페이지 내 보조 작업을 처리하기 위한 언어였기 때문에 다른 언어와 달리 모듈 시스템이 없었습니다.
초창기 자바스크립트는 주로 웹 서버에서 실행되었고 브라우저는 서버로부터 전달받은 HTML/CSS를 단순히 렌더링만 하는 수준이었죠.
그런데 자바스크립트로 더 복잡한 작업들을 처리하고자 하는 니즈가 생기게 됨에 따라 자바스크립트는 빠르게 진화하기 시작하였고 모듈시스템도 등장을 하게 되었어요.
엄청나죠..? 자바스크립트의 변천사도 굉장히 흥미롭답니다.
다음에 자바스크립트의 동작 원리와 함께 정리해 봐야겠어요.
번들러는 의존성이 있는 모듈 코드를 하나 혹은 여러개의 파일로 만들어주는 도구이다.
React, Angular, Vue와 같은 JS 프레임워크가 등장하면서 웹 사이트를 구축하기 위한 파일들이 점점 많아졌습니다. 이렇게 많은 모듈들을 하나로 묶는 과정은 어렵습니다.
변수 또는 함수명이 중복되는 경우나 모듈 간의 종속성 때문에 배포하기 전에 많은 문제가 발생할 수 있고 많은 시간을 소모하게 됩니다.
그리고 모든 브라우저에서 모듈 시스템을 지원하는 것은 아닙니다. 크롬 브라우저 버젼 61부터는 모듈 시스템을 지원하고 있으나 , 지원하지 않는 브라우저도 있습니다. 브라우저에 무관하게 모듈을 사용 할 수는 없을까요 ?
또한 한 페이지에서 사용하는 JS 파일들이 엄청나게 많다고 했을 때, 웹 페이지가 로드될 때 JS 파일 모두를 네트워크를 통해 받아와야 합니다. 이 때 한 번에 요청하는 수가 많아져 네트워크 병목 현상이 발생할 수 있어요. 네트워크 속도가 빠르다고 해서 웹 페이지가 완벽하게 로드되는 시간까지 빠른 건 아닙니다. 웹 페이지를 구성하는 js, css , html , 이미지 파일들이 많을 수록 웹 서버에 요청하고 응답을 받는 시간이 길어지게 되는거죠.
모듈 번들러는 위의 문제들을 해결하고 애플리케이션을 최적화 해줍니다.
✅ 번들러의 기능(1) - 모듈 코드를 하나 혹은 여러개의 파일로 묶어주는 도구
모듈 번들러는 모듈의 의존 관계를 분석하여 브라우저가 인식할 수 있는 자바스크립트 코드로 변환합니다.
[효과]
모듈 단위로 개발하여 유지 보수성을 높일 수 있다.
번들러를 사용하지 않을 때는 각각의 JS 파일을 사용하기 위해 파일의 종속성을 고려하여 많은 스크립트 태그를 추가해야 했는데, 번들러는 파일끼리의 종속성을 알아서 확인하여 묶어줍니다.
한 번에 많은 요청을 하지 않아도 된다.
모듈 번들러는 JS 모듈을 브라우저에서 실행할 수 있는 단일 JS 파일로 번들링 해줍니다. 따라서 한 번의 네트워크 요청으로 우리는 웹 페이지를 로드할 수 있게 됩니다.
✅ 번들러의 기능(2) - 최적화
번들러는 성능 향상을 위해 추가 기능을 제공합니다.
Tree Shaking
필요 없는 코드를 제거하고 번들 파일의 크기, 번들링 시간을 줄여줍니다.
HMR (Hot Module Replacement)
코드가 변경되면 감지하고 브라우저에 최신 코드를 반영하여 자동으로 모듈을 교체합니다. 개발자는 새로고침을 하지 않아도 반영된 것을 빠르게 확인할 수 있으며 변경 사항만 업데이트 하기 때문에 개발 속도가 빨라집니다.
Code Splitting
JS를 청크로 분할하고, 청크가 필요한 경로에만 제공하여 성능을 향상시킵니다.
모듈 번들러가 의존성 있는 모듈끼리 번들링해서 클라이언트에게 보내 HTTP request 통신 횟수를 최소화 하여 유저의 로딩 시간을 줄였습니다. 그런데 이렇게 되면 하나의 번들 파일의 크기가 너무 커지지 않을까요? 큰 번들을 브라우저에서 파싱하고 컴파일 해야하니 로딩 시간이 더 길어지지 않을까요 ? 맞습니다!
그래서 이러한 문제를 인지하여 코드 스플리팅이라는 개념이 생겼습니다. 하나의 큰 번들을 여러 개의 번들로 쪼개고 필요한 경로에만 제공하여 최적화를 시킵니다.
Transformations
트랜스파일러를 사용하여 ES6 버전 이상의 스크립트를 사용 가능하게 해줍니다.
ES6나 JSX등의 최신 고급 문법은 구형 브라우저에서 이해하지 못하는 호환성 문제가 존재합니다. 트랜스파일러는 최신 문법을 구형 문법으로 변환해줍니다. 가장 유명한 게 Babel이죠. 그런데 갑자기 웬 트렌스파일러 얘기냐구요?
실무 환경에서는 바벨을 직접 사용하는 것보다 웹팩으로 통합해서 사용하는 경우가 많기 때문이에요. 트랜스파일러는 원본 코드를 구형 버전의 JS로 변환해주어 번들러에 전달해줍니다.
또한 변환 전후의 추상화 수준이 다른 빌드와는 다르게 트랜스파일은 추상화 수준을 그대로 유지한답니다.
ex. 타입스크립트 → 자바스크립트 , JSX → 자바스크립트 , Sass -> css
module: {
rules: [
{
test: /\.(ts|tsx|js|jsx)$/,
use: "babel-loader",
exclude: /node_modules/,
},
{
test: /\.scss$/,
use: [
"style-loader", // creates style nodes from JS strings
"css-loader", // translates CSS into CommonJS
"sass-loader" // compiles Sass to CSS, using Node Sass by default
],
exclude: /node_modules/
},
],
},
웹팩 config 일부 인데요. babel-loader 보이시죠?
웹팩은 babel-loader, css-loader 같은 로더를 사용합니다. 롤업은 플러그인을 사용하고, 파셀은 별도 설정이 없어요.
각 번들러에 대해서는 아래에서 더 살펴볼게요.
[ 번들러의 기능 요약 ]
쉽게 말해 지정한 메인 파일에서 시작하여 자바스크립트의 require과 import(ES6)문을 참고하여 프로젝트의 모든 의존성을 조사하고 로더를 이용해 처리한 후 번들로 묶은 자바스크립트 파일을 생성하는 것입니다.
모듈 번들러의 핵심 작업은 여러 JS 파일을 하나로 결합하여 단일 파일을 만드는 것입니다. 따라서 브라우저는 하나의 단일 파일을 로드하는 것 만으로도 애플리케이션이 동작합니다. JS뿐만 아니라 CSS, 이미지, 글꼴 등 여러 리소스에 대해서도 번들링 할 수 있습니다. 지금은 번들링 뿐만 아니라 빌드 과정에서의 최적화 등도 지원해주고 있어요.
번들링 되는 과정을 이해해 봅시다.
번들러 작동 원리에 대해 잘 쓰여진 글과 발표를 참고하여 정리하였습니다.
시간이 된다면 원문을 읽어 보시는 게 도움이 많이 될거에요 👀
번들러는 일반적으로 entry file에서 시작하여 해당 파일에 필요한 모든 항목들을 묶습니다.
✅ 이러한 번들러의 작업은 두 단계로 나뉩니다.
인데요. 각각 자세히 살펴볼게요.
의존성 해결의 목표는 entry point에서 시작하여(예를 들어 app.js) 모든 코드의 종속성(작동에 필요한 다른 코드 조각)을 해결하고 그래프로 구성하는 것입니다.
의존성 그래프 생성을 위해서는 파일의 이름과 식별자, 파일의 경로, 파일의 코드, 파일의 의존성 정보가 필요합니다. 그래프는 각 파일의 종속성을 재귀적으로 파악하여 구축됩니다. JS에서 이러한 데이터를 가장 쉽게 나타낼 수 있는 방법은 객체입니다.
이 정보를 바탕으로 엔트리 파일 중심의 관계성 맵을 그리게 되는데요. 각 파일에 고유한 ID를 부여하면서 종속성을 탐색합니다.
이 때 생성되는 것이 module map 입니다.
require('./utils') 처럼 상대 경로로 import 를 할 때, 모든 것이 패키징 되어 있다면 번들러는 ./utils 파일이 무엇인지 어떻게 알 수 있을까요 ?
해답은 module map 입니다.각 모듈에 대해 고유한 ID값을 가지고 있기 때문에 런타임때 올바른 모듈을 고를 수 있게 되는거죠.
마지막으로 모든 의존성을 추출하고, 모든 파일간의 관계를 나타내는 의존성 그래프를 생성합니다.
의존성 그래프를 생성하는 과정이 필요한 이유는 아래와 같습니다.
아웃풋을 만드는 단계를 Packing 이라고 부릅니다.
이 과정에서는 의존성 그래프를 활용하여 여러 코드 파일들을 통합합니다. 필요한 함수와 module.exports 객체를 주입하고, 브라우저에게 줄 수 있는 static한 결과물을 만듭니다.
즉, 번들링 과정을 통해 모듈을 하나의 실행 가능한 에셋으로 만들게 되는 겁니다.
후욱... 넘무 어렵네요. 사실 지금 정리한 거는 번들러의 동작 원리의 큰 개념 정도이구요. 파고 들면 알아야 할 개념들이 더 많답니다. 그럼에도 가장 중요한 것은, 의존성 그래프의 의의를 이해 할 수 있느냐 인 것 같습니다.
다음 챕터에 계속
오늘은 번들러의 기능과 실행 원리에 대하여 알아보았습니다.
사실 번들러를 똑똑하게 쓰려면 자신의 프로젝트를 이해하고 번들러를 사용하는 목적을 이해하며, 해당 번들러의 구성이나 플러그인등을 아는 것이 더 중요할 수 있습니다. 이와 관련해서는 각 번들러의 공식 문서들이 너무너무 잘 되어 있기 때문에 참고하여 프로젝트에 활용해본다면 더욱 성장할 수 있겠죠 ?
프론트 개발을 막 시작했을 때에는 웹팩이나 바벨에 대해 이해하는 건 멀게만 느껴지고 피하고만 싶었는데요. 그래도 개발을 하면서 알고 싶은 욕심이 생기고 실천에 옮기고 있는 것 같아서 즐겁습니다.
다음에는 CRA없이 프로젝트 세팅하기에 대하여 정리해보도록 할게요 !
번들러에 대한 이해 -
번들러에 대한 이해 (영문) -
번들러 작동 방식을 더 이해하고 싶다면 -
번들러를 만들어 보자 ( 작동 방식 이해 심화 ) -
웹팩과 바벨을 더 이해하고 싶다면 -