실전 리액트 프로그래밍 개정판 - 이재승 지음 (참고자료)
지난번에 간단하게 바벨과 웹팩을 언급했었다.
바벨과 웹팩은 웹 애플리케이션을 제작할 때 없어서는 안 되는 기반 기술이 되었다. 리액트의 create-react-app, next.js 등의 도구는 바벨과 웹팩을 기본적으로 포함한다.
바벨은 다음과 같이 다양한 방식으로 실행될 수 있다.
@babel/register를 이용하면 노드에서 require 코드가 실행될 때 동적으로 바벨이 실행되게 할 수 있다. (리액트를 @babel/register와 함께 사용하는 경우는 많지 않다.)
실습을 위한 프로젝트를 생성해 보자.
mkdir test-babel-how
cd test-bable-how
npm init -y
필요한 패키지들을 설치하자.
npm install @babel/core @babel/cli @babel/plugin-transform-arrow-functions @babel/plugin-transform-template-literals @babel/preset-react
바벨을 실행하기 위해서는 @babel/core 패키지를 필수로 설치해야 한다. 두 개의 플러그인과 프리셋 하나를 추가로 설치했다.
이제 코드를 작성해 보자. 프로젝트 루트에 src폴더를 만들고 그 밑에 code.js 파일을 만든다.
다음 명령어를 실행해 보자.
npx babel src/code.js --presets=@babel/preset-react --plugins=@babel/plugin-transform-template-literals,@babel/plugin-transform-arrow-functions
이렇게 하면 다음 내용이 출력된다.
@bable/cli로 거의 모든 설정값을 표현할 수 있지만, 설정할 내용이 많거나 실행 환경에 따라 설정값이 다른 경우에는 설정 파일을 따로 만드는 게 좋다.
바벨 6까지는 .babelrc 파일로 설정값을 관리했지만, 바벨 7부터는 babel.config.js 파일로 관리하는게 좋다.
babel.config.js 파일을 만든 다음 내용을 입력해 보자.
앞에서 바벨 클리 명령어로 입력했던 설정과 같은 내용이다. 자바스크립트 파일이기 때문에 동적으로 설정값을 만들 수 있다.
이제 명령어는 다음처럼 간소화된다.
npx babel src/code.js
컴파일된 결과를 저장하고 싶다면 다음과 같이 입력해 보자.
npx babel src/code.js --out-file dist.js
npx babel src --out-dir dist
첫 번째 명령어는 파일 단위로, 두 번째 명령어는 폴더 단위로 처리한다.
웹팩을 이용하기 위해 다음과 같이 패키지를 설치해 보자.
npm install webpack webpack-cli babel-loader
프로젝트 루트에 webpack.config.js 파일을 만들고 다음 내용을 입력한다.
이제 웹팩을 실행해 보자.
npx webpack
파일의 앞부분에는 웹팩의 런타임 코드가 추가된다. 파일의 뒷부분에서는 바벨이 생성한 코드를 확인할 수 있다.
프로젝트 루트에 runBabel.js 파일을 만들고, 다음 코드를 입력하자.
파일로 저장하길 원한다면 fs 모듈을 이용하면 된다.
다음 명령어를 실행하면 의도한 대로 잘 동작하는 것을 확인할 수 있다.
node runBabel.js
@bable/core 모듈을 직접 사용하는 방식은 자유도가 높다는 장점이 있다. 같은 코드에 대해 다음과 같이 두 가지 설정을 적용한다고 생각해 보자.
//설정 1
const presets = ['@babel/preset-react'];
const plugins = ['@babel/plugin-transform-template-literals'];
//설정 2
const presets = ['@babel/preset-react'];
const plugins = ['@babel/plugin-transform-arrow-functions'];
@babel/cli 또는 babel-loader를 이용한다면 바벨을 두 번 실행해야 한다. @babel/core를 사용하면 바벨을 좀 더 효율적으로 실행할 수 있다.
바벨은 컴파일 시 다음 세 단계를 거친다.
AST는 코드의 구문(syntax)이 분석된 결과를 담고 있는 구조체다.
코드가 같다면 AST도 같기 때문에 같은 코드에 대해서 하나의 AST를 만들어 놓고 재사용할 수 있다.
runBabel2.js 파일을 만들고, 다음 코드를 입력해 보자.
바벨 설정 파일에서 사용할 수 있는 다양한 속성 중에서 extends, env, overrides 속성을 알아보자.
extends 속성을 이용하면 다른 설정 파일을 가져와서 확장할 수 있고, env 또는 overrides 속성을 이용하면 환경별 또는 파일별로 다른 설정을 적용할 수 있다.
프로젝트를 하나 만들어 보자.
mkdir test-babel-config
cd test-babel-config
npm init -y
npm install @babel/core @babel/cli @babel/plugin-transform-arrow-functions @babel/plugin-transform-template-literals @babel/preset-react babel-preset-minify
먼저 프로젝트 루트에 common 폴더를 만들고 그 밑에 .babelrc 파일을 만든다.
.babelrc 파일에 다음 내용을 입력해 보자.
플러그인에 옵션을 설정할 때는 배열로 만들어서 두 번째 자리에 옵션을 넣는다. 템플릿 리터럴 플러그인에 loose 옵션을 주면 문자열을 연결할 때 concat메서드를 사용하는 대신 + 연산자를 사용한다.
프로젝트 루트에 src 폴더를 만들고 src 폴더 밑에 example-extends 폴더를 만들어 보자. 그다음 example-extends 폴더 밑에 .babelrc 파일을 만들고, 다음 내용을 입력한다.
템플릿 리터럴 플러그인은 가져온 설정에 이미 존재한다. 이때 플러그인 옵션은 현재 파일의 옵션으로 결정된다.
=> 기존에 설정했던 loose 옵션은 사라진다.
example-extends 폴더 밑에 code.js 파일을 만들고, 다음 코드를 입력하자.
const element = <div>babel test</div>;
const text = `element type is ${element.type}`;
const add = (a, b) => a + b;
이제 바벨을 실행해 보자.
npx babel src/example-extends/code.js
실행 결과는 다음과 같다.
loose 옵션이 적용되지 않았기 때문에 concat 메서드가 보인다.
이제 환경별로 다른 설정값을 적용하는 방법을 알아보자.
src 폴더 밑에 example-env 폴더를 만들고 그 밑에 .babelrc 파일을 만들고, 다음 내용을 입력하자.
example-env 폴더 밑에 code.js 파일을 만들고 위와 같은 코드를 입력해 보자.
바벨ㄹ에서 현재 환경은 다음과 같이 결정된다.
process.env.BABEL_ENV || process.env.NODE_ENV || "development"
다음과 같이 프로덕션 환경으로 바벨을 실행해 보자.
콘솔에 출력되는 내용은 다음과 같다.
여기서는 압축 프리셋이 적용되어 코드를 읽기가 힘들다. 이번에는 개발 환경에서 바벨을 실행해 보자.
npx babel ./src/example-env
NODE_ENV 환경 변수를 설정하지 않으면 기본값 development가 사용된다.
src 폴더 밑에 example-overrides 폴더를 만들고 그 밑에 .babelrc 파일을 만들고 다음 내용을 입력한다.
example-overrides 폴더 밑에 code.js 파일을 만들고 위에서와 같은 코드를 입력한다. example-overrides 폴더 밑에 service1 폴더를 만든다. 그 다음 code.js 파일을 복사해서 그 밑에 code1.js, code2.js 파일을 만든다.
이제 바벨을 실행해서 세 개의 파일을 변환해 보자.
npx babel ./src/example-overrides
바벨 설정 파일은 크게 두 가지 종류로 나눌 수 있다.
전체 설정 파일과 지역 설정 파일이 어떤 차이가 있는지 알아보자.
프로젝트 하나를 생성한다.
mkdir test-babel-config-file
cd test-babel-config-file
npm init -y
npm install @babel/core @babel/cli @babel/plugin-transform-arrow-functions @babel/plugin-transform-template-literals @babel/preset-react
루트에 babel.config.js 파일을 만들고 다음 코드를 입력하자.
그 다음 루트에 src 폴더를 만들고 그 밑에 service1 폴더를 만든다.
service1 폴더 밑에 .babelrc 파일을 만들고 다음 내용을 입력해 보자.
service1 폴더 밑에 code.js 파일을 만들고 위에서의 코드를 그대로 입력한다.
src/service1/code.js 파일을 위한 설정은 다음과 같이 결정된다.
다음과 같이 바벨을 실행해 보자.
npx babel src
지역 설정 파일의 템플릿 리터럴 플러그인이 적용되면서 전체 설정 파일의 loose 옵션이 적용되지 않았다. 이는 지역 설정이 전체 설정을 덮어쓰기 때문이다.
(코드들이 같아서 지루할 수도 있지만 하나하나 깊이 알아가는 것이니 공부하자)
src 폴더 밑에 service2 폴더를 만들고 다음과 같은 구조로 폴더와 파일을 생성하자.
src/service2/folder1/code.js 파일을 위한 설정은 다음과 같이 결정된다.
다음과 같이 바벨을 실행해 보자.
npm babel src
folder1의 code.js는 전체 설정 파일만 적용된 것을 확인할 수 있다.
자바스크립트의 최신 기능을 모두 사용하면서 오래된 브라우저를 지원하려면 바벨로 코드 문법을 변환하는 동시에 폴리필도 사용해야 한다.
폴리필(polyfill)은 런타임에 기능을 주입하는 것을 말한다. 런타임에 기능이 존재하는지 검사해서 기능이 없는 경우에만 주입한다.
바벨을 사용하면 최신 자바스크립트 표준에 추가된 모든 기능을 사용할 수 있다고 오해하기 쉽다. 바벨을 사용하더라도 폴리필에 대한 설정은 별도로 해야 한다.
한 가지 예로 ES8에 추가된 String.padStart 메서드는 폴리필을 이용해서 추가할 수 있다. 반면에 async await는 폴리필로 추가할 수 없으며, 컴파일 타임에 코드 변환을 해야 한다.
다음은 String.padStart 메서드를 폴리필로 추가하는 코드다.
if (!String.prototype.padStart) {
String.prototype.padStart = func; // func는 padStart 폴리필 함수
}
padStart 메서드가 있는지 검사해서 없는 경우에만 기능을 주입한다.
core-js는 바벨에서 폴리필을 위해 공식적으로 지원하는 패키지다. 가장 간단한 사용법은 core-js 모듈을 자바스크립트 코드로 불러오는 것이다.
import 'core-js';
const p = Promise.resolve(10);
const obj = {
a: 10,
b: 20,
c: 30,
};
const arr = Object.values(obj);
const exist = arr.includes(20);
core-js 모듈을 가져오면 해당 모듈의 모든 폴리필이 포함된다. 따라서 낮은 버전의 브라우저에서도 프로미스, Object.values, 배열의 includes 메서드를 사용할 수 있다.
웹팩을 사용하는 경우에는 다음과 같이 entry 속성에 core-js 모듈을 넣는다.
module.exports = {
entry: ['core-js', './src/index.js'],
//...
};
core-js 모듈은 사용법이 간단하지만, 필요하지 않은 폴리필까지 포함되므로 번들 파일의 크기가 커진다. 이 말은 반대로 번들 파일의 크기에 민감하지 않은 프로젝트에서 사용하기 좋다는 의미이기도 하다.
core-js로부터 직접 필요한 폴리필만 가져오면 번들 파일의 크기를 줄일 수 있다.
import 'core-js/features/promise';
import 'core-js/features/object/values';
import 'core-js/features/array/includes';
const p = Promise.resolve(10);
const obj = {
a: 10,
b: 20,
c: 30,
};
const arr = Object.values(obj);
const exist = arr.includes(20);
core-js 모듈은 폴리필을 추가하는 과정이 번거롭고, 필요한 폴리필을 깜빡하고 포함시키지 않는 실수를 할 수 있다. 하지만 번들 파일의 크기를 최소화할 수 있는 방법이므로 크기에 민감한 프로젝트에 적합하다.
@babel/preset-env 프리셋은 실행 환경에 대한 정보를 설정해 주면 자동으로 필요한 기능을 주입해 준다.
예를들어, babel.config.js 파일에 다음 내용을 입력하면 특정 버전의 브라우저를 위한 플러그인만 포함한다.
const presets = [
[
'@babel/preset-env',
{
targets: '> 0.25%, not dead',
},
],
];
module.exports = { presets };
targets 속성으로 지원하는 브라우저 정보를 입력한다. 여기서는 시장 점유율이 0.25% 이상이고 업데이트가 종료되지 않은 브라우저를 입력했다.
(브라우저 정보는 browserslist라는 패키지의 문법을 사용한다.)
실습을 위한 프로젝트를 만들어 보자.
mkdir test-babel-env
cd test-babel-env
npm init -y
1npm install @babel/core @babel/cli @babel/preset-env core-js
프로젝트 루트에 babel.config.js 파일을 만들고, 다음 코드를 입력하자.
프로젝트 루트에 src 폴더를 만들고 그 밑에 code.js 파일을 만들고, 다음 코드를 입력해 보자.
useBuiltIns 속성에 entry를 입력하면 core-js 모듈을 가져오는 코드는 각 폴리필 모듈을 가져오는 여러 줄의 코드로 변환된다.
바벨을 실행해 보자.
npx babel src/code.js
모듈을 가져오는 코드가 수십 줄에 걸쳐서 출력된다. 여기에 출력되는 폴리필은 크롬 버전 40에 없는 기능을 위한 것이다.
실제로 사용한 기능은 프로미스, Object.values, 배열의 includes 메서드밖에 없는데 불필요하게 많은 폴리필 코드가 추가됐다.
=> useBuiltIns 속성에 usage를 입력하면 코드에서 사용된 기능의 폴리필만 추가된다.
babel.config.js 파일에서 useBuiltIns 속성값을 usage로 입력하자. usage를 입력할 때는 core-js 모듈을 가져오는 코드가 필요하지 않으니 src/code.js 파일에서 core-js 모듈을 가져오는 코드를 제거해 보고 바벨을 실행해 보자.
npx babel src/code.js
이 파일의 코드와 관련된 세 개의 폴리필이 추가되고 문자열의 includes 폴리필이 불필요하게 추가됐다. 이는 바벨이 코드에서 사용된 변수의 타입을 추론하지 못하기 때문이다.
=> 바벨 입장에서는 보수적으로 폴리필을 추가할 수밖에 없다.
번들 파일의 크기를 최적화할 목적이라면 필요한 폴리필을 직접 추가하는 방식이 가장 좋다. 만약 적당한 번들 파일 크기를 유지하면서 폴리필 추가를 깜빡하는 실수를 막고 싶다면 @babel/preset-env가 좋은 선택이 될 수 있다.