더 빠른 애플리케이션을 위한 최신 자바스크립트 배포, 제공 및 설치


원문: "Publish, ship, and install modern JavaScript for faster applications" https://web.dev/publish-modern-javascript/- Houssein Djirdeh and Jason Miller 라이선스: CC BY 4.0

최신 자바스크립트로 작성된 의존성 모듈과 결과물 중심으로 성능을 개선한다.


브라우저의 90% 이상이 최신 자바스크립트를 실행할 수 있지만 널리 퍼져있는 레거시 자바스크립트는 오늘날 웹에서 성능 문제를 일으키는 가장 큰 원인 중 하나이다. EStimator.dev는 간단한 웹 기반 도구로, 최신 자바스크립트 문법을 사용하였을 때 해당 사이트가 얼마큼 크기와 성능을 개선할 수 있는지 계산하여 알려준다.

image1

오늘날 웹은 레거시 자바스크립트에 대한 제약이 있으며, ES2017 문법을 사용하여 웹 페이지 또는 패키지를 작성, 배포 및 제공하는 것만큼 성능을 ​​향상시키는 단일 최적화 방식은 없을 것이다.

최신 자바스크립트

최신 자바스크립트는 특정 버전의 ECMAScript 사양으로 작성된 코드가 아니라 모든 최신 브라우저에서 지원되는 문법으로 규정된다. 크롬, 엣지, 파이어폭스, 그리고 사파리와 같은 최신 웹 브라우저들은 브라우저 시장의 90%이상을 차지하고 있으며, 이와 동일한 기본 렌더링 엔진에 의존하는 다른 브라우저가 추가로 5%를 차지한다. 즉, 전 세계 웹 트래픽의 95%가 아래 열거된 기능들을 포함하여 지난 10년간 가장 널리 사용된 자바스크립트 언어 기능을 지원하는 브라우저에서 나온 것이다.

  • 클래스 (ES2015)
  • 화살표 함수 (ES2015)
  • 제너레이터 (ES2015)
  • 블록 스코핑 (ES2015)
  • 디스트럭처링 (ES2015)
  • Rest와 Spread 매개 변수 (ES2015)
  • 객체 축약(Object shorthand) (ES2015)
  • Async/await (ES2017)

일반적으로 언어 사양에서 더 새로운 버전에 해당하는 기능들은 최신 브라우저에서 일관된 동작을 하지 않는다. 예를 들어, 많은 ES2020과 ES2021 기능들은 브라우저 시장의 70%에서만 지원된다. 여전히 대다수의 브라우저에서 지원되지만 이러한 기능을 자유롭게 사용하기엔 문제가 발생할 수 있다. 즉, "최신" 자바스크립트가 움직이는 대상(a moving target - 역자주: 특정 버전의 언어 사양의 정해진 문법으로 작성한 코드가 아니라 최신 브라우저가 지원하는 문법이므로 범위가 달라질 수 있음을 의미)일지라도, ES2017은 일반적으로 사용되는 최신 문법 기능을 포함하여 광범위한 브라우저 호환성을 가지고 있음을 의미한다. 다시 말해, ES2017은 오늘날에 사용되는 최신 문법에 가장 가깝다.

레거시 자바스크립트

레거시 자바스크립트는 위에서 언급한 모든 언어 기능 사용을 피하는 코드이다. 대부분의 개발자는 최신 문법을 사용하여 소스 코드를 작성하지만, 브라우저 지원 범위를 확대하기 위해 모든 코드를 레거시 문법으로 컴파일한다. 레거시 문법으로 컴파일하면 브라우저 지원 범위가 늘지만, 효과는 생각하는 것보다 작다. 브라우저 지원은 약 95%에서 98%로 증가하였지만 다음과 같은 중요한 비용 문제가 발생한다.

  • 레거시 자바스크립트는 같은 동작을 하는 최신 코드와 비교했을 때 약 20%정도 사이즈가 더 크고 느려진다. 도구 결함(Tooling deficiencies)과 빌드 시스템의 잘못된 구성(misconfiguration)은 이 격차를 훨씬 더 넓힐 것이다.
  • 설치된 라이브러리는 일반적인 프로덕션 자바스크립트 코드의 90%를 차지한다. 라이브러리 코드는 최신 코드로 배포되지 않기 위해 폴리필과 복제된 헬퍼 함수가 포함되어 있으며, 이로 인해 훨씬 더 높은 레거시 자바스크립트 오버헤드를 발생시킨다.

npm의 최신 자바스크립트

최근 Node.js는 패키지의 진입점을 정의하는 필드로 "exports"를 표준화했다.

{
  "exports": "./index.js"
}

