내가 직접 node package를 만드는 과정에서 jsx가 동작하지 않는 상황이 발생했다. 등장한 오류 메세지는 대략 'jsx 구문에 대한 지원이 활성화되지 않았다'는 것이었다. 왜 이런 문제 상황이 발생했는지 찾아보니 번들러(Bundler)인 Webpack과 트랜스파일러(Transpiler)인 Babel의 추가적인 설정이 필요했다.
프론트에서 빌드(Build)와 번들(Bundle)이라는 용어는 많이 혼용된다. 해당 포스트에서는 Webpack의 역할을 더욱 정확히 설명하기 위해 둘의 개념을 분리해서 사용하겠다.
대략적으로 5가지의 과정을 거쳐 프론트엔드 프로젝트가 빌드된다. 여기서 Babel과 Webpack이 수행하는 1번, 2번 과정을 중심으로 포스트를 작성하겠다.
프론트에서 개발할 때 자주 사용되는 JSX, SASS(SCSS), TypeScript와 같은 언어는 브라우저가 이해할 수 없는 언어이다. 브라우저는 HTML, CSS, JavaScript만 이해할 수 있다.
최근 등장한 ES6을 이해하지 못하는 브라우저도 존재한다. 이때는 ES6을 ES5로 변환하는 작업이 필요하기도 하다.
따라서 우리가 작성한 파일을 브라우저가 이해할 수 있는 언어로 변환하는 작업이 필요한데 이 과정을 Transpile이라고 한다.
Compile: MDN에 따르면 컴파일은 컴퓨터 언어로 작성된 컴퓨터 프로그램을 다른 형태나 언어로 변환한 것을 의미한다고 한다.
Compiler: 주로 C, Java와 같은 언어에서 컴퓨터가 이해할 수 있는 이진 코드, 바이트 코드로 변환하는 작업의 의미한다.
Transpiler: 한 언어를 같은 수준의 언어로 변환하는 것을 의미하며 일반적으로 사용하는 컴파일러와의 의미와는 차이가 있어 Source-to-source compiler라고 부른다.
Babel의 공식문서에서는 Javascript compiler로 칭하고 있다. ES6의 언어를 이전의 호환 가능한 JS로 변환하는 작업을 수행한다. 또한, JSX의 문법을 변환한다.
직접 JSX로 파일을 작성하고 Barbel로 변환한 JS 문법을 확인해보자.
npm install --save-dev @babel/cli @babel/preset-env @babel/preset-react
@babel/cli: Babel의 CLI를 제공하는 패키지이다. 터미널에서 사용자가 Babel을 통해 컴파일할 수 있다.
@babel/preset-env: 구형의 브라우저에서 호환되도록 변환하는데 필요한 플러그인들을 자동으로 관리해주는 패키지이다. 지원하고자 하는 브라우저를 지정할 수 있으며 babel.config.js 혹은 .bablerc 파일에서 사용자가 다양한 설정을 할 수 있다.
@babel/preset-react: React와 관련된 플러그인들을 자동으로 포함시켜준다. 해당 플러그인에는 JSX의 문법을 해석하고 JS로 변환시켜주는 플러그인들이 포함되어 있다.
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
babel에서 사용할 preset을 정의했다. 각자의 프리셋에 대한 설명은 위와 같다.
"scripts": {
"test": "npm test",
"build": "babel src -d dist"
}
빌드 대상이 될 파일과 파일이 출력될 위치를 지정한다. 위 코드는 src 폴더 내의 파일을 transpile한 뒤 dist라는 폴더 내에 출력된 파일을 위치시킨다는 의미이다.
이제 내가 만든 패키지를 사용할 수 있겠지?? 두근두근

