Babel(+Polyfill) & Webpack

Babel

Babel is a JavaScript compiler.

Babel is a toolchain that is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments.

Babel은 최신 자바스크립트 문법을 지원하지 않는 브라우저에서도 자바스크립트가 실행될 수 있게 옛날 문법으로 변환시켜준다.

컴파일러와 트랜스파일러의 차이

  • compiler : 코드를 바이트 코드로 변환(사람이 이해할 수 있는 하이 레벨의 코드를 컴퓨터가 이해할 수 있는 로우 레벨의 기계어로 변환)한다.
  • transpiler : 코드를 같은 레벨의 다른 언어로 변환한다.

JS transpiler는 coffeescript, typescript, babel 등이 있다. coffeescript와 typescript는 고유 문법을 JS로 변환하고, babel은 JS 코드를 JS 코드로 변환하는 transpiler이다.

그런데 공식 홈페이지에서는 바벨을 컴파일러로 소개한다. 그리고 바벨을 컴파일러라고 부르는 사람도 있다. 컴파일러와 트랜스파일러의 구분이 모호한 것 같다. 어떻게 불러도 크게 중요하지 않은 것 같다.

바벨은 구 브라우저에서 최신 자바스크립트 코드가 작동하도록 변환해주는 컴파일러(혹은 트랜스파일러)이다.

바벨 등장 배경

왜 Babel이 등장했을까? ES6+는 지원하지 않는 브라우저들이 많기에(IE11은 최신 버전임에도 불구하고 ES6를 지원하지 않는다.) ES5 코드로 바꿔주어야 하기 때문이다. (다행히도 마이크로소프트가 2021년 8월에 IE 지원 종료를 선언했다. 👏🏻👏🏻)

즉, Babel은 크로스 브라우징 이슈를 해결하기 위해 생겨난 도구이다. ES6+ 버전의 JS나 TS, JSX 등 다른 언어로 분류되는 언어들에 대해서도 모든 브라우저에서 동작할 수 있도록 호환성을 지켜준다. (이래서 컴파일러라고 소개하는 것 같기도 하다.)

Babel의 빌드 단계

  1. 파싱(Parsing) : 코드를 읽고 추상 구문 트리(AST)로 변환하는 단계
  2. 변환(Transforming) : 추상 구문 트리를 변경
  3. 출력(Printing) : 변경된 결과물을 출력

여기서 Babel은 파싱과 출력을 담당하고 변환은 Plugin이 진행한다. 바벨 플러그인은 바벨이 어떤 코드를 어떻게 변환할지에 대한 규칙을 나타낸다. 플러그인은 직접 만들어서 커스텀 플러그인을 사용해도 되고, 이미 잘 만들어진 플러그인을 가져다 써도 된다.

그런데 플러그인을 매번 일일이 설정하는 것은 매우 귀찮은 일이다. 그렇기 때문에 이런 플러그인들을 목적에 따라 세트로 묶어놓은 경우가 많다. 이러한 세트를 preset이라 한다. 프리셋 역시 마찬가지로 직접 모듈을 만들어 설정할 수도 있고, 바벨에서 제공하는 프리셋을 가져와서 사용할 수도 있다.

대표적인 프리셋으로 ES6+를 변환하는 프리셋인 preset-env가 있다. (@babel/preset-env)

Polyfill이란?

Babel과 Polyfill은 명확히 구분되는 개념이다.

Polyfill은 최신 ECMAScript 환경을 만들기 위해 코드가 실행되는 환경에 존재하지 않는 빌트인, 메소드 등을 추가하는 역할을 한다.

Babel은 ES5로 변환할 수 있는 것들만 변경해준다. ES5에 이미 존재했던 개념은 Polyfill은 최신 ECMAScript 환경을 만들어준다. Babel은 ES6->ES5 변환할 수 있는 것들만 변환한다. 근데 Promise같이 ES6에서 비동기 처리를 위해 등장한 것은 ES5에서 변환할 수 있는 대상이 없어 에러가 발생한다. 이런 경우 Polyfill을 이용하여 이슈를 해결할 수 있다.