"exports"필드에 참조된 모듈은 ES2019를 지원하는 Node 버전 12.8이상이라는 것을 내포하고 있다. "exports"필드를 사용하여 참조된 모든 모듈은 최신 자바스크립트로 작성할 수 있다. 패키지 사용자는 "exports"필드가 있는 모듈에는 최신 코드가 포함되어 있으며, 필요한 경우에는 트랜스 파일해서 사용해야 한다는 것을 인지하고 사용하여야 한다.

최신 코드 전용

최신 코드로 패키지를 배포하고, 패키지 사용자에게 트랜스파일 처리를 맡기고 싶다면, 오직 "exports" 필드만을 사용한다.

{
  "name": "foo",
  "exports": "./modern.js"
}

:exclamation: 주의: 이 방법은 권장하지 않는다. 완벽한 세상에서, 모든 개발자는 모든 의존성(node_modules)에 대해 자신의 프로젝트에서 요구하는 문법으로 트랜스파일하기 위한 빌드 시스템을 이미 구성해 놓았을 것이다. 그러나 현재는 이러한 상황이 아니므로, 최신 문법만을 사용하여 패키지를 배포하면 레거시 브라우저를 통해 접근하는 애플리케이션에서는 해당 패키지를 사용할 수 없다.

레거시 폴백을 위한 지원

최신 코드가 사용된 패키지를 배포하고 레거시 브라우저용 ES5 + CommonJS 폴백도 포함하려면 "main"필드와 "exports" 필드를 함께 사용한다.

{
  "name": "foo",
  "exports": "./modern.js",
  "main": "./legacy.cjs"
}

레거시 폴백 및 ESM 번들러 최적화 지원

CommonJS 폴백 파일의 진입점을 정의하는 것 외에도 "module" 필드를 사용하여 비슷한 레거시 폴백 번들을 지정해줄 수 있다. 여기에 사용되는 파일은 자바스크립트 모듈 문법( importexport)을 사용한다.

{
  "name": "foo",
  "exports": "./modern.js",
  "main": "./legacy.cjs",
  "module": "./module.js"
}

webpack 및 Rollup과 같은 많은 번들러는 module 필드를 통해 모듈 기능을 활용하며, 트리 쉐이킹을 가능하게 한다. 이 파일은 import/export 문법을 제외하고는 여전히 최신 코드를 포함하지 않는 레거시 번들 파일이므로, 번들링에 최적화된 레거시 폴백과 최신 코드를 함께 제공하려면 이 접근 방식을 사용하길 바란다.

애플리케이션에서 최신 자바스크립트

일반적으로, 웹 애플리케이션에서 써드 파티 의존성은 프로덕션용 자바스크립트 코드의 대부분을 차지한다. npm 의존성은 역사적으로 레거시 ES5 문법으로 작성되어 배포되었지만 이제는 안전하지 않으며, 의존성 업데이트로 인해 애플리케이션의 지원 브라우저에 문제가 생길 수 있다.

최신 자바스크립트 코드를 배포하는 npm 패키지의 수가 증가함에 따라 빌드 도구가 이를 처리하도록 설정되어 있는지 확인하는 것이 중요하다. 이미 사용하고 있는 npm 의존성 패키지 중 일부가 최신 언어 기능을 사용하고 있을 가능성이 크다. 오래된 브라우저에서 애플리케이션을 멈추지 않고 npm의 최신 코드를 사용할 수 있는 여러 옵션이 있지만, 일반적으로 드는 생각은 빌드 시스템이 의존성 모듈을 프로젝트의 소스 코드와 동일한 문법을 사용한 코드로 트랜스파일하는 것이다.

webpack

webpack 5부터 번들 및 모듈용 코드를 생성할 때 webpack이 사용할 문법을 설정할 수 있다. 코드나 의존성 모듈에 대해 트랜스파일하는 것이 아니라 webpack에 의해 생성된 "접착제(glue)" 코드에만 영향을 미친다. 브라우저 지원 대상을 지정하려면 프로젝트에 browserslist를 설정하거나 webpack 설정 파일에서 직접 해줄 수 있다.

module.exports = {
  target: ['web', 'es2017'],
};

최신 ES 모듈 환경을 target으로 할 때 불필요한 래퍼 기능을 생략하여 최적화된 번들을 생성하도록 webpack을 설정할 수 있다. 또한 <script type="module">을 사용하여 코드 스플리팅된 번들을 로드하도록 webpack을 설정할 수 있다.

module.exports = {
  target: ['web', 'es2017'],
  output: {
    module: true,
  },
  experiments: {
    outputModule: true,
  },
};

Optimize Plugin과 BabelEsmPlugin과 같이, 레거시 브라우저를 계속 지원하면서 최신 자바스크립트 코드를 컴파일하고 제공할 수 있도록 해주는 많은 webpack 플러그인들이 있다.

