페이지 로드 속도를 높이기 위해 최신 브라우저에 최신 코드 제공하기

이 Codelab에서는 사용자가 임의의 고양이를 평가할 수 있는 간단한 애플리케이션의 성능을 개선합니다. 트랜스파일되는 코드 양을 최소화하여 JavaScript 번들을 최적화하는 방법을 알아보세요.

앱 스크린샷

샘플 앱에서는 각 고양이가 얼마나 좋아하는지 나타내는 단어나 그림 이모티콘을 선택할 수 있습니다. 버튼을 클릭하면 앱이 현재 고양이 이미지 아래에 버튼 값을 표시합니다.

측정

최적화를 추가하기 전에 항상 웹사이트를 검사하는 것이 좋습니다.

  1. 사이트를 미리 보려면 View App을 누른 다음 Fullscreen 전체 화면을 누릅니다.
  2. `Control+Shift+J` (Mac의 경우 `Command+Option+J`)를 눌러 DevTools를 엽니다.
  3. 네트워크 탭을 클릭합니다.
  4. 캐시 사용 중지 체크박스를 선택합니다.
  5. 앱을 새로고침합니다.

원래 번들 크기 요청

이 애플리케이션에서는 80KB 이상을 사용했습니다. 번들의 일부가 사용되지 않는지 확인합니다.

  1. Control+Shift+P (Mac의 경우 Command+Shift+P)를 눌러 Command 메뉴를 엽니다. 명령어 메뉴

  2. Show Coverage를 입력하고 Enter를 눌러 Coverage 탭을 표시합니다.

  3. 범위 탭에서 새로고침을 클릭하여 적용 범위를 캡처하는 동안 애플리케이션을 새로고침합니다.

    코드 적용 범위로 앱 새로고침

  4. 사용된 코드 수와 기본 번들에 로드된 코드를 살펴보세요.

    번들의 코드 적용 범위

번들의 절반 이상 (44KB)은 사용되지 않습니다. 이는 애플리케이션이 이전 브라우저에서도 작동할 수 있도록 폴리필로 구성된 많은 코드가 있기 때문입니다.

@babel/preset-env 사용

자바스크립트 언어의 구문은 ECMAScript 또는 ECMA-262로 알려진 표준을 준수합니다. 사양의 최신 버전은 매년 출시되며 제안서 절차를 통과한 새 기능을 포함합니다. 주요 브라우저마다 이러한 기능을 지원하는 단계가 항상 다릅니다.

다음 ES2015 기능이 애플리케이션에 사용됩니다.

다음 ES2017 기능도 사용됩니다.

자유롭게 src/index.js의 소스 코드를 살펴보고 이 모든 것이 어떻게 사용되는지 확인해 보세요.

이 기능은 모두 최신 버전의 Chrome에서 지원되지만 이 기능을 지원하지 않는 다른 브라우저는 어떻게 해야 하나요? 애플리케이션에 포함된 Babel은 이전 브라우저와 환경에서 이해할 수 있는 최신 문법을 코드에 포함하는 코드를 컴파일하는 데 가장 많이 사용되는 라이브러리입니다. 이는 두 가지 방법으로 이루어집니다.

  • 최신 ES2015+ 함수를 에뮬레이션하기 위해 Polyfill이 포함되었으므로, 브라우저에서 지원하지 않는 경우에도 API를 사용할 수 있습니다. 다음은 Array.includes 메서드의 polyfill 예입니다.
  • 플러그인은 ES2015 코드 (또는 이후 버전)를 이전 ES5 구문으로 변환하는 데 사용됩니다. 이는 구문 관련 변경사항 (예: 화살표 함수)이므로 폴리필을 사용하여 에뮬레이션할 수 없습니다.

package.json를 보고 포함된 Babel 라이브러리를 확인합니다.

"dependencies": {
  "@babel/polyfill": "^7.0.0"
},
"devDependencies": {
  //...
  "babel-loader": "^8.0.2",
  "@babel/core": "^7.1.0",
  "@babel/preset-env": "^7.1.0",
  //...
}
  • @babel/core는 핵심 Babel 컴파일러입니다. 이렇게 하면 모든 Babel 구성이 프로젝트 루트에 있는 .babelrc에서 정의됩니다.
  • babel-loader는 webpack 빌드 프로세스에 Babel을 포함합니다.

이제 webpack.config.js를 보고 babel-loader이 규칙으로 포함되는 방식을 확인합니다.

