최신 JavaScript 종속 항목 및 출력을 사용 설정하여 성능을 개선합니다.
90% 이상의 브라우저에서 최신 JavaScript를 실행할 수 있지만, 기존 JavaScript는 오늘날 웹에서 성능 문제의 큰 원인으로 남아 있습니다.
최신 JavaScript
최신 JavaScript는 특정 ECMAScript 사양 버전으로 작성된 코드가 아니라 모든 최신 브라우저에서 지원하는 구문으로 구분됩니다. Chrome, Edge, Firefox, Safari와 같은 최신 웹브라우저가 브라우저 시장의 90% 이상을 차지하며, 동일한 기본 렌더링 엔진을 사용하는 다른 브라우저가 5%를 차지합니다. 즉, 전 세계 웹 트래픽의 95%는 다음을 비롯하여 지난 10년 동안 가장 널리 사용된 JavaScript 언어 기능을 지원하는 브라우저에서 발생합니다.
- 클래스(ES2015)
- 화살표 함수(ES2015)
- 생성기(ES2015)
- 블록 범위 지정(ES2015)
- 디스트럭처링 (ES2015)
- rest 및 spread 매개변수(ES2015)
- 객체 쇼트핸드(ES2015)
- Async/await (ES2017)
언어 사양의 최신 버전의 기능은 일반적으로 최신 브라우저에서 일관성 없이 지원됩니다. 예를 들어 많은 ES2020 및 ES2021 기능은 브라우저 시장의 70%에서만 지원됩니다. 이는 여전히 대부분의 브라우저이지만 이러한 기능을 직접 사용하는 것이 안전하지는 않습니다. 즉, '최신' JavaScript는 계속 변화하는 대상이지만 ES2017은 일반적으로 사용되는 최신 문법 기능 대부분을 포함하면서 가장 광범위한 브라우저 호환성을 제공합니다. 즉, ES2017은 오늘날 최신 문법에 가장 근접합니다.
레거시 JavaScript
기존 JavaScript는 특히 위의 모든 언어 기능의 사용을 피하는 코드입니다. 대부분의 개발자는 최신 문법을 사용하여 소스 코드를 작성하지만 브라우저 지원을 강화하기 위해 모든 것을 기존 문법으로 컴파일합니다. 기존 문법으로 컴파일하면 브라우저 지원이 증가하지만 그 효과는 생각보다 작습니다. 대부분의 경우 지원이 약 95%에서 98%로 증가하면서 상당한 비용이 발생합니다.
기존 JavaScript는 일반적으로 동등한 최신 코드보다 약 20% 더 크고 느립니다. 도구의 결함과 잘못된 구성으로 인해 이러한 격차가 더 벌어지는 경우가 많습니다.
설치된 라이브러리는 일반적인 프로덕션 JavaScript 코드의 최대 90%를 차지합니다. 라이브러리 코드는 최신 코드를 게시하여 피할 수 있는 polyfill 및 도우미 중복으로 인해 훨씬 더 높은 레거시 JavaScript 오버헤드를 발생시킵니다.
npm의 최신 JavaScript
최근 Node.js는 패키지의 진입점을 정의하는 "exports"
필드를 표준화했습니다.
{
"exports": "./index.js"
}
"exports"
필드에서 참조하는 모듈은 ES2019를 지원하는 최소 12.8 버전의 Node를 의미합니다. 즉, "exports"
필드를 사용하여 참조된 모든 모듈은 최신 JavaScript로 작성할 수 있습니다. 패키지 소비자는 "exports"
필드가 있는 모듈에 최신 코드가 포함되어 있고 필요한 경우 트랜스파일이라고 가정해야 합니다.
현대만
최신 코드로 패키지를 게시하고 소비자가 종속 항목으로 사용할 때 트랜스파일링을 처리하도록 하려면 "exports"
필드만 사용하세요.
{
"name": "foo",
"exports": "./modern.js"
}
기존 대체가 있는 최신 버전
최신 코드를 사용하여 패키지를 게시하면서 기존 브라우저를 위한 ES5 + CommonJS 대체를 포함하려면 "main"
와 함께 "exports"
필드를 사용하세요.
{
"name": "foo",
"exports": "./modern.js",
"main": "./legacy.cjs"
}
기존 대체 및 ESM 번들러 최적화와 함께 최신 버전
"module"
필드는 대체 CommonJS 진입점을 정의하는 것 외에도 JavaScript 모듈 문법 (import
및 export
)을 사용하는 유사한 레거시 대체 번들을 가리키는 데 사용할 수 있습니다.
{
"name": "foo",
"exports": "./modern.js",
"main": "./legacy.cjs",
"module": "./module.js"
}
webpack 및 Rollup과 같은 많은 번들러는 이 필드를 사용하여 모듈 기능을 활용하고 트리 쉐이킹을 사용 설정합니다.
여전히 import
/export
문법 외에는 최신 코드가 포함되지 않은 기존 번들입니다. 따라서 이 접근 방식을 사용하여 번들에 최적화된 기존 대체 항목과 함께 최신 코드를 제공합니다.
애플리케이션의 최신 JavaScript
서드 파티 종속 항목은 웹 애플리케이션의 일반적인 프로덕션 JavaScript 코드의 대부분을 구성합니다. npm 종속 항목은 이전에 기존 ES5 문법으로 게시되었지만, 이는 더 이상 안전한 가정이 아니며 종속 항목 업데이트로 인해 애플리케이션의 브라우저 지원이 중단될 위험이 있습니다.
점점 더 많은 npm 패키지가 최신 JavaScript로 전환됨에 따라 빌드 도구에서 이를 처리하도록 설정하는 것이 중요합니다. 종속 항목으로 사용하는 일부 npm 패키지가 이미 최신 언어 기능을 사용하고 있을 가능성이 큽니다. 이전 브라우저에서 애플리케이션을 중단시키지 않고 npm의 최신 코드를 사용할 수 있는 여러 옵션이 있지만 일반적인 개념은 빌드 시스템이 종속 항목을 소스 코드와 동일한 문법 타겟으로 트랜스파일하도록 하는 것입니다.
webpack
이제 webpack 5부터 번들 및 모듈의 코드를 생성할 때 webpack에서 사용할 문법을 구성할 수 있습니다. 이렇게 해도 코드나 종속 항목은 트랜스파일되지 않으며 webpack에서 생성된 '글루' 코드에만 영향을 미칩니다. 브라우저 지원 대상을 지정하려면 프로젝트에 browserslist 구성을 추가하거나 webpack 구성에서 직접 이를 추가합니다.
module.exports = {
target: ['web', 'es2017'],
};
최신 ES 모듈 환경을 타겟팅할 때 불필요한 래퍼 함수를 생략하는 최적화된 번들을 생성하도록 webpack을 구성할 수도 있습니다. 이렇게 하면 <script type="module">
를 사용하여 코드 분할 번들을 로드하도록 webpack도 구성됩니다.
module.exports = {
target: ['web', 'es2017'],
output: {
module: true,
},
experiments: {
outputModule: true,
},
};
최신 JavaScript를 컴파일하고 제공하면서 기존 브라우저를 계속 지원할 수 있는 여러 webpack 플러그인(예: Optimize Plugin, BabelEsmPlugin)이 있습니다.
최적화 도구 플러그인
최적화 플러그인은 각 개별 소스 파일 대신 최종 번들 코드를 최신 JavaScript에서 기존 JavaScript로 변환하는 webpack 플러그인입니다. webpack 구성에서 모든 것이 최신 JavaScript라고 가정하고 여러 출력 또는 문법에 대한 특별한 브랜치가 없도록 허용하는 자체 구성입니다.
최적화 플러그인은 개별 모듈이 아닌 번들에서 작동하므로 애플리케이션의 코드와 종속 항목을 동등하게 처리합니다. 이렇게 하면 코드가 번들로 묶여 올바른 구문으로 변환 컴파일되므로 npm의 최신 JavaScript 종속 항목을 안전하게 사용할 수 있습니다. 또한 두 번의 컴파일 단계가 수반되는 기존 솔루션보다 빠를 수 있으며, 최신 브라우저와 기존 브라우저용 별도의 번들을 계속 생성할 수 있습니다. 두 세트의 번들은 module/nomodule 패턴을 사용하여 로드되도록 설계되었습니다.
// webpack.config.js
const OptimizePlugin = require('optimize-plugin');
module.exports = {
// ...
plugins: [new OptimizePlugin()],
};
Optimize Plugin
는 일반적으로 최신 코드와 기존 코드를 별도로 번들로 묶는 맞춤 webpack 구성보다 빠르고 효율적입니다. 또한 Babel 실행을 자동으로 처리하고 최신 및 기존 출력에 대해 별도의 최적화 설정을 사용하여 Terser를 사용하여 번들을 축소합니다. 마지막으로, 생성된 레거시 번들에 필요한 polyfill이 전용 스크립트로 추출되므로 최신 브라우저에서 중복되거나 불필요하게 로드되지 않습니다.
BabelEsmPlugin
BabelEsmPlugin은 @babel/preset-env와 함께 작동하여 기존 번들의 최신 버전을 생성하여 덜 변환된 코드를 최신 브라우저에 전달하는 webpack 플러그인입니다. module/nomodule을 위한 가장 인기 있는 상용 솔루션으로, Next.js 및 Preact CLI에서 사용됩니다.
// webpack.config.js
const BabelEsmPlugin = require('babel-esm-plugin');
module.exports = {
//...
module: {
rules: [
// your existing babel-loader configuration:
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
],
},
plugins: [new BabelEsmPlugin()],
};
BabelEsmPlugin
는 애플리케이션의 두 가지 별개의 빌드를 실행하므로 다양한 webpack 구성을 지원합니다. 두 번 컴파일하면 대규모 애플리케이션의 경우 약간의 시간이 더 걸릴 수 있지만 이 기법을 사용하면 BabelEsmPlugin
를 기존 webpack 구성에 원활하게 통합할 수 있으므로 가장 편리한 옵션 중 하나가 됩니다.
node_modules를 트랜스파일하도록 babel-loader 구성
위의 두 플러그인 중 하나를 사용하지 않고 babel-loader
를 사용하는 경우 최신 JavaScript npm 모듈을 사용하려면 중요한 단계가 필요합니다. 두 개의 별도 babel-loader
구성을 정의하면 node_modules
에 있는 최신 언어 기능을 ES2017로 자동 컴파일하면서 프로젝트 구성에 정의된 Babel 플러그인 및 사전 설정으로 자체 퍼스트 파티 코드를 계속 트랜스파일할 수 있습니다. 이렇게 하면 module/nomodule 설정에 최신 및 기존 번들이 생성되지는 않지만 이전 브라우저를 중단하지 않고 최신 JavaScript가 포함된 npm 패키지를 설치하고 사용할 수 있습니다.
webpack-plugin-modern-npm은 이 기법을 사용하여 package.json
에 "exports"
필드가 있는 npm 종속 항목을 컴파일합니다. 최신 구문이 포함될 수 있기 때문입니다.
// webpack.config.js
const ModernNpmPlugin = require('webpack-plugin-modern-npm');
module.exports = {
plugins: [
// auto-transpile modern stuff found in node_modules
new ModernNpmPlugin(),
],
};
또는 문제가 해결될 때 모듈의 package.json
에서 "exports"
필드를 확인하여 웹훅 구성에서 이 기법을 수동으로 구현할 수 있습니다. 간결성을 위해 캐싱을 생략하면 맞춤 구현은 다음과 같습니다.
// webpack.config.js
module.exports = {
module: {
rules: [
// Transpile for your own first-party code:
{
test: /\.js$/i,
loader: 'babel-loader',
exclude: /node_modules/,
},
// Transpile modern dependencies:
{
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'],
},
},
},
],
},
};
이 접근 방식을 사용할 때는 최신 문법이 최소화 도구에서 지원되는지 확인해야 합니다. Terser와 uglify-es에는 압축 및 형식 지정 중에 ES2017 문법을 보존하고 경우에 따라 생성하기 위해 {ecma: 2017}
를 지정하는 옵션이 있습니다.
롤업
롤업에는 단일 빌드의 일부로 여러 번들의 세트를 생성하는 기능이 기본적으로 내장되어 있으며 기본적으로 최신 코드를 생성합니다. 따라서 롤업을 이미 사용 중인 공식 플러그인을 사용하여 최신 및 기존 번들을 생성하도록 구성할 수 있습니다.
@rollup/plugin-babel
Rollup을 사용하는 경우 getBabelOutputPlugin()
메서드(Rollup의 공식 Babel 플러그인에서 제공)는 개별 소스 모듈이 아닌 생성된 번들의 코드를 변환합니다.
롤업에는 단일 빌드의 일부로 여러 번들 세트를 생성하는 기능이 내장되어 있으며, 각 번들 세트에는 자체 플러그인이 있습니다. 이를 사용하여 각각 다른 Babel 출력 플러그인 구성을 통해 최신 버전과 기존 버전의 서로 다른 번들을 생성할 수 있습니다.
// rollup.config.js
import {getBabelOutputPlugin} from '@rollup/plugin-babel';
export default {
input: 'src/index.js',
output: [
// modern bundles:
{
format: 'es',
plugins: [
getBabelOutputPlugin({
presets: [
[
'@babel/preset-env',
{
targets: {esmodules: true},
bugfixes: true,
loose: true,
},
],
],
}),
],
},
// legacy (ES5) bundles:
{
format: 'amd',
entryFileNames: '[name].legacy.js',
chunkFileNames: '[name]-[hash].legacy.js',
plugins: [
getBabelOutputPlugin({
presets: ['@babel/preset-env'],
}),
],
},
],
};
추가 빌드 도구
롤업과 webpack은 구성이 매우 유연합니다. 즉, 일반적으로 각 프로젝트는 종속 항목에서 최신 JavaScript 문법을 사용 설정하도록 구성을 업데이트해야 합니다. Parcel, Snowpack, Vite, WMR과 같이 구성보다 관례와 기본값을 선호하는 상위 수준의 빌드 도구도 있습니다. 이러한 도구의 대부분은 npm 종속 항목에 최신 문법이 포함될 수 있다고 가정하고 프로덕션용으로 빌드할 때 적절한 문법 수준으로 트랜스파일합니다.
webpack 및 Rollup 전용 플러그인 외에도 기존 대체가 포함된 최신 JavaScript 번들은 devolution을 사용하여 모든 프로젝트에 추가할 수 있습니다. Devolution은 빌드 시스템의 출력을 변환하여 기존 JavaScript 변형을 생성하는 독립형 도구로, 번들링 및 변환이 최신 출력 타겟을 가정할 수 있도록 합니다.