Optimize Plugin

Optimize Plugin은 최종 번들 코드를 최신 자바스크립트 코드에서 레거시 자바스크립트 코드로 트랜스파일하는 webpack 플러그인이다. 프로젝트의 개별 소스 코드 파일을 Babel로 트랜스파일하는 것과는 다르다. 이 플러그인은 자체 설정(self-contained setup)으로 webpack 설정이 다중 출력 결과물(mutiple output)과 언어 문법에 대한 특별한 분기 없이 모든 코드가 최신 자바스크립트로 되어있다고 가정한다.

Optimize Plugin은 개별 모듈이 아닌 번들에서 동작하기 때문에 애플리케이션 코드와 의존성 코드를 동등하게 처리한다. 따라서, npm의 최신 자바스크립트로 작성된 의존성 모듈을 사용하는 것이 안전한데, 의존성 모듈의 코드가 번들로 제공되며 올바른 문법으로 트랜스파일 되어있기 때문이다. 또한 두 가지 컴파일 단계를 거치는 기존 방식보다 속도가 더 빠를 수 있으며, 최신 브라우저와 레거시 브라우저에 따른 별도의 번들을 생성할 수 있다. 만들어진 두 번들은 module/nomodule 패턴을 사용하여 로드되도록 설계되어 있다.

// webpack.config.js
const OptimizePlugin = require('optimize-plugin');

module.exports = {
  // ...
  plugins: [new OptimizePlugin()],
};

Optimize Plugin을 사용하면, 일반적으로 최신 코드와 레거시 코드를 각각 번들로 제공하기 위해 사용자가 직접 webpack 설정을 해주는 것보다 더 빠르고 효율적일 수 있다. 또한 Babel 실행을 제어하고 최신 코드와 레거시 코드의 출력 결과물의 별도의 최적화 설정을 위해 Terser를 사용하여 번들 파일의 크기를 최소화한다. 마지막으로, 생성된 레거시 번들에 필요한 폴리필은 전용 스크립트로 추출하여 최신 브라우저에서 중복되거나 불필요하게 로드되지 않도록 한다.

비교 : 소스 모듈을 두 번 트랜스파일 vs 생성된 번들을 한 번 트랜스파일

BabelEsmPlugin

BabelEsmPlugin@babel/preset-env와 함께 동작하여 기존 번들 파일을 최신 버전으로 생성하여 최신 브라우저에 덜 트랜스파일 된 코드를 제공하는 webpack 플러그인이다. Next.jsPreact CLI에서 사용되고 있는 module/nomodule을 위한 가장 인기 있고 바로 사용할 수 있는(off-the-shelf) 플러그인이다.

// webpack.config.js
const BabelEsmPlugin = require('babel-esm-plugin');

