gzip으로 네트워크 페이로드 축소 및 압축

이 Codelab에서는 다음 애플리케이션의 JavaScript 번들을 축소하고 압축하여 앱의 요청 크기를 줄여 페이지 성능을 개선하는 방법을 살펴봅니다.

앱 스크린샷

측정

최적화를 추가하기 전에 항상 먼저 애플리케이션의 현재 상태를 분석하는 것이 좋습니다.

  • 사이트를 미리 보려면 View App을 누른 다음 Fullscreen 전체 화면을 누릅니다.

'사용하지 않는 코드 삭제' Codelab에서도 다룬 이 앱을 사용하면 가장 좋아하는 새끼 고양이에게 투표할 수 있습니다. 🐈

이제 이 애플리케이션이 얼마나 큰지 살펴보겠습니다.

  1. `Control+Shift+J` (Mac의 경우 `Command+Option+J`)를 눌러 DevTools를 엽니다.
  2. 네트워크 탭을 클릭합니다.
  3. 캐시 사용 중지 체크박스를 선택합니다.
  4. 앱을 새로고침합니다.

네트워크 패널의 원래 번들 크기

이 번들 크기를 줄이기 위해 '사용하지 않는 코드 삭제' Codelab에서 많은 진전이 있었지만 여전히 225KB는 상당히 큽니다.

축소

다음 코드 블록을 살펴보세요.

function soNice() {
  let counter = 0;

  while (counter < 100) {
    console.log('nice');
    counter++;
  }
}

이 함수를 자체 파일로 저장하는 경우 파일 크기는 약 112B (바이트)입니다.

공백을 모두 삭제하면 결과 코드는 다음과 같습니다.

function soNice(){let counter=0;while(counter<100){console.log("nice");counter++;}}

이제 파일 크기는 약 83B가 됩니다. 변수 이름의 길이를 줄이고 일부 표현식을 수정하여 더욱 손상된 경우 최종 코드는 다음과 같을 수 있습니다.

function soNice(){for(let i=0;i<100;)console.log("nice"),i++}

이제 파일 크기가 62B에 도달합니다.

단계가 될 때마다 코드를 읽기가 더 어려워집니다. 하지만 브라우저의 자바스크립트 엔진은 이러한 각 항목을 완전히 동일한 방식으로 해석합니다. 이러한 방식으로 코드를 난독화하면 파일 크기를 줄일 수 있습니다. 112B는 처음에는 별다른 문제가 없었지만 크기는 여전히 50%나 줄었습니다.

이 애플리케이션에서는 webpack 버전 4가 모듈 번들러로 사용됩니다. 구체적인 버전은 package.json에서 확인할 수 있습니다.

"devDependencies": {
  //...
  "webpack": "^4.16.4",
  //...
}

버전 4는 이미 프로덕션 모드 중에 기본적으로 번들을 축소합니다. Terser용 플러그인 TerserWebpackPlugin를 사용합니다. Terser는 JavaScript 코드를 압축하는 데 널리 사용되는 도구입니다.

최소화된 코드가 어떻게 표시되는지 확인하려면 DevTools Network 패널에서 main.bundle.js를 클릭합니다. 이제 Response 탭을 클릭합니다.

응답 최소화

최소화되고 훼손된 최종 형식의 코드가 응답 본문에 표시됩니다. 축소되지 않은 경우 번들의 크기를 확인하려면 webpack.config.js를 열고 mode 구성을 업데이트합니다.