module: {
  rules: [
    //...
    {
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader"
    }
  ]
},
  • @babel/polyfill는 최신 ECMAScript 기능을 지원하지 않는 환경에서도 작동할 수 있도록 새로운 ECMAScript 기능에 필요한 모든 polyfill을 제공합니다. 이미 src/index.js.의 맨 위에서 가져왔습니다.
import "./style.css";
import "@babel/polyfill";
  • @babel/preset-env는 타겟으로 선택된 브라우저 또는 환경에 필요한 변환 및 폴리필을 식별합니다.

Babel 구성 파일 .babelrc를 살펴보고 포함되는 방법을 확인합니다.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions"
      }
    ]
  ]
}

Babel 및 webpack 설정입니다. webpack과 다른 모듈 번들러를 사용하는 경우 애플리케이션에 Babel을 포함하는 방법을 알아보세요.

.babelrctargets 속성은 타겟팅 중인 브라우저를 식별합니다. @babel/preset-env는 browserlist와 통합됩니다. 즉, browserlist 문서에서 이 필드에서 사용할 수 있는 호환 가능한 쿼리의 전체 목록을 찾을 수 있습니다.

"last 2 versions" 값은 모든 브라우저의 마지막 두 버전에 대해 애플리케이션에서 코드를 트랜스파일링합니다.

디버깅

브라우저의 모든 Babel 타겟과 포함된 모든 변환 및 polyfill을 완전히 살펴보려면 debug 필드를 .babelrc:에 추가합니다.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
      }
    ]
  ]
}
  • 도구를 클릭합니다.
  • 로그를 클릭합니다.

애플리케이션을 새로고침하고 편집기 하단의 Glitch 상태 로그를 확인합니다.

타겟팅된 브라우저

Babel은 코드가 컴파일된 모든 대상 환경을 비롯하여 컴파일 프로세스에 대한 여러 세부정보를 콘솔에 로깅합니다.

타겟팅된 브라우저

Internet Explorer와 같이 지원 중단된 브라우저가 이 목록에 어떻게 포함되어 있는지 확인하세요. 지원되지 않는 브라우저에는 최신 기능이 추가되지 않고 Babel이 특정 구문을 계속 트랜스파일하기 때문에 문제가 됩니다. 따라서 사용자가 이 브라우저를 사용하여 사이트에 액세스하지 않는 경우 번들의 크기가 불필요하게 증가합니다.

Babel은 사용된 변환 플러그인의 목록도 기록합니다.

사용된 플러그인 목록

목록이 꽤 많습니다. 이는 Babel이 모든 타겟팅된 브라우저에서 ES2015+ 구문을 이전 구문으로 변환하는 데 필요한 모든 플러그인입니다.

그러나 Babel은 사용된 특정 polyfill이 표시되지 않습니다.

추가된 폴리필 없음

전체 @babel/polyfill를 직접 가져오기 때문입니다.

개별적으로 polyfill 로드

기본적으로 Babel은 @babel/polyfill을 파일로 가져오면 완전한 ES2015+ 환경에 필요한 모든 polyfill을 포함합니다. 대상 브라우저에 필요한 특정 polyfill을 가져오려면 구성에 useBuiltIns: 'entry'을 추가하세요.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
        "useBuiltIns": "entry"
      }
    ]
  ]
}

애플리케이션을 새로고침합니다. 이제 포함된 모든 특정 polyfill을 확인할 수 있습니다.

가져온 polyfill 목록

이제 "last 2 versions"에 필요한 폴리필만 포함되었지만 여전히 매우 긴 목록입니다. 이는 모든 최신 기능의 대상 브라우저에 필요한 polyfill이 여전히 포함되기 때문입니다. 코드에서 사용 중인 기능에 필요한 항목만 포함하려면 속성 값을 usage로 변경합니다.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true,
        "useBuiltIns": "entry"
        "useBuiltIns": "usage"
      }
    ]
  ]
}

이렇게 하면 필요한 곳에 폴리필이 자동으로 포함됩니다. 즉, src/index.js.에서 @babel/polyfill 가져오기를 삭제할 수 있습니다.

import "./style.css";
import "@babel/polyfill";

이제 애플리케이션에 필요한 필수 폴리필만 포함되었습니다.

polyfill 목록이 자동으로 포함됨

애플리케이션 번들 크기가 크게 줄어듭니다.

번들 크기가 30.1KB로 감소

지원되는 브라우저 목록의 범위 좁히기

