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

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

앱 스크린샷

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

측정

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

  1. 사이트를 미리 보려면 앱 보기를 누른 다음 전체 화면 전체 화면을 누릅니다.
  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 키를 눌러 범위 탭을 표시합니다.

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

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

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

    번들의 코드 적용 범위

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

@babel/preset-env 사용

JavaScript 언어의 구문은 ECMAScript(ECMA-262)라는 표준을 준수합니다.ECMA-262 새로운 버전의 사양은 매년 출시되며 제안 프로세스를 통과한 새로운 기능을 포함합니다. 각각의 주요 브라우저는 항상 이러한 기능을 지원하는 서로 다른 단계에 있습니다.

애플리케이션에서 사용되는 ES2015 기능은 다음과 같습니다.

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

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

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

  • Polyfill은 최신 ES2015+ 함수를 에뮬레이션하기 위해 포함되어 있으므로 브라우저에서 지원하지 않는 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은 대상으로 선택된 브라우저 또는 환경에 필요한 변환 및 polyfill을 식별합니다.

Babel 구성 파일 .babelrc에 포함된 방식을 확인하세요.

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

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

.babelrctargets 속성은 타겟팅 중인 브라우저를 식별합니다. @babel/preset-env는 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를 직접 가져오기 때문입니다.

개별적으로 폴리필 로드

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

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

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

가져온 폴리필 목록

이제 "last 2 versions"에 필요한 polyfill만 포함되었지만 여전히 매우 긴 목록입니다. 모든 최신 기능의 대상 브라우저에 필요한 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";

이제 애플리케이션에 필요한 폴리필만 포함됩니다.

자동으로 포함된 폴리필 목록

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

번들 크기가 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"> 사용

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

JavaScript 모듈모든 주요 브라우저에서 지원되는 비교적 새로운 기능입니다. 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>

Babel이 필요하지 않고 JavaScript 모듈을 지원하는 환경에서는 이미 많은 최신 ECMAScript 기능이 지원됩니다. 즉, 애플리케이션의 두 가지 버전을 브라우저에 전송하도록 Babel 구성을 수정할 수 있습니다.

  • 모듈을 지원하는 최신 브라우저에서 작동하는 버전으로, 대부분 트랜스파일되지 않지만 파일 크기는 더 작은 모듈을 포함합니다.
  • 모든 기존 브라우저에서 작동하는 더 큰 트랜스파일된 스크립트가 포함된 버전

Babel과 함께 ES 모듈 사용

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

먼저 기존 스크립트의 구성을 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 모듈을 지원하지 않는 모든 브라우저를 타겟팅하는 데 필요한 모든 변환 및 polyfill이 포함되어 있습니다.

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

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 모듈을 가져옴

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

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

이전 브라우저에서 30KB 스크립트를 가져왔습니다.

결론

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