module.exports = {
  mode: 'production',
  mode: 'none',
  //...

애플리케이션을 새로고침하고 DevTools 네트워크 패널을 통해 번들 크기를 다시 확인합니다.

번들 크기 767KB

엄청난 차이가 있죠! 😅

계속하기 전에 여기에서 변경사항을 되돌려야 합니다.

module.exports = {
  mode: 'production',
  mode: 'none',
  //...

애플리케이션에 코드를 축소하는 프로세스를 포함하는 방법은 사용하는 도구에 따라 다릅니다.

  • webpack v4 이상을 사용하는 경우 코드가 기본적으로 프로덕션 모드에서 축소되므로 추가 작업을 할 필요가 없습니다. 👍
  • 이전 버전의 webpack을 사용하는 경우 TerserWebpackPlugin를 설치하고 webpack 빌드 프로세스에 포함합니다. 문서에서 이에 대해 자세히 설명합니다.
  • BabelMinifyWebpackPlugin, ClosureCompilerPlugin과 같은 다른 압축 플러그인도 사용할 수 있습니다. 이 플러그인을 대신 사용할 수 있습니다.
  • 모듈 번들러를 전혀 사용하지 않는 경우에는 Terser를 CLI 도구로 사용하거나 직접 종속 항목으로 포함합니다.

압축

'압축'이라는 용어는 축소 프로세스 중에 코드가 어떻게 축소되는지를 설명하기 위해 느슨하게 사용되기도 하지만, 실제로는 문자 그대로 압축되지 않습니다.

압축은 일반적으로 데이터 압축 알고리즘을 사용하여 수정된 코드를 나타냅니다. 최종적으로 유효한 코드를 제공하는 압축과 달리 압축된 코드는 사용하기 전에 압축을 해제해야 합니다.

모든 HTTP 요청 및 응답에서 브라우저 및 웹 서버는 headers를 추가하여 가져오거나 수신하는 애셋에 대한 추가 정보를 포함할 수 있습니다. DevTools Network 패널의 Headers 탭에서 세 가지 유형이 표시된 것을 확인할 수 있습니다.

  • General은 전체 요청-응답 상호작용과 관련된 일반 헤더를 나타냅니다.
  • 응답 헤더는 서버의 실제 응답과 관련된 헤더 목록을 표시합니다.
  • 요청 헤더는 클라이언트가 요청에 첨부한 헤더 목록을 보여줍니다.

Request Headersaccept-encoding 헤더를 살펴보세요.

인코딩 헤더 허용

accept-encoding는 브라우저에서 지원하는 콘텐츠 인코딩 형식 또는 압축 알고리즘을 지정하는 데 사용됩니다. 많은 텍스트 압축 알고리즘이 있지만 여기서는 HTTP 네트워크 요청의 압축 (및 압축 해제)을 위해 지원되는 세 가지만 있습니다.

  • Gzip (gzip): 서버 및 클라이언트 상호작용에 가장 널리 사용되는 압축 형식입니다. 이 라이브러리는 Deflate 알고리즘을 기반으로 하며 모든 최신 브라우저에서 지원됩니다.
  • Deflate (deflate): 일반적으로 사용되지 않습니다.
  • Brotli (br): 압축률을 더욱 높이는 것을 목표로 하는 최신 압축 알고리즘으로, 페이지 로드 속도가 훨씬 빨라질 수 있습니다. 이 기능은 대부분의 브라우저의 최신 버전에서 지원됩니다.

이 튜토리얼의 샘플 애플리케이션은 이제 Express가 서버 프레임워크로 사용된다는 점을 제외하고 '사용하지 않는 코드 삭제' Codelab에서 완료한 앱과 동일합니다. 다음 섹션에서는 정적 압축과 동적 압축을 모두 살펴봅니다.

동적 압축

동적 압축에는 브라우저에서 요청할 때 애셋을 즉시 압축하는 작업이 포함됩니다.

장점

  • 저장된 압축 버전의 애셋을 만들고 업데이트할 필요가 없습니다.
  • 즉석 압축은 동적으로 생성된 웹페이지에서 특히 잘 작동합니다.

단점

  • 더 나은 압축률을 얻기 위해 높은 수준에서 파일을 압축하는 데 시간이 더 오래 걸립니다. 이로 인해 서버에서 애셋을 전송하기 전에 사용자가 애셋이 압축될 때까지 대기하면서 성능 저하가 발생할 수 있습니다.

Node/Express를 사용한 동적 압축

server.js 파일은 애플리케이션을 호스팅하는 노드 서버 설정을 담당합니다.

const express = require('express');

const app = express();

app.use(express.static('public'));

const listener = app.listen(process.env.PORT, function() {
  console.log('Your app is listening on port ' + listener.address().port);
});

현재 하는 작업은 express를 가져오고 express.static 미들웨어를 사용하여 public/ 디렉터리의 모든 정적 HTML, JS, CSS 파일을 로드하는 것입니다. 이러한 파일은 모든 빌드와 함께 webpack에서 생성됩니다.

요청될 때마다 모든 애셋이 압축되도록 하려면 compression 미들웨어 라이브러리를 사용하면 됩니다. 먼저 package.jsondevDependency로 추가합니다.

"devDependencies": {
  //...
  "compression": "^1.7.3"
},

서버 파일 server.js로 가져옵니다.

const express = require('express');
const compression = require('compression');

express.static가 마운트되기 전에 미들웨어로 추가합니다.

//...

const app = express();

app.use(compression());

app.use(express.static('public'));

//...

이제 앱을 새로고침하고 네트워크 패널에서 번들 크기를 확인합니다.

동적 압축을 사용하는 번들 크기

225KB에서 61.6KB로 줄었습니다. 이제 Response Headers에서 content-encoding 헤더는 서버가 gzip로 인코딩된 이 파일을 전송 중임을 보여줍니다.

콘텐츠 인코딩 헤더

정적 압축

정적 압축의 개념은 미리 애셋을 압축하여 저장하는 것입니다.

장점

  • 높은 압축 수준으로 인한 지연 시간은 더 이상 문제가 되지 않습니다. 이제 파일을 직접 가져올 수 있으므로 파일을 압축하기 위해 즉석에서 아무 작업도 수행할 필요가 없습니다.

단점

  • 애셋은 빌드마다 압축되어야 합니다. 높은 압축 수준을 사용하면 빌드 시간이 크게 증가할 수 있습니다.

Node/Express 및 webpack을 사용한 정적 압축

정적 압축에는 파일을 미리 압축하는 작업이 포함되므로 빌드 단계의 일부로 애셋을 압축하도록 webpack 설정을 수정할 수 있습니다. 이를 위해 CompressionPlugin를 사용할 수 있습니다.

먼저 package.jsondevDependency로 추가합니다.

"devDependencies": {
  //...
  "compression-webpack-plugin": "^1.1.11"
},

다른 webpack 플러그인과 마찬가지로 구성 파일 webpack.config.js:로 가져옵니다.

const path = require("path");

//...

const CompressionPlugin = require("compression-webpack-plugin");

plugins 배열 내에 포함합니다.

module.exports = {
  //...
  plugins: [
    //...
    new CompressionPlugin()
  ]
}

기본적으로 플러그인은 gzip를 사용하여 빌드 파일을 압축합니다. 다른 알고리즘을 사용하거나 특정 파일을 포함/제외하는 옵션을 추가하는 방법은 문서를 참고하세요.

앱이 새로고침되고 다시 빌드되면 이제 기본 번들의 압축된 버전이 생성됩니다. Glitch Console을 열어 노드 서버가 제공하는 최종 public/ 디렉터리 내부의 내용을 살펴봅니다.

  • 도구 버튼을 클릭합니다.
  • 콘솔 버튼을 클릭합니다.
  • 콘솔에서 다음 명령어를 실행하여 public 디렉터리로 변경하고 모든 파일을 확인합니다.
cd public
ls

공개 디렉터리의 최종 출력 파일

이제 번들의 gzip 버전인 main.bundle.js.gz도 여기에 저장됩니다. 또한 CompressionPlugin는 기본적으로 index.html를 압축합니다.

다음으로 해야 할 일은 원래 JS 버전이 요청될 때마다 이러한 gzip 파일을 보내도록 서버에 지시하는 것입니다. 이렇게 하려면 파일이 express.static로 제공되기 전에 server.js에서 새 경로를 정의하면 됩니다.

const express = require('express');
const app = express();

app.get('*.js', (req, res, next) => {
  req.url = req.url + '.gz';
  res.set('Content-Encoding', 'gzip');
  next();
});

app.use(express.static('public'));

//...

app.get는 특정 엔드포인트에 대한 GET 요청에 응답하는 방법을 서버에 알리는 데 사용됩니다. 그런 다음 이 요청을 처리하는 방법을 정의하는 데 콜백 함수를 사용합니다. 경로는 다음과 같이 작동합니다.

  • '*.js'를 첫 번째 인수로 지정하면 JS 파일을 가져오기 위해 실행된 모든 엔드포인트에서 이 작업이 작동합니다.
  • 콜백 내에서 .gz는 요청의 URL에 연결되고 Content-Encoding 응답 헤더는 gzip로 설정됩니다.
  • 마지막으로 next()는 시퀀스가 다음에 올 수 있는 콜백까지 계속 진행되도록 합니다.

앱이 새로고침되면 Network 패널을 한 번 더 확인합니다.

정적 압축을 사용하여 번들 크기 축소

이전과 마찬가지로 번들 크기가 대폭 감소했습니다.

결론

이 Codelab에서는 소스 코드를 축소하고 압축하는 프로세스를 다루었습니다. 이 두 기법은 현재 사용 가능한 많은 도구에서 기본값이 되고 있으므로 도구 모음에서 이미 이러한 기법을 지원하는지 또는 두 프로세스를 모두 직접 적용해야 하는지 여부를 확인하는 것이 중요합니다.