포함된 브라우저 타겟의 수는 여전히 많으며 Internet Explorer와 같이 지원 중단된 브라우저를 사용하는 사용자는 많지 않습니다. 구성을 다음과 같이 업데이트합니다.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "targets": [">0.25%", "not ie 11"],
        "debug": true,
        "useBuiltIns": "usage",
      }
    ]
  ]
}

가져온 번들의 세부정보를 살펴봅니다.

번들 크기 30.0KB

애플리케이션이 매우 작기 때문에 이러한 변경사항에도 큰 차이가 없습니다. 하지만 사용자가 사용하지 않는다고 확신하는 특정 브라우저를 제외하는 것과 함께 브라우저 시장점유율 (예: ">0.25%")을 사용하는 것이 좋습니다. 자세한 내용은 제임스 카일의 유해한 것으로 간주되는 '최근 2개 버전' 문서를 참고하세요.

<script type="module"> 사용

아직 개선의 여지가 있습니다. 사용되지 않는 여러 폴리필이 삭제되었지만 일부 브라우저에는 필요하지 않은 여러 폴리필이 제공됩니다. 모듈을 사용하면 불필요한 폴리필을 사용하지 않고도 최신 구문을 직접 작성하고 브라우저에 제공할 수 있습니다.

자바스크립트 모듈모든 주요 브라우저에서 지원되는 비교적 새로운 기능입니다. type="module" 속성을 사용하여 모듈을 만들어 다른 모듈에서 가져오고 내보내는 스크립트를 정의할 수 있습니다. 예를 들면 다음과 같습니다.

// math.mjs
export const add = (x, y) => x + y;

<!-- index.html -->
<script type="module">
  import { add } from './math.mjs';

  add(5, 2); // 7
</script>

많은 최신 ECMAScript 기능이 자바스크립트 모듈을 지원하는 환경에서 이미 지원되고 있습니다 (Babel이 필요 없음). 즉, 두 가지 버전의 애플리케이션을 브라우저에 보내도록 Babel 구성을 수정할 수 있습니다.

  • 모듈을 지원하는 최신 브라우저에서 작동하고 대부분 변환 컴파일되지 않지만 파일 크기가 더 작은 모듈을 포함하는 버전
  • 기존 브라우저에서 작동하는 더 크고 트랜스파일된 스크립트가 포함된 버전

Babel과 함께 ES 모듈 사용

애플리케이션의 두 버전에 별도의 @babel/preset-env 설정을 적용하려면 .babelrc 파일을 삭제합니다. 각 애플리케이션 버전에 대해 두 가지 컴파일 형식을 지정하여 Webpack 구성에 Babel 설정을 추가할 수 있습니다.

먼저 기존 스크립트의 구성을 webpack.config.js에 추가합니다.

const legacyConfig = {
  entry,
  output: {
    path: path.resolve(__dirname, "public"),
    filename: "[name].bundle.js"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
          presets: [
            ["@babel/preset-env", {
              useBuiltIns: "usage",
              targets: {
                esmodules: false
              }
            }]
          ]
        }
      },
      cssRule
    ]
  },
  plugins
}

"@babel/preset-env"targets 값을 사용하는 대신 값이 falseesmodules가 대신 사용됩니다. 즉, Babel에는 아직 ES 모듈을 지원하지 않는 모든 브라우저를 타겟팅하는 데 필요한 모든 변환 및 폴리필이 포함되어 있습니다.

entry, cssRule, corePlugins 객체를 webpack.config.js 파일의 시작 부분에 추가합니다. 이는 모두 브라우저에 제공되는 모듈과 기존 스크립트 간에 공유됩니다.

const entry = {
  main: "./src"
};

const cssRule = {
  test: /\.css$/,
  use: ExtractTextPlugin.extract({
    fallback: "style-loader",
    use: "css-loader"
  })
};

const plugins = [
  new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
  new HtmlWebpackPlugin({template: "./src/index.html"})
];

이제 마찬가지로 legacyConfig가 정의된 아래 모듈 스크립트의 구성 객체를 만듭니다.

const moduleConfig = {
  entry,
  output: {
    path: path.resolve(__dirname, "public"),
    filename: "[name].mjs"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
          presets: [
            ["@babel/preset-env", {
              useBuiltIns: "usage",
              targets: {
                esmodules: true
              }
            }]
          ]
        }
      },
      cssRule
    ]
  },
  plugins
}

