[React 정복기] 바벨과 웹팩 자세히 들여다보기 1 (실전 리액트 프로그래밍) - 4

예흠·2020년 12월 15일
0

React 정복기!

목록 보기
4/8
post-thumbnail

React를 정복해보자 💪

실전 리액트 프로그래밍 개정판 - 이재승 지음 (참고자료)

지난번에 간단하게 바벨과 웹팩을 언급했었다.
바벨과 웹팩은 웹 애플리케이션을 제작할 때 없어서는 안 되는 기반 기술이 되었다. 리액트의 create-react-app, next.js 등의 도구는 바벨과 웹팩을 기본적으로 포함한다.

* 바벨 실행 및 설정하기

1. 바벨을 실행하는 여러 가지 방법

바벨은 다음과 같이 다양한 방식으로 실행될 수 있다.

  • @babel/cli로 실행하기
  • 웹팩에서 babel-loader로 실행하기
  • @babel/core를 직접 실행하기
  • @babel/register로 실행하기

@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 파일을 만든다.

- @babel/cli로 실행하기

다음 명령어를 실행해 보자.

npx babel src/code.js --presets=@babel/preset-react --plugins=@babel/plugin-transform-template-literals,@babel/plugin-transform-arrow-functions

이렇게 하면 다음 내용이 출력된다.

  • jsx문법은 createElement 함수 호출로 변환된다.
  • 템플릿 리터럴은 문자열의 concat 메서드 호출로 변환된다.
  • 화살표 함수는 일반 함수로 변환된다.

@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

첫 번째 명령어는 파일 단위로, 두 번째 명령어는 폴더 단위로 처리한다.

- 웹팩의 babel-loader로 실행하기

웹팩을 이용하기 위해 다음과 같이 패키지를 설치해 보자.

npm install webpack webpack-cli babel-loader

프로젝트 루트에 webpack.config.js 파일을 만들고 다음 내용을 입력한다.

  • entry : 웹팩으로 번들링할 파일을 지정한다.
  • output : 번들링된 결과를 dist/code.bundle.js 파일로 저장한다.
  • rulse(bable-loader) : 자바스크립트 파일을 babel-loader가 처리하도록 설정한다.
    => 바벨 로더는 바벨의 설정 파일을 이요하므로 이전에 만들어 놓은 babel.config.js 파일의 내용이 설정값으로 적용된다.
  • optimization : 웹팩은 기본적으로 자바스크립트 파일을 압축한다. 하지만 바벨이 제대로 실행됐는지 확인 하기 위해 압축 기능을 잠시 끈 것이다.

이제 웹팩을 실행해 보자.

npx webpack

파일의 앞부분에는 웹팩의 런타임 코드가 추가된다. 파일의 뒷부분에서는 바벨이 생성한 코드를 확인할 수 있다.

- babel/core를 직접 이용하기

프로젝트 루트에 runBabel.js 파일을 만들고, 다음 코드를 입력하자.

  • source : 컴파일할 파일의 내용을 가져온다.
  • presets, plugins : 바벨 플러그인과 프리셋을 설정한다.
  • transformSync : transformSync함수를 호출해서 바벨을 실행한다.
  • configFile : babel.config.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를 사용하면 바벨을 좀 더 효율적으로 실행할 수 있다.

바벨은 컴파일 시 다음 세 단계를 거친다.

  • 파싱(parse) 단계 : 입력된 코드로부터 AST(abstract syntax tree)를 생성한다.
  • 변환(transform) 단계 : AST를 원하는 형태로 변환한다.
  • 생성(generate) 단계 : AST를 코드로 출력한다.

AST는 코드의 구문(syntax)이 분석된 결과를 담고 있는 구조체다.

코드가 같다면 AST도 같기 때문에 같은 코드에 대해서 하나의 AST를 만들어 놓고 재사용할 수 있다.

runBabel2.js 파일을 만들고, 다음 코드를 입력해 보자.

  • { ast }부분 : 코드는 생성하지 않고 AST만 생성한다. 프리셋은 두 가지 설정 모두 같으므로 AST를 만들 때 해당 프리셋을 미리 적용한다.
  • code1, code2 : 이렇게 만들어진 AST로부터 첫 번째 설정의 플러그인이 반영된 코드를 생성한다. 설정의 개수가 많아질수록 이 방식의 효율은 높아진다.

2. 확장성과 유연성을 고려한 바벨 설정 방법

바벨 설정 파일에서 사용할 수 있는 다양한 속성 중에서 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

- extends 속성으로 다른 설정 파일 가져오기

먼저 프로젝트 루트에 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 메서드가 보인다.

- env 속성으로 환경별로 설정하기

이제 환경별로 다른 설정값을 적용하는 방법을 알아보자.

src 폴더 밑에 example-env 폴더를 만들고 그 밑에 .babelrc 파일을 만들고, 다음 내용을 입력하자.

  • env : env 속성을 이용하면 환경별로 다른 설정을 줄 수 있다.
  • production : 프로덕션 환경에서는 압축 프리셋을 사용하도록 설정한다. 앞에서 설정한 리액트 프리셋은 유지되고 압축 프리셋이 추가되는 형태가 된다.

example-env 폴더 밑에 code.js 파일을 만들고 위와 같은 코드를 입력해 보자.

바벨ㄹ에서 현재 환경은 다음과 같이 결정된다.

process.env.BABEL_ENV || process.env.NODE_ENV || "development"