module.exports = {
  //...
  module: {
    rules: [
      // 기존 babel-loader 설정
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
  plugins: [new BabelEsmPlugin()],
};

BabelEsmPlugin은 애플리케이션의 빌드를 크게 두 가지로 분리하여 실행하기 때문에 다양한webpack 설정을 지원한다. 두 번 컴파일하는 것은 대규모 애플리케이션의 경우 약간의 추가 시간이 소요될 수 있지만, BabelEsmPlugin이 기존에 작성된 webpack 설정과 완벽하게 통합할 수 있으며, 가장 편리하게 사용할 수 있는 옵션 중 하나이다.

node_modules를 트랜스파일하기 위한 bebel-lodader 설정

앞서 얘기한 두 플러그인 중 하나가 없이 babel-loader를 사용하려는 경우에는 최신 자바스크립트로 작성된 npm 모듈을 사용하기 위해서 필수적으로 중요한 단계가 있다. 두 개의 개별 babel-loader 설정을 정의하면 node_modules에 있는 최신으로 작성된 기능을 ES2017로 자동 컴파일하는 동시에, 프로젝트 Babel 설정에 정의된 plugins와 presets를 가지고 퍼스트 파티 코드를 트랜스파일 할 수 있다. module/nomodule 설정을 위한 최신 번들과 레거시 번들 파일을 생성하지 않지만, 오래된 브라우저를 지원하는데 문제를 일으키지 않고 최신 자바스크립트를 포함하는 npm 패키지를 설치하고 사용할 수 있다.

webpack-plugin-modern-npm은 이 기법을 사용하여 package.json에 "exports"필드가 있는 npm 의존성 모듈을 컴파일한다. 의존성 모듈이 최신 문법을 포함할 수도 있기 때문이다.

// webpack.config.js
const ModernNpmPlugin = require('webpack-plugin-modern-npm');

module.exports = {
  plugins: [
    // node_modules에 최신 문법으로 작성된 의존성 모듈을 자동 트랜스파일
    new ModernNpmPlugin(),
  ],
};

다른 방법으로, 모듈이 resolve될 때 package.json에서 "exports"필드를 확인하도록 수동으로 webpack 설정을 작성해줄 수 있다. 짧게 작성하기 위해 캐시 하는 것을 생략했다. 직접 작성한 구현 코드는 아래와 같다.

// webpack.config.js
module.exports = {
  module: {
    rules: [
      // 퍼스트 파티 코드 트랜스 파일
      {
        test: /\.js$/i,
        loader: 'babel-loader',
        exclude: /node_modules/,
      },
      // 최신 문법으로 작성된 의존성 모듈 트랜스파일
      {
        test: /\.js$/i,
        include(file) {
          let dir = file.match(/^.*[/\\]node_modules[/\\](@.*?[/\\])?.*?[/\\]/);
          try {
            return dir && !!require(dir[0] + 'package.json').exports;
          } catch (e) {}
        },
        use: {
          loader: 'babel-loader',
          options: {
            babelrc: false,
            configFile: false,
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
};

위 방법을 사용할 때 최신 문법이 minifier에서 지원되는지 확인해 보아야 한다. Terseruglify-es는 최신 문법을 유지하기 위해 {ecma : 2017}옵션을 제공한다. 상황에 따라서는, 압축(compression)과 포매팅(fromatting) 과정에서 ES2017 문법을 생성한다.

Rollup

Rollup은 단일 빌드의 일부로 여러 번들 셋을 생성하는 빌트인 기능을 제공하며 기본적으로 최신 코드를 생성한다. 따라서 이미 사용 중인 공식 플러그인으로 최신 번들과 레거시 번들 파일을 생성하도록 Rollup을 설정할 수 있다.

@rollup/plugin-babel

Rollup을 사용한다면 getBabelOutputPlugin() 메서드(Rollup의 공식 Babel 플러그인에서 제공됨)가 각각의 소스 모듈이 아닌 생성된 번들 코드를 변환한다. Rollup은 각각 플러그인을 설정하여 단일 빌드의 일부로 여러 번들 세트를 생성하기 위한 기능이 기본적으로 지원된다. 이를 사용하여 최신 버전과 레거시 버전에 따라 getBabelOutputPlugin 옵션에 다른 값을 설정해서 각각 다른 번들 파일을 생성할 수 있다.

// rollup.config.js
import {getBabelOutputPlugin} from '@rollup/plugin-babel';

export default {
  input: 'src/index.js',
  output: [
    // 최신 자바스크립트용 번들 설정
    {
      format: 'es',
      plugins: [
        getBabelOutputPlugin({
          presets: [
            [
              '@babel/preset-env',
              {
                targets: {esmodules: true},
                bugfixes: true,
                loose: true,
              },
            ],
          ],
        }),
      ],
    },
    // 레거시(ES5)용 번들 설정
    {
      format: 'amd',
      entryFileNames: '[name].legacy.js',
      chunkFileNames: '[name]-[hash].legacy.js',
      plugins: [
        getBabelOutputPlugin({
          presets: ['@babel/preset-env'],
        }),
      ],
    },
  ],
};

이외 빌드 도구

Rollup과 webpack은 세밀하게 옵션 설정이 가능하므로, 일반적으로 각 프로젝트가 의존성 모듈에서 최신 자바스크립트 문법을 사용할 수 있도록 구성파일을 수정해야 한다. 또한 Parcel, Snowpack, ViteWMR과 같이, 옵션 설정 보다는 규칙(convention)과 기본값에 충실한 빌드 도구도 있다. 대부분 이러한 도구들은 npm 의존성 모듈에 최신 문법이 포함될 수 있다고 가정하고 프로덕션용 빌드 시 알맞은 문법으로 트랜스파일한다.

결론

EStimator.dev는 대부분의 사용자가 최신(modern-capable) 자바스크립트 코드로 전환할 때 미치는 영향을 쉽게 평가할 수 있도록 해준다. 오늘날 ES2017은 최신 문법에 가장 가까우며, npm, Babel, webpack 및 Rollup 같은 도구를 사용하여 빌드 시스템을 구성하고 최신 문법을 사용하여 패키지를 작성할 수 있다. 본 게시글은 이에 대한 몇 가지 접근 방식을 다루었으며, 이용 사례에 따라 적합한 옵션을 구성하여 사용하도록 한다.



FE Weekly Picks는 FE개발랩의 기술 활동으로,
프론트엔드관련 아티클을 직접 작성하거나 관심있는 영문 아티클을 선정하여 번역하고 공유합니다.


저작권 및 라이선스