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

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

앱 스크린샷

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

측정

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

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

원래 번들 크기 요청

이 애플리케이션에 80KB가 넘게 사용됩니다. 번들의 일부가 사용되지 않는지 확인할 시간입니다.

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

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

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

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

  4. 기본 번들의 코드 사용량과 로드된 코드의 양을 살펴봅니다.

    번들의 코드 적용 범위

번들의 절반 이상 (44KB)이 활용되지도 않습니다. 이는 내부 코드의 상당 부분이 애플리케이션이 이전 브라우저에서 작동하도록 하는 폴리필로 구성되어 있기 때문입니다.

@babel/preset-env 사용

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

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

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

src/index.js의 소스 코드를 살펴보고 이러한 기능이 모두 어떻게 사용되는지 알아보세요.

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

  • 폴리필은 최신 ES2015+ 함수를 에뮬레이션하여 브라우저에서 지원되지 않더라도 API를 사용할 수 있도록 포함되어 있습니다. 다음은 Array.includes 메서드의 폴리필의 예입니다.
  • 플러그인은 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 기능에 필요한 모든 폴리필을 제공합니다. 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는 browserslist와 통합됩니다. 즉, 이 필드에서 사용할 수 있는 호환 가능한 쿼리의 전체 목록은 browserlist 문서에서 확인할 수 있습니다.

"last 2 versions" 값은 모든 브라우저의 최근 두 버전에 맞게 애플리케이션의 코드를 트랜스파일합니다.

디버깅

모든 브라우저의 Babel 타겟과 포함된 모든 변환 및 polyfill을 전체적으로 보려면 .babelrc:debug 필드를 추가합니다.

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

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

타겟팅된 브라우저

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이 계속 포함되어 있기 때문입니다. 속성 값을 usage로 변경하여 코드에서 사용 중인 기능에 필요한 항목만 포함합니다.

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

이렇게 하면 필요한 위치에 polyfill이 자동으로 포함됩니다. 즉, 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"> 사용

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

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>

마지막으로 모듈과 기존 스크립트에 각각 modulenomodule 속성을 추가하고 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를 사용하여 타겟팅된 브라우저에 필요한 필수 폴리필만 제공하는 방법을 알게 되었습니다. 또한 JavaScript 모듈이 애플리케이션의 두 가지 다른 변환된 버전을 제공하여 성능을 더욱 개선할 수 있는 방법도 알아봤습니다. 이 두 가지 기법을 통해 번들 크기를 크게 줄일 수 있는 방법을 제대로 이해했다면 계속 최적화해 보세요.