본문 바로가기

FrontEnd+

[성능최적화] 요청 사이즈 다이어트

 

<여기서만나> 프로젝트의 요청 크기 최적화 방법을 정리한 글입니다.

 

1. 이미지 요청 크기 줄이기

평균적으로 웹 페이지 용량에서 이미지가 차지하는 비율은 60% 이상인 점을 고려하면, 이미지 요청 사이즈를 개선하는 것도 요청 사이즈를 줄이는데 효과적인 방법일 수 있다.

<여기서만나> 프로젝트에서는 총 28개의 이미지 파일을 사용하고 있었다. 9월 이전까지는 이 이미지 파일들에 대한 최적화가 하나도 되어있지 않았었고, 이 때문에 사용성에도 문제가 있었다. 특히 로그인 페이지의 일러스트처럼 용량이 큰 이미지를 불러올 때면, 이미지를 받아오는데 너무 오래걸려서 깜-빡하고 이미지가 나중에 뜨는 현상이 나타났다.

 

최초에 이미지 파일을 준비할 때, '혹시나' 화질이 안좋아 보일까봐 원본 이미지에 2배 크기로 저장했는데, 이렇게 저장할 때 원본 이미지의 사이즈를 렌더될 사이즈에 맞추지 않았다는 점이 문제가 되었다.

예를 들어, 웹 화면에서 width: 50px 로 그려지는 이미지가 있다고 가정해보자. (이미지의 가로세로 비율은 1:1로 가정하자.) 여기에 넣을 원본이미지의 가로폭이 1,000px이라면 이미지를 오히려 축소해서 저장해야 할 것이다.

이 때 실제로 웹 화면에 렌더될 사이즈(50px)를 고려하지 않은 채, 괜한 걱정으로 2배 키워 저장한다면, 50px이면 충분한데 사용자는 쓸데없이 가로폭 2,000px짜리 too much 고화질, 고용량의 이미지를 불러오게 된다. 이 문제는 모바일 화면일 때 더욱 심각해진다. 화면의 크기에 상관없이 동일한 이미지를 받아온다면, 모바일 화면에서는 20px면 충분할 것을 2,000px짜리를 받아오게 되버린다.

예시가 다소 과장되게 들리지만 사실 우리 프로젝트의 이미지 파일들은 실제로 이런 상황이었다. 개선의 여지가 굉장히 많았다. 

 

 

📊 개선효과 요약

화면의 크기에 상관없이 항상 522kB 상당의 이미지를 받아왔다. 개선 후에는 디바이스 크기, 해상도에 따라 최적의 옵션을 결정해서 이미지를 받아와 웹에서는 76.7kB(81% 감소), 모바일에서는 43.5kB(92% 감소)의 이미지를 받아온다.

웹 화면

웹 화면 이미지 최적화 전/후

모바일 화면

모바일 화면 이미지 최적화 전/후

 

 

⚒️ 이미지 resize

우선, 이미지 크기를 실제 렌더 크기에 맞게 변경한다. 각 이미지가 실제로 얼마만큼 사이즈로 렌더되는지 하나하나 확인해주는 작업이 필요하다. 

<여기서만나>에서 사용한 아바타 이미지를 예로 들어보자. 왼쪽은 개선 전 저장했던 이미지의 크기이고, 오른쪽은 렌더 크기가 52px 인 것을 확인하고 조정한 모습이다. 얼마나 불필요하게 큰 이미지를 저장하고 있었는지 알 수 있는 대목이다.

피그마 - 이미지 크기 조정

 

참고로, 아래 왼쪽의 그림과 같이 figma 왼쪽 탭에서 이미지의 이름을 미리 수정해두면 해당 이름으로 파일이 저장되어서, 저장할 때마다 새로 이름 변경할 필요가 없어서 편리하다.

또한, 드래그로 저장할 이미지들을 선택해서 한번에 저장할 수도 있다.

피그마 - 편리한 기능들

한편, 실제 렌더 크기에 맞춘 후라면, 레티나 디스플레이 대응을 위해 'name@2x', 'name@3x'와 같은 파일 이름으로 2배, 3배 이미지를 함께 저장해도 좋다. 이렇게 이름이 같고 배율만 다른 이미지는 mac의 파인더에서 [우클릭]-[이름 변경]-[텍스트 추가] 에서 suffix를 일괄적으로 붙여줄 수 있다.

 

피그마 - 배율 선택 저장 / 맥OS - 파인더 파일명 일괄 변경 기능

⚒️ webp 압축

webp는 2010년에 등장한 이미지 포맷으로 구글이 개발했다. png/jpg 대비 30% 정도 크기가 줄어든다.