모던 자바스크립트를 이용해 스크립트를 작성하려면 바벨과 같은 트랜스파일러와 폴리필은 필수이다.

@babel/preset-env를 이용하면 지원할 브라우저 정보와 일부 옵션 지정 시 자동으로 필요한 기능들(컴파일에 필요한 바벨 변환 플러그인들과 core-js polyfills)을 주입해준다.

자세한 내용은 공식 문서를 참고하자.

설치해야 하는 라이브러리

  • @babel/core
  • @babel/preset-env
  • @babel/cli (사용 시)

바벨 관련 설정은 .babelrc(+ .json or .js or .cjs or .mjs) 혹은 .babel.config.json(or js or cjs or mjs)파일에서 하면 된다. 참고

SWC(Speedy Web Compiler)란?

SWC 공식 홈페이지

SWC (stands for Speedy Web Compiler) is a super-fast TypeScript / JavaScript compiler written in Rust.

SWC의 개발자는 강동윤님으로, (자랑스러운) 한국인이다. 현재 Next.js를 개발한 Vercel에 스카웃되셔서 근무하고 계시다.

강동윤님의 블로그를 읽어봐도 좋을 것 같다.

SWC is 20x faster than Babel on a single thread and 70x faster on four cores.

SWC는 바벨보다 무려 20배 빠르다고 한다.

웹팩과 연결할 수 있는 swc-loader도 지원한다. Babel 대신 SWC를 사용해봐도 좋을 것 같다.

Webpack

webpack is a static module bundler for modern JavaScript applications.

webpack

Webpack은 최신 프론트엔드 라이브러리/프레임워크에서 가장 많이 사용되는 모듈 번들러이다.

모듈 번들러란 웹 애플리케이션을 구성하는 자원(HTML, CSS, JavaScript, Images, Font 등)을 각각의 모듈로 보고 이를 조합해서 병합된 하나의 결과물을 만드는 도구이다.

웹팩 등장 배경

1. 페이지 로딩 속도 저하

웹 사이트 개발 시 JS, CSS, IMG 등 수많은 리소스 파일이 생겨난다. (개발자 도구 네트워크 탭 확인) 그리고 이는 곧 웹 사이트 접속 시 수많은 파일의 다운로드를 기다려야하는 것을 의미한다. 많은 파일이 다운로드되면 서버와의 통신이 많고, 그것은 곧 로딩 속도가 느리다는 것을 의미한다.

그렇기 때문에 웹팩을 통해 리소스 파일의 개수를 줄여 로딩 속도를 높인다.

2. JS 모듈화 이슈

<script src="/js/vendor/vendor01.js"></script>
<script src="/js/module01.js"></script>
<script src="/js/lib/library01.js"></script>
<script src="/js/module02.js"></script>
<script src="/js/module03.js"></script>

위와 같이 일반적인 방식인 태그로 자바스크립트를 모듈화한다면 여러 개의 파일을 로딩하더라도 같은 스코프를 공유하기 때문에 변수의 충돌이 발생할 수 있다. 그리고 라이브러리 로딩 순서에 따른 이슈, 복잡도 관리 문제도 생길 수 있다.

3. 웹 개발 작업 자동화

이전부터 프론트엔드 개발 업무를 할 때 가장 많이 반복하는 작업은 에디터에서 코드를 수정하고 저장한 뒤 브라우저에서 새로고침을 누르는 일이었다.

이외에도 웹 서비스를 배포할 때 HTML, CSS, JS 압축 / 이미지 압축 / CSS 전처리기 변환 등을 해주어야 했다. 이러한 일들을 자동화해주는 도구들이 필요하게 됐다. 그래서 웹 태스크 매니저(Grunt, Gulp 등)가 등장했다.

이러한 웹 태스크 매니저가 하는 일 + 위에서 말한 모듈 관리까지 해주는 게 웹팩이다.


이와 같은 문제들을 해결하기 위해 Bundler가 등장했고, 번들러 중 가장 많이 사용되는 것이 바로 Webpack이다. 또한, Webpack은 Code Splitting 기능 또한 제공한다.

