일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- linux 배포판
- Headless 컴포넌트
- task queue
- 프로세스
- Event Loop
- docker
- useMemo
- Microtask Queue
- prettier-plugin-tailwindcss
- useLayoutEffect
- react
- 암묵적 타입 변환
- Compound Component
- 좋은 PR
- 주니어개발자
- 명시적 타입 변환
- Custom Hook
- CS
- JavaScript
- Sparkplug
- AJIT
- Dockerfile
- 타입 단언
- TypeScript
- useEffect
- React.memo
- useCallback
- type assertion
- prettier
- Render Queue
- Today
- Total
구리
[Webpack] 모듈 번들러, 그리고 ESBuild를 통한 빌드 속도 개선 (1) 본문
회사에서 개발중인 CRA 기반의 React 프로젝트를 Docker 이미지로 빌드시 npm run build 명령어를 통한 빌드 시간이 너무 오래 걸려 이를 개선한 과정을 작성한 글입니다.
프로젝트를 그냥 빌드하면 1분 50초정도 걸리지만 Docker 이미지로 빌드시 빌드시간이 약 6분정도로 3배가량 차이가 났습니다.
그래서 Webpack을 커스텀해 빌드 시간 자체를 줄이면 Docker 이미지 빌드 시간도 줄일 수 있지 않을까 싶어 빌드 과정을 개선해보겠습니다.
내용이 길어질 것 같아 2개의 글로 나눠 이번 포스팅은 모듈 번들러, Webpack이란 무엇이고 왜 쓰는지에 대해 알아보겠습니다.
사전 지식
본론
Create React APP (CRA)
복잡한 설정 없이 React 개발 환경을 구축해주는 boilerplate로 CRA 공식 홈페이지에서는 아래와 같은 기능을 제공한다고 설명하고 있습니다.
Less to Learn
패키지, 리액트의 지속적인 버전업에 대비해 페이스북에서 리액트의 지속적인 많은 빌드 도구를 배우고 구성할 필요가 없습니다. 배포할 때 자동으로 번들을 최적화합니다.
Only One Dependency
앱에는 하나의 빌드 종속성만 필요합니다.
우리는 Create React App을 테스트하여 모든 기본 요소가 복잡한 버전 불일치 없이 원활하게 함께 작동하는지 확인합니다.
No Lock-In
내부적으로는 webpack, Babel, ESLint 및 기타 놀라운 프로젝트를 사용하여 앱을 강화합니다. 고급 구성을 원할 경우 Create React App에서 "제거"하고 구성 파일을 직접 편집할 수 있습니다.
구체적으로는 다음과 같은 기능을 제공합니다.
- 별도 설정 없이 React 개발 환경 구축
- public 디렉토리 내 index.html, logo, favicon 등 정적 파일 및 기본 리액트 코드 설정 (app.js)
- webpack, babel 등 리액트 개발에 필요한 환경설정 세팅
- react, react-dom 등 라이브러리 설치
- react-script를 사용한 package.json에 npm 커맨드 정의
React 빌드 과정
프로젝트를 빌드하기 위해선 npm run build
명령어를 실행합니다. 그러면 package.json에 나와 있듯이 react-scripts build
명령어를 실행하게 됩니다.
react-scripts 라이브러리의 내부를 살펴보면 Webpack과 같은 모듈 번들러가 포함되어있는 것을 확인할 수 있습니다.
빌드가 완료된 후 build 폴더에는 빌드 결과물이 담기게 되며 build/static 디렉토리에는 chunk.js와 같은 JS와 CSS 파일이 포함되어 있습니다.
그렇다면 Webpack과 모듈 번들러는 무엇이고 왜 사용하는 걸까요?
모듈 시스템
일반적으로 모듈은 대개 클래스 하나 혹은 특정한 목적을 가진 복수의 함수로 구성된 라이브러리 하나로 구성됩니다.
개발하는 앱의 크기가 커지면 언젠간 파일을 여러 개로 분리하게 되는데 이때 분리된 파일 각각을 모듈이라 합니다.
모듈이 필요한 이유는 무엇일까요?
- 모듈 자신만의 스코프 보장
- 자바스크립트는 파일마다 독립적인 파일 스코프를 갖지 않고 하나의 전역 객체를 공유해 사용합니다. 이렇게 되면 전역변수가 중복되는 문제가 발생할 수 있음
- 따라서 기능별로 코드를 분리해 여러 파일로 나누면 분리된 파일끼리는 자신만의 스코프가 존재해 영향을 주고 받지 않음
- 기능 분리 및 단순한 인터페이스
- 하나의 파일에 많은 기능들을 작성하면 어떤 의미인지 파악하고 관리하기가 어려워지기에 각 기능별로 파일을 분리해 관리하는 것이 필요
- 모듈 재사용을 통한 개발 효율성 증가 및 유지보수 용이
- 예를 들어 공통 기능 혹은 UI를 개발할 경우, 공통 컴포넌트로 분리해 어디서든 사용할 수 있음
자바스크립트 초기에는 스크립트 크기가 작고 기능도 단순했기에 모듈 관련 표준 문법이 없어도 됐었지만 스크립트 크기가 커지고 기능도 복잡해지면서 특별한 라이브러리를 만들어 필요한 모듈을 언제든지 불러올 수 있게 해주거나 코드를 모듈 단위로 구성해주는 방법을 만들기 위한 시도를 하며 다음과 같은 모듈 시스템이 만들어졌습니다.
CommonJS
브라우저, 서버 사이드 앱 등 범용적인 용도로 사용되는 JS 모듈 시스템을 만들기 위해 조직된 그룹으로 Node.js에서 이를 사용하며 exports
키워드로 모듈을 만들고 require()
함수로 임포트하는 방식입니다.
module.exports = foo;
const foo = require("./foo");
CommonJS는 애초에 브라우저 외 환경에서도 동작하는 범용적인 JS를 위한 모듈 시스템이기에, 모든 디펜던시가 로컬 디스크에 존재해서 필요한 모듈을 바로 사용할 수 있는 환경을 전제로 합니다. 따라서 동기적으로 모듈을 호출하는 방식을 선택했습니다.
require와 exports 키워드를 활용한 직관적이고 간단한 문법이 장점이지만 비동기 방식보다 느리고, 트리 쉐이킹(임포트되었지만 실제로 사용되지 않는 코드를 분석하고 삭제하는 코드 최적화 기술)이 어려운데다, 순환 참조에 취약한 단점이 명확했습니다.
AMD
Asynchronous Module Definition의 약자로 비동기 상황에서도 JS 모듈을 사용하기 위해 만들어졌습니다.
문법이 다소 복잡하지만 비동기적으로 모듈을 호출하는 특성 때문에 퍼포먼스적으로 CommonJS보다 나은 성능을 보였습니다.
define(['./foo.js', './boo.js'], function(foo, boo){
//
})
ES6 Module
하지만 근본적으로 모듈 시스템의 부재라는 문제를 해결하지 못했기에 2015년, ES6에 표준 모듈 시스템이 명세되었습니다.
이를 ES6 Module(ES Module)이라고 부릅니다.
import foo from "bar";
export default qux;
동기/비동기 로드를 모두 지원하고 문법도 간단합니다. 하지만 비교적 최근에 정의된 문법으로, IE 같은 구형 브라우저에서는 제대로 동작하지 않는 문제가 있었습니다.
모듈 번들러
번들러는 의존성이 있는 모듈 코드를 하나(또는 여러 개)의 파일로 만들어주는 도구입니다.
React, Angular, Vue와 같은 JS 프레임워크가 등장하면서 웹사이트 구축을 위한 파일들이 많아지며 많은 모듈들을 하나로 묶는 과정은 어렵습니다. 변수 또는 함수명이 중복되거나 모듈 간의 종속성 때문에 배포전 많은 문제가 발생할 수 있기 때문입니다.
또한 위에도 나와있듯이 모든 브라우저에서 모듈 시스템을 지원하는 것은 아니었으며 한페이지에서 사용하는 JS 파일이 많을 경우 모두 네트워크를 통해 받아야하는데 한번에 요청하는 수가 많아서 네트워크 병목 현상이 발생할 수 있습니다.
모듈 번들러는 위 문제들을 해결하고 애플리케이션을 최적화해주기 위해 탄생했으며 다음과 같은 기능을 제공합니다.
번들러 제공 기능과 효과
(1) 모듈을 하나 혹은 여러 개의 파일로 묶어주는 도구
- 모듈 단위 개발을 통한 유지 보수성 증가
- 번들러를 사용하지 않는 경우, 각각의 파일 종속성을 고려해 많은 스크립트 태그를 추가하는 상황이 발생했지만 번들러를 통해 파일끼리의 종속성을 알아서 확인해 번들링함
- 한 번에 많은 리소스 요청 방지
- JS 모듈을 브라우저에서 실행할 수 있는 단일 JS 파일로 번들링해주기에 한 번의 네트워크 요청으로 웹페이지 로드 가능
(2) 성능 향상을 위한 최적화
- Tree Shaking
- 불필요한 코드를 제거하고 번들 파일 크기, 번들링 시간 축소
- HMR (Hot Module Replacement)
- 코드 변경을 감지해 최신 코드로 자동 모듈 교체를 진행하며 변경 사항만 업데이트하기에 개발 속도가 빨라짐
- Code Splitting
- JS를 청크로 분할하고, 청크가 필요한 경로에만 제공해 성능 향상
- 모듈 번들러는 의존성 있는 모듈을 하나의 파일로 번들링하지만 그러면 번들 파일 크기가 커져 로딩 시간이 길어지기에 하나의 큰 번들 파일을 여러 개의 번들로 쪼개 필요한 경로에만 제공해 최적화 진행
- Transformations
- 트랜스파일러를 사용해 ES6 이상의 스크립트를 사용 가능하게 해줌
다음은 번들러의 동작 원리에 대해 가볍게 알아보겠습니다.
번들러 동작원리
번들러는 entry file을 기반으로 프로젝트 내의 모든 모듈을 탐색하고, 각 모듈간의 의존성을 파악합니다. 일반적으로 import 및 require문을 분석해 수집합니다. 이때 번들러는 의존성 그래프를 생성합니다.
의존성 그래프란 모듈 간의 의존성을 시각적으로 나타내는 구조로 entry file에서 찾은 의존성을 기반으로 모듈 간의 의존성을 탐색하는 과정을 재귀적으로 진행하며 의존성 그래프를 구성합니다. 이 그래프는 노드(Node)와 엣지(Edge)로 구성되며 노드는 각 모듈을 나타내고 엣지는 의존성 관계를 나타냅니다.
예를 들어 모듈 A가 모듈 B를 import 하는 경우, A는 B에 대한 의존성을 가지며, 이는 의존성 그래프에서 A에서 B로 향하는 엣지로 표현됩니다.
의존성 그래프를 만든 후 모듈 번들러는 이 그래프를 기반으로 모듈을 번들링하고 최종 번들 파일을 생성합니다. 이렇게 의존성 그래프를 효과적으로 관리하면 필요한 모듈만 포함된 번들을 생성할 수 있으며 트리 쉐이킹(Tree Shaking)과 같은 최적화 기능 구현에 활용할 수 있습니다. 더 자세한 내용은 해당 링크에 있는 PPT를 참고하시면 좋을 것 같습니다.
번들러의 동작 원리를 정리하면 다음과 같습니다.
- 진입점(Entry Point) 식별
- 진입점 모듈 분석
- 찾은 의존성을 기반으로 모듈 간의 의존성을 재귀적으로 탐색
- 의존성 그래프 구성
- 번들링과 최적화
트랜스 파일러
ES6에서 자바스크립트 표준 모듈 문법이 정의되었지만 구형 브라우저에서 사용하지 못하는 문제를 해결하기 위해 트랜스파일러(Transpiler)가 탄생했습니다.
즉, 한 번 컴파일하면 구형 브라우저에서도 동작하는 자바스크립트 코드가 나오게 만드는 도구입니다.
가장 유명한 것은 바벨(Babel)로 개발할 때는 최신 자바스크립트 문법을 사용하되, 바벨로 컴파일을 하면 구형 브라우저 호환이 되는 자바스크립트 문법으로 변환돼 호환성 걱정 없이 최신 문법을 사용할 수 있게 되었습니다.
실무 환경에서는 바벨을 직접 사용하기보단 웹팩으로 통합해 사용하는 경우가 많습니다.
트랜스파일러는 원본 코드를 구형 버전의 JS로 변환해 번들러에게 전달합니다. 또한 변환 전후의 추상화 수준이 다른 빌드와는 다르게 트랜스파일은 추상화 수준을 그대로 유지합니다. (TS → JS, JSX → JS, Sass → css)
Webpack
웹팩은 자바스크립트 애플리케이션을 위한 정적 모듈 번들러이며 특징은 다음과 같습니다.
Webpack 특징
- 오래된만큼 생태계가 풍부하고 안정성이 뛰어난 번들러
- 서드파이 라이브러리나 CSS 전처리, 이미지 에셋 등 다른 번들러보다 강점을 보입니다.
- JS로 변환하기 위한 로더와 플러그인 설치가 필요
- 웹팩은 JS와 JSON 파일만 이해하기에 다른 형태의 파일들은 웹팩이 이해할 수 있도록 변경해야함
- 로더 : 번들 되기 전 파일 단위를 처리
- 플러그인 : 번들된 결과물을 추가로 처리 (ex : 자바스크립트를 난독화하는 등의 후처리에 사용)
Webpack 주요 개념
웹팩은 Entry, Output, Loaders, Plugins, Mode로 구성되어 있으며 각각의 개념을 알아보겠습니다.
Entry
- entry는 웹팩이 빌드할 파일의 시작 위치를 나타냄
- entry 지점으로부터 import되어 있는 다른 모듈과 라이브러리에 대한 의존성을 찾음
- 기본값은 ./src/index.js
module.exports = {
entry: ' ./src/index.js'
};
Output
- 웹팩에 의해 생성되는 번들을 내보낼 위치와 파일명을 지정해 웹팩에게 알려주는 역할
- 기본값은 ./dist/main.js
const path = require('path');
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js'
}
};
Loaders
- 웹팩은 기본적으로 자바스크립트와 JSON 파일만 이해하기에 자바스크립트 파일이 아닌 다른 파일들도 유효한 모듈로 변환시켜주는 역할
- TS → JS, 이미지 → data url 형식의 문자열로 변환, 혹은 css 파일을 자바스크립트에서 직접 로딩할 수 있도록 해주는 역할
- 로더는 오른쪽 → 왼쪽 (또는 아래 → 위) 방향으로 평가/실행됨
- 싱글 모듈에 대한
rules
프로퍼티를 정의해야 하며, rules 프로퍼티는 test, use 프로퍼티를 소유 test
: 변환이 필요한 파일들을 식별하는 속성use
: 변환 수행에 사용되는 로더를 명시하는 속성- 아래 예제는 sass-loader로 실행시 시작되고, css-loader로 이어지며 마지막으로 style-loader로 끝남
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
modules: true,
},
},
{ loader: 'sass-loader' },
],
},
],
},
};
Plugins
- 웹팩의 기본적인 동작에 추가적인 기능을 제공하는 속성
- 로더는 번들되기 전 파일 단위를 처리 (파일을 해석하고 변환하는 과정에 기여) / 플러그인은 번들된 결과물을 추가로 처리 (해당 결과물의 형태를 바꾸는 역할)
const HtmlWebpackPlugin = require("html-webpack-plugin"); // 웹팩으로 빌드한 결과물로 HTML 파일을 생성해주는 플러그인
const path = require("path");
const webpack = require("webpack");
module.exports = {
entry:'./src/index.tsx',
module: {
rules: [
{
test: /\.(ts|tsx|js|jsx)$/,
use: "babel-loader",
exclude: /node_modules/,
},
{
test: /\.scss$/,
use: [
"style-loader",
"css-loader",
"sass-loader"
],
exclude: /node_modules/
},
],
},
plugins: [
// 생성된 모든 번들을 자동으로 삽입해 애플리케이션용 HTML 파일 생성
new HtmlWebpackPlugin({
template: 'public/index.html',
// 메타 태그
meta: {
'theme-color': '#4285f4',
'description': 'webpack with wynter',
},
}),
// 자주 사용되는 모듈을 미리 등록하여 매번 작성하지 않게 해줌
new webpack.ProvidePlugin({
React: "react",
}),
],
resolve: {
alias: {
"@": path.resolve(__dirname, "..src/"),
},
extensions: [".js", ".ts", ".jsx", ".tsx", ".css", ".json"],
},
};
Mode
- 웹팩 4부터 추가된 개념으로 웹팩의 실행 모드를 설정할 수 있음
- none, development, production으로 설정하며 실행 모드에 따라 환경별 최적화 진행이 달라짐
- 자세한 내용은 공식 문서를 참고
module.exports = {
mode: 'development',
};
Webpack 핵심 개념 정리
- entry부터 시작해 의존성이 있는 모든 모듈을 찾음
- loaders를 통해 Webpack이 이해할 수 있는 유효한 모듈로 변환
- 위 모듈들을 병합하고 압축해 output에 번들된 결과물을 생성
- plugins를 통해 생성된 결과물에 추가적인 작업 처리
지금까지 모듈, 모듈 번들러, Webpack에 대해 알아봤습니다. 다음 글에서는 Webpack + ESBuild 방식을 적용해 빌드 속도를 개선한 경험에 대해 알아보겠습니다.
참고 자료
https://wiki.commonjs.org/wiki/CommonJS
https://ko.javascript.info/modules-intro#ref-1216
https://ui.toast.com/fe-guide/ko_BUNDLER
https://ui.toast.com/fe-guide/ko_BUNDLER
https://snipcart.com/blog/javascript-module-bundler
'React' 카테고리의 다른 글
[React] React 성능 개선의 여정 (144ms에서 61ms까지) (1) | 2024.11.24 |
---|---|
[React] React Hook 파헤쳐보기 - useEffect (2) | 2024.11.08 |
React 동작원리(Virtual DOM과 Fiber) (1) | 2024.11.03 |
[React] React Hook 파헤쳐보기 - useState (0) | 2024.10.21 |
[React] 변경에 유연한 Headless 컴포넌트 만들기 (0) | 2024.01.16 |