npm i -D image-minimizer-webpack-plugin min-webp
// webpack.config.js

const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');

const config = {
  ...
  plugins: [
    new ImageMinimizerPlugin({
      test: /drawing.*\.png$/i,
      deleteOriginalAssets: false,
      filename: '/images/[name].webp',
      minimizerOptions: {
        plugins: ['imagemin-webp'],
      },
  }),
  ]
  ...
}

이때, webp를 지원하지 않는 경우에 대응하기 위해 deleteOriginalAssets 옵션을 false로 꺼준다.

 

webp 지원범위 (출처: https://caniuse.com)

 

⚒️ srcset & media 설정

디바이스 환경에 따라 브라우저가 최적의 이미지를 받아오도록 할 수 있다. 

- type: webp 와 png 중 어떤 형식으로 가져올지
- srcset: 1x, 2x, 3x 중 어떤 이미지를 가져올지
- media: 모바일/태블릿 사이즈에 맞춰 가져올지 아니면 데스크탑 사이즈에 맞춰 가져올지

<picture>
  <source
    type="image/webp"
    srcSet=`${minImageTablet.x1} 1x, ${minImageTablet.x2} 2x, ${minImageTablet.x3} 3x`
    media="(max-width: 833px)"
  />
  <source
    type="image/webp"
    srcSet=`${minImage.x1} 1x, ${minImage.x2} 2x, ${minImage.x3} 3x`
  />
  <source
    type="image/png"
    srcSet=`${imageTablet.x1} 1x, ${imageTablet.x2} 2x, ${imageTablet.x3} 3x`
    media="(max-width: 833px)"
  />
  <source
    type="image/png"
    srcSet=`${image.x1} 1x, ${image.x2} 2x, ${image.x3} 3x`
  />
  <img src={image.x1} alt={alt} />
</picture>

이 때, source 태그의 순서에 주의해야 한다. 'image/webp'를 'image/png'보다 위에 작성해야 한다. (media도 마찬가지) 순서를 바꾸면 먼저 만족한 조건을 가져가서 webp를 뒤에쓰면 무조건 png를 받아오거나, max-width를 뒤에 쓰면 무조건 큰 파일을 받아오는 이슈가 있었다.

프로젝트에 사용한 28개의 이미지 각각에서 위와 같은 코드가 반복되어 사용되었다. 이러한 코드의 중복을 피하기 위해 다음과 같이 추상화한 Picture 컴포넌트를 작성했다. (소스코드 바로가기)

<Picture 
  image={image} 
  tabletImage={imageTablet} 
  minType="webp" 
  alt="사용자 이미지" 
/>

 

 

2. 소스코드 요청 사이즈 크기

소스코드의 크기를 줄이는 방법은 크게 minify(경량화), uglify(난독화), 압축 3가지로 나눌 수 있다. 

📊 개선효과 요약

<여기서만나> 메인화면 기준, 개선 전 2.5MB에 달하는 번들을 받아왔지만, 개선 후에는 1.8 MB로 번들 사이즈를 줄일 수 있었다. (약 700 KB 감소효과) 

메인화면 JS 번들 크기 최적화 전/후

개선 전 외부에서 받아온 리소스만 약 200kB 압축해서 받아와 압축률이 8%에 그쳤던 데 비해, 개선 후에는 압축률이 27%로 상승했다. 300kB 가량 추가적인 트래픽을 절약하는 효과가 있었다.

 

⚒️ minify & uglify

minify, uglify는 webpack5 기준, production mode로 빌드할 경우 자동으로 도와준다.

"scripts": {
  "build": "webpack --mode=production
}

 

⚒️ 압축

압축은 gzip과 brotli 중 브라우저 지원범위가 더 넓은 gzip을 적용하였다.

gzip / brotli 브라우저 지원범위

compression 플러그인을 설치하고, 웹팩 설정파일에 아래와 같이 추가해주면, 각 js파일을 압축한 gz파일이 준비된다.

npm i -D compression-webpack-plugin
// webpack.config.js

const CompressionPlugin = require('compression-webpack-plugin');
const config = {
  ...
  plugins: [
    new CompressionPlugin({
      algorithm: 'gzip',
      test: /\.js$/,
    }),
  ]
  ...
}

 

⚒️ 서버야, gz 로 보내줘

만약 웹서버가 nginx 이고, 브라우저에서 gz파일을 받아오지 못하고 있다면, nginx 서버에 아래와 같이 설정을 추가해주자. 이 설정을 추가하면 서버에서 .js 요청을 받았을 때 보내줄 .gz가 있는지 찾아보고 있으면 .gz를 보내준다.

// etc/nginx/sites-enabled/default

location / {
  gzip_static on;
}