Code Splitting이란?

코드 스플리팅이란 하나의 큰 번들을 여러 개의 작은 번들들로 쪼개준다.

그렇다면 위에서는 여러 개의 파일을 한꺼번에 로드하면 로딩 속도가 오래 걸리므로 합치기 위해서 Webpack을 쓴다고 해놓고 왜 또 쪼개는 것일까?

그것은 바로 첫 로딩 시간이 오래 걸리기 때문이다. 한 페이지만 로드하면 되는 첫 시작 페이지에서 필요로 하지 않는 여러 페이지(또는 프레임워크를 사용하는 경우 import한 컴포넌트 파일)을 로딩하기 때문이다.

웹팩은 바로 이러한 문제를 해결하기 위해 코드 스플리팅을 제공한다.

Code Splitting | webpack를 보면 코드 스플리팅을 하는 방법으로는 3가지(Entry Points / Prevent Duplication / Dynamic Imports)가 있다고 한다. 더 자세한 것은 링크에 들어가 확인하길 바란다.

Lazy Loading이란?

Lazy Loading이란 페이지를 불러오는 시점에 당장 필요하지 않은 리소스들을 추후에 로딩하게 하는 기술이다.

즉, 웹팩은 Code Splitting을 통하여 Lazy Loading을 하는 것이다.

무한 스크롤과 이미지 플레이스홀더가 대표적인 레이지 로딩의 예시이다.

Dynamic Import란?

Dynamic Import은 말 그대로 동적으로 import 할 수 있게 해주는 구문이다.

Dynamic Import를 사용하면 런타임 시에 필요한 module을 import할 수 있다.

위에서도 말했듯이 웹팩은 Dynamic Import를 이용하여 Code Splitting을 지원해준다.

설치해야 하는 라이브러리

  • webpack
  • webpack/cli
  • webpack-dev-server (개발 시 유용)
  • babel-loader (babel 사용 시, webpack과 babel을 연결해준다. 실무 환경에서는 바벨을 따로 사용하지 않고 웹팩을 통해 사용하게 된다.)

웹팩의 4가지 주요 속성

웹팩의 속성들은 웹팩 설정 파일(webpack.config.js)에 명시한다.

webpack의 4가지 주요 속성

1. entry

entry 속성은 웹팩에서 웹 자원을 변환하기 위해 필요한 최초 진입점이자 자바스크립트 파일 경로이다. 즉, 웹팩을 실행할 대상 파일을 의미한다. entry 속성에 번들링하고 싶은 파일들을 선언한다.

entry 속성에 지정된 파일은 웹 애플리케이션의 전반적인 구조와 내용이 담겨 있어야 한다. 웹팩은 entry 속성에 지정된 파일을 보고 웹 애플리케이션에서 사용되는 모듈들의 연관 관계를 이해하고 분석하기 때문이다.

웹팩은 엔트리를 통해서 필요한 모듈들을 로딩하고, 하나의 파일로 묶는 과정을 진행한다.

// webpack.config.js
module.exports = {
  entry: {
    app: './src/index.js', // app이 entry의 이름이 된다.
  },

  // 또는 단순하게 파일 이름만 명시할 수도 있다.
  entry: './src/index.js',
}

2. output

output 속성은 웹팩을 돌리고 난 결과물의 파일 경로를 의미한다.

번들된 결과물을 처리할 위치를 output에 기록한다.

최소한 filename은 지정해줘야 하며, 일반적으로 path 속성을 함께 정의한다.

// webpack.config.js
const path = require('path')

module.exports = {
  output: {
    filename: '[name | id | chunkhash].js',
    // [] 안의 값에 따라 결과 파일의 이름은 각각 엔트리 이름, 모듈 아이디, 해시 값 등으로 지정된다.
    // [name]으로 한다면 위의 entry 코드에서 이름을 명시한 경우 그 이름(app).js으로 파일이 생성된다.
    // 명시하지 않았다면 main.js로 파일이 생성된다.
    path: path.resolve(__dirname, './dist'),
  },
}