다음과 같이 프로덕션 환경으로 바벨을 실행해 보자.

  • 맥 : NODE_ENV=production npx babel ./src/example-env
  • 윈도우 : set NODE_ENV=production && npx babel ./src/example-env

콘솔에 출력되는 내용은 다음과 같다.

여기서는 압축 프리셋이 적용되어 코드를 읽기가 힘들다. 이번에는 개발 환경에서 바벨을 실행해 보자.

npx babel ./src/example-env

NODE_ENV 환경 변수를 설정하지 않으면 기본값 development가 사용된다.

- overrides 속성으로 파일별로 설정하기

src 폴더 밑에 example-overrides 폴더를 만들고 그 밑에 .babelrc 파일을 만들고 다음 내용을 입력한다.

  • overrides : overrides 속성을 이용하면 파일별로 다른 설정을 할 수 있다.
  • include : service1 폴더 밑에 있는 파일에는 plugins에 있는 설정을 적용한다.
  • exclude : service1/code2.js 파일에는 plugins 설정을 적용하지 않는다.
    => service1 폴더 하위에서 code2.js 파일을 제외한 모든 파일에 화살표 함수 플러그인을 적용한다.

example-overrides 폴더 밑에 code.js 파일을 만들고 위에서와 같은 코드를 입력한다. example-overrides 폴더 밑에 service1 폴더를 만든다. 그 다음 code.js 파일을 복사해서 그 밑에 code1.js, code2.js 파일을 만든다.

이제 바벨을 실행해서 세 개의 파일을 변환해 보자.

npx babel ./src/example-overrides

3. 전체 설정 파일과 지역 설정 파일

바벨 설정 파일은 크게 두 가지 종류로 나눌 수 있다.

  • 모든 자바스크립트 파일에 적용되는 전체(project-wide) 설정 파일
    => 바벨 버전 7에 추가된 babel.config.js 파일이 전체 설정 파일이다.
  • 자바스크립트 파일의 경로에 따라 결정되는 지역(file-relative) 설정 파일
    => .babelrc, .babelrc.js 파일과 바벨 설정이 있는 package.json 파일이 지역 설정 파일이다.

전체 설정 파일과 지역 설정 파일이 어떤 차이가 있는지 알아보자.

프로젝트 하나를 생성한다.

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 파일을 위한 설정은 다음과 같이 결정된다.

  • package.json, .babelrc, .babelrc.js 파일을 만날 때까지 부모 포더로 이동한다. src/service1/.babelrc 파일을 만났고, 그 파일이 지역 설정 파일이다.
  • 프로젝트 루트의 babel.config.js 파일이 전체 설정 파일이다.
  • 전체 설정 파일과 지역 설정 파일을 병합한다.

다음과 같이 바벨을 실행해 보자.
npx babel src

지역 설정 파일의 템플릿 리터럴 플러그인이 적용되면서 전체 설정 파일의 loose 옵션이 적용되지 않았다. 이는 지역 설정이 전체 설정을 덮어쓰기 때문이다.

(코드들이 같아서 지루할 수도 있지만 하나하나 깊이 알아가는 것이니 공부하자)

src 폴더 밑에 service2 폴더를 만들고 다음과 같은 구조로 폴더와 파일을 생성하자.

src/service2/folder1/code.js 파일을 위한 설정은 다음과 같이 결정된다.

  • package.json 파일을 만났고 package.json 파일에 babel 속성이 없으므로 지역 설정 파일은 없다.
  • 프로젝트 루트의 babel.config.js 파일이 전체 설정 파일이다.

다음과 같이 바벨을 실행해 보자.

npm babel src

folder1의 code.js는 전체 설정 파일만 적용된 것을 확인할 수 있다.

4. 바벨과 폴리필

자바스크립트의 최신 기능을 모두 사용하면서 오래된 브라우저를 지원하려면 바벨로 코드 문법을 변환하는 동시에 폴리필도 사용해야 한다.

폴리필(polyfill)은 런타임에 기능을 주입하는 것을 말한다. 런타임에 기능이 존재하는지 검사해서 기능이 없는 경우에만 주입한다.

바벨을 사용하면 최신 자바스크립트 표준에 추가된 모든 기능을 사용할 수 있다고 오해하기 쉽다. 바벨을 사용하더라도 폴리필에 대한 설정은 별도로 해야 한다.

한 가지 예로 ES8에 추가된 String.padStart 메서드는 폴리필을 이용해서 추가할 수 있다. 반면에 async await는 폴리필로 추가할 수 없으며, 컴파일 타임에 코드 변환을 해야 한다.

다음은 String.padStart 메서드를 폴리필로 추가하는 코드다.

if (!String.prototype.padStart) {
  String.prototype.padStart = func; // func는 padStart 폴리필 함수
}

padStart 메서드가 있는지 검사해서 없는 경우에만 기능을 주입한다.

- core-js 모듈의 모든 폴리필 사용하기

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 모듈에서 필요한 폴리필만 가져오기

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/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 파일을 만들고, 다음 코드를 입력하자.

  • chrome : 크롬 버전을 최소 40으로 설정한다.
  • useBuiltIns : useBuiltIns 속성은 폴리필과 관련된 설정이다. useBuiltIns 속성에 entry를 입력하면 지원하는 브라우저에서 필요한 폴리필만 포함시킨다.
  • corejs : 바벨에게 core-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가 좋은 선택이 될 수 있다.

profile
노래하는 개발자입니다.

0개의 댓글