여기서 주요 차이점은 출력 파일 이름에 .mjs 파일 확장자가 사용된다는 점입니다. 여기서 esmodules 값은 true로 설정됩니다. 즉, 이 모듈에 출력되는 코드는 더 작고 컴파일된 더 작은 스크립트이며 이 예에서는 사용된 모든 기능이 모듈을 지원하는 브라우저에서 이미 지원되고 있으므로 이 예에서는 변환을 거치지 않습니다.

파일 맨 끝에서 두 구성을 단일 배열로 내보냅니다.

module.exports = [
  legacyConfig, moduleConfig
];

이제 이를 지원하는 브라우저를 위한 더 작은 모듈과 이전 브라우저의 더 큰 트랜스파일된 스크립트가 모두 빌드됩니다.

모듈을 지원하는 브라우저는 nomodule 속성이 있는 스크립트를 무시합니다. 반대로 모듈을 지원하지 않는 브라우저는 type="module"가 있는 스크립트 요소를 무시합니다. 즉, 컴파일된 대체와 모듈을 포함할 수 있습니다. 애플리케이션의 두 버전은 다음과 같이 index.html에 있는 것이 좋습니다.

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js"></script>

모듈을 지원하는 브라우저는 main.mjs를 가져오고 실행하고 main.bundle.js.를 무시하며 모듈을 지원하지 않는 브라우저는 그 반대로 이루어집니다.

일반 스크립트와 달리 모듈 스크립트는 기본적으로 항상 지연된다는 점에 유의해야 합니다. 동등한 nomodule 스크립트도 지연되고 파싱된 후에만 실행되도록 하려면 defer 속성을 추가해야 합니다.

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js" defer></script>

마지막으로 여기서 해야 하는 작업은 module 속성과 nomodule 속성을 각각 모듈과 기존 스크립트에 추가하는 것입니다. webpack.config.js 맨 위에서 ScriptExtHtmlWebpackPlugin을 가져옵니다.

const path = require("path");

const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");

이제 이 플러그인을 포함하도록 구성에서 plugins 배열을 업데이트합니다.

const plugins = [
  new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
  new HtmlWebpackPlugin({template: "./src/index.html"}),
  new ScriptExtHtmlWebpackPlugin({
    module: /\.mjs$/,
    custom: [
      {
        test: /\.js$/,
        attribute: 'nomodule',
        value: ''
    },
    ]
  })
];

이러한 플러그인 설정은 모든 .mjs 스크립트 요소의 type="module" 속성을 추가하고 모든 .js 스크립트 모듈의 nomodule 속성을 추가합니다.

HTML 문서에서 모듈 제공

마지막으로 해야 할 작업은 기존 및 최신 스크립트 요소를 모두 HTML 파일로 출력하는 것입니다. 안타깝게도 최종 HTML 파일인 HTMLWebpackPlugin를 만드는 플러그인은 모듈 스크립트와 nomodule 스크립트의 출력을 현재 지원하지 않습니다. 이 문제를 해결하기 위해 만든 해결 방법과 별도의 플러그인(예: BabelMultiTargetPluginHTMLWebpackMultiBuildPlugin)이 있지만 이 가이드에서는 모듈 스크립트 요소를 수동으로 추가하는 더 간단한 방법을 사용합니다.

파일 끝의 src/index.js에 다음을 추가합니다.

    ...
    </form>
    <script type="module" src="main.mjs"></script>
  </body>
</html>

이제 최신 버전의 Chrome과 같이 모듈을 지원하는 브라우저에서 애플리케이션을 로드합니다.

최신 브라우저용으로 네트워크를 통해 5.2KB 모듈을 가져옴

모듈만 가져오며, 대부분이 변환 컴파일되지 않아 번들 크기가 훨씬 작습니다. 다른 스크립트 요소는 브라우저에서 완전히 무시됩니다.

이전 브라우저에서 애플리케이션을 로드하면 필요한 polyfill과 변환이 모두 포함된 더 큰 트랜스파일 스크립트만 가져옵니다. 다음은 이전 버전의 Chrome (버전 38)에서 이루어진 모든 요청의 스크린샷입니다.

이전 브라우저용으로 30KB 스크립트를 가져옴

결론

이제 @babel/preset-env를 사용하여 타겟팅된 브라우저에 필요한 polyfill만 제공하는 방법을 이해했습니다. 또한 트랜스파일된 버전의 애플리케이션 두 개를 제공하여 JavaScript 모듈이 성능을 더욱 개선하는 방법도 배웠습니다. 이 두 가지 기법이 어떻게 번들 크기를 크게 줄일 수 있는지 충분히 이해했으니 이제 계속 최적화하겠습니다.