3. loader

loader 속성은 웹팩이 웹 애플리케이션을 해석할 때 JS 파일이 아닌 웹 자원(HTML, CSS, Images, Font 등)을 변환할 수 있도록 도와주는 속성이다. (Webpack은 JS밖에 모른다.)

TS 같은 다른 언어를 JS 문법으로 변환해 주거나 이미지를 data URL 형식의 문자열로 변환해준다. 또한, CSS 파일을 JS에서 직접 로딩할 수 있도록 해준다.

entry, output과 달리 module이라는 이름을 사용한다.

웹팩은 모든 파일을 모듈로 관리하지만 JS만 알고 있어서 다른 파일들을 웹팩이 이해할 수 있도록 변경해주는 것이 로더의 역할이다.

// webpack.config.js
module.exports = {
  module: {
    rules: [
      { test: /\.css$/, use: ['style-loader', 'css-loader' }, // 모든 CSS 파일에 대해 CSS 로더, style 로더 적용
      { test: /\.ts$/, use: 'ts-loader' }, // 모든 ts 파일에 대해 ts 로더 적용
      // ...
      // test : 로더를 적용할 파일 유형 (일반적으로 정규 표현식 사용)
      // use : 해당 파일에 적용할 로더의 이름 (여러 개 쓰려면 배열로 묶기)
      // 로더는 use에 적힌 것들의 오른쪽부터 순서대로 적용된다. (css -> style)
      // 즉, sass 파일에 로더를 적용시키고 싶다면 sass -> css -> style 순이어야 한다.
      // CSS 로더는 CSS 코드가 웹팩 안으로 들어갈 수 있게(빌드 결과물에 CSS 코드가 포함되게) 해주는 것이고,
      // style 로더는 스타일 코드를 head 안에 inline으로 넣어주는 것이다.
      // 즉, CSS 로더로 처리하면 자바스크립트 코드로만 변경되었을 뿐 돔에 적용되지 않았기 때문에 스타일이 적용되지 않은 상태이다.
      // style 로더는 자바스크립트로 변경된 스타일을 동적으로 DOM에 추가하는 로더이다.
    ],
  },
}

4. plugin

plugin 속성은 웹팩의 기본적인 동작에 추가적인 기능을 제공하는 속성이다.

웹팩으로 변환한 파일에 추가적인 기능을 더하고 싶을 때 사용한다. 웹팩 변환 과정 전반에 대한 제어권을 가지고 있다.

loader는 파일을 해석하고 변환하는 과정에 해당하고, plugin은 해당 결과물의 형태를 바꾸는 과정에 해당한다.

플러그인의 배열에는 생성자 함수로 생성한 객체 인스턴스만 추가될 수 있다.

// webpack.config.js
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  plugins: [
    new HtmlWebpackPlugin(), // 웹팩으로 빌드한 결과물로 HTML 파일을 생성해주는 플러그인
    new webpack.ProgressPlugin(), // 웹팩의 빌드 진행율을 표시해주는 플러그인
  ],
}

resolve, devtool, devServer

resolve, devtool, devServer 속성에 대해서도 알고 있으면 좋다.

// webpack.config.js
module.exports = {
  devtool: 'eval', // hidden-source-map
  resolve: {
    alias: {
      Utilities: path.resolve(__dirname, 'src/utilities/'),
      Templates: path.resolve(__dirname, 'src/templates/'),
    },
    extensions: ['.jsx', '.js', '.tsx', '.ts'],
  },
  // devtool : 소스맵 설정
  // resolve : alias, extensions 등 여러 가지 옵션이 있다.
  // alias는 별칭을 지어서 특정 모듈을 더 쉽게 import 해올 수 있게 한다.
  // extensions는 여러 파일이 동일한 이름을 공유하지만 확장자가 다른 경우,
  // 배열에서 먼저 나열된 확장자를 가진 파일을 확인하고 나머지는 건너 뛴다.
}