실패다.. 내가 만든 패키지를 찾은 수 없단다..
이유를 찾아보니 파일을 프로젝트 외부로 내보내기 위한 번들링의 과정이 필요하다고 한다. CRA에서는 webpack이라는 번들러를 사용한다는 것을 알고 Webpack을 사용해보기로 했다.
Bundler -> Bundle (묶다) + er (행위를 하는 사람)
즉, Bundler는 묶는 행위를 하는 대상을 의미한다. 그렇다면 Bundle은 무엇을 의미하는 것일까?
18.2 version의 React Code-Splitting 문서를 찾아보면 Bundling은 파일을 import하고 이들을 하나의 파일로 합치는 과정이라고 설명하고 있다.
// app.js
import {add} from "./math.js"
console.log(add(16, 26)); //42
// math.js
export function add(a, b){
return a + b;
}
Bundle
function add(a,b){
return a + b;
}
console.log(add(16, 26));
위와 같은 과정이 번들링인 것이다.
우리는 왜 여러 js 파일들을 왜 하나로 묶어야 할까?
1. 여러 js 파일들을 하나로 합침으로써 HTTP의 요청 수를 줄여 로드 시간을 단축시킬 수 있다.
2. 번들링의 과정에서 js 파일을 최소화하고 압축함으로써 브라우저에 전달되는 파일의 크기를 줄인다.
3. code Splitting을 통해 코드를 다양한 번들로 분해하며 요청에 따라 필요한 번들만을 로드하거나 병렬로 로드할 수 있다.
1. 여러 모듈 간의 의존성을 자동으로 해결하고 파일 간의 충돌을 방지한다.
엔트리 포인트부터 의존성 분석을 시작한다. 이 엔트리 포인트의 경로 정보를 Resolver instance에 전송하면 resolver는 파일 시스템의 정보, 절대 경로, 고유 ID을 반환한다. 이때, 고유 ID는 webpack이 의존성 그래프를 생성하는데 중요한 역할을 한다. 이후 resolver에서 반환된 값을 바탕으로 모듈을 생성한 후 파싱한다. 로더를 통해 파싱한 데이터를 바탕으로 AST(Abstract Syntax Tree)를 생성, 이를 바탕으로 의존성을 분석함으로써 dependency graph를 생성한다.
2. 같은 코드를 모듈로 따로 분리시킴으로써 코드의 재사용성을 향상시킨다.
1. Tree Shaking을 통해 사용하지 않는 코드를 제거함으로써 파일 크기를 최소화한다.
1. 코드를 하나의 번들로 묶음으로써 원본의 소스 코드가 외부에 노출되지 않게 한다.
2. 파일 충돌과 버전 충돌을 방지함으로써 안정성을 제공한다.
1. 대규모의 프로젝트에서도 번들링을 통해 구조를 명확하게 하고 협업을 원활하게 한다.
npm i --save-dev webpack webpack-cli
const path = require('path');
module.exports = {
entry: './src/index.js', //entry point 지정
output: {
filename: 'bundle.js', //output file name 지정
path: path.resolve(__dirname, 'dist'), //root폴더에 dist 폴더 생성, 해당 폴더에 bundle.js 저장
library: { name: "MainModule", type: "umd" }, //MainModule이라는 라이브러리를 umd 형식으로 내보낸다.
globalObject: "this", //이 객체에 라이브러리를 마운트한다.
},
externals: {
react: "react", //react는 외부 의존성으로 설정 (출력 번들에서 의존성 제외)
"react-dom": "react-dom", //react-dom 또한 외부 의존성 설정
},
module: {
rules: [ //모듈 생성 시 규칙 생성
{
test: /\.(js|jsx)$/, //해당 테스트를 통과하는 모든 모듈 포함
exclude: /node_modules/, //node_modules는 제외
use: ["babel-loader"], //사용할 로더 적용
},
],
},
resolve: {
extenstions: ["*", ".js", ".jsx"], //전체 파일, js, jsx 순으로 해석
},
};
참고문헌
https://medium.com/webpack/the-contributors-guide-to-webpack-part-3-44cc149af02c