“소스 맵(Source Map)“이란 배포용으로 빌드한 파일과 원본 파일을 서로 연결시켜주는 기능이다. 보통 서버에 배포할 때 성능 최적화를 위해 HTML, CSS, JS와 같은 자원들을 압축한다. 그런데 만약 압축하여 배포한 파일에서 에러가 난다면 어떻게 디버깅을 할 수 있을까? 이때 바로 소스 맵을 이용해서 배포용 파일의 특정 부분이 원본 소스의 어떤 부분인지 확인한다.


devServer 속성은 webpack-dev-server 관련 설정을 할 수 있다.

DevServer | webpack 참조

// webpack.config.js
module.exports = {
  devServer: {
    port: 9000,
    hot: true,
    // HMR(Hot Module Replacement) : 브라우저를 새로 고치지 않아도 웹팩으로 빌드한 결과물이
    // 웹 애플리케이션에 실시간으로 반영될 수 있게 도와주는 설정이다.
    // 브라우저 새로고침을 위한 LiveReload 대신에 사용할 수 있으며,
    // 웹팩 데브 서버와 함께 사용할 수도 있다.
    // 리액트, 앵귤러, 뷰와 같은 대부분의 라이브러리/프레임워크에서 이미 HMR을 사용할 수 있는 로더들을 지원하고 있지만
    // 개별적으로 설정하고 싶다면 위와 같이 설정할 수 있다.
  },
}

추가로 서버에 API 요청과 같은 다른 Origin으로 요청을 보내기 위해 프록시 설정을 해야 한다면 프록시(Proxy) 설정 | Webpack Dev Server을 참고하자.

웹팩 데브 서버 (devServer)

웹팩 데브 서버는 웹 애플리케이션을 개발하는 과정에서 유용하게 쓰이는 도구이다.

웹팩의 빌드 대상 파일이 변경되었을 때 매번 웹팩 명령어를 실행하지 않아도 코드만 변경하고 저장하면 웹팩으로 빌드한 후 브라우저를 새로고침해준다.

매번 명령어를 치는 시간과 브라우저를 새로고침하는 시간뿐만 아니라 웹팩 빌드 시간 또한 줄여준다.


웹팩 데브 서버를 실행하여 웹팩 빌드를 하는 경우에는 빌드한 결과물이 파일 탐색기나 프로젝트 폴더에서 보이지 않는다. 좀 더 구체적으로 얘기하면, 웹팩 데브 서버로 빌드한 결과물은 메모리에 저장되고 파일로 생성하지는 않기 때문에 컴퓨터 내부적으로 접근할 수 있지만 사람이 직접 눈으로 보고 파일을 조작할 수는 없다. (컴퓨터 구조 관점에서 파일 입출력보다 메모리 입출력이 더 빠르고 컴퓨터 자원이 덜 소모된다.)

따라서 웹팩 데브 서버는 개발할 때만 사용하다가 개발이 완료되면 웹팩 명령어를 이용해 결과물을 파일로 생성해야 한다.

// package.json
{
  "scripts": {
    // "dev": "webpack-dev-server", // 버전 4
    "dev": "webpack serve", // 버전 5부터는 이 명령어를 사용한다.
    "build": "webpack"
  }
}

웹팩 실행 시 필수 3가지 속성

mode 속성은 웹팩에 내장된 최적화 방법 중 어느 것을 사용할지 웹팩에게 알려주는 역할을 한다.

mode

module.exports = {
  mode: 'production' | 'development' | 'none',
}

mode에 따라 웹팩 설정을 다르게 실행하고 싶다면 객체 대신 함수를 반환해야 한다.

var config = {
  entry: './app.js',
  //...
}

module.exports = (env, argv) => {
  if (argv.mode === 'development') {
    config.devtool = 'source-map'
  }

  if (argv.mode === 'production') {
    //...
  }

  return config
}

entry

위에서 설명했으므로 생략한다.

output

위에서 설명했으므로 생략한다.

추가로 읽으면 좋은 글

참고


Written by정선아
🌱 공부한 것을 기록하여 성장하기 위한 블로그입니다.

GitHubGmail