🖱️ 이전 편 : 성능 최적화 - 1. 필요성,측정 도구 및 성능 저하 해결 방법
Webpack 5는 기본적으로 TerserPlugin을 사용하여 자바스크립트 파일을 압축한다. 추가적인 설정이 필요 없다면 mode: 'production'
으로 설정하면 자동으로 압축이 적용된다.
// webpack.config.js
module.exports = {
mode: "production", // 자동으로 TerserPlugin이 활성화됨
};
난독화는 코드의 가독성을 낮추어 악의적인 사용자가 코드를 분석하거나 도용하는 것을 방지하기 위한 기법입니다. 난독화 과정에서 변수 이름이 짧아지기 때문에 번들 크기를 줄이는데 기여한다. Webpack 5에서 기본적으로 제공하는 TerserPlugin을 사용하여 난독화를 진행 할 수 있다.
// webpack.config.js
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
mode: "production", // 기본 압축 및 최적화 활성화
optimization: {
minimize: true, // production 모드에서 minimize의 기본값은 true
minimizer: [
new TerserPlugin({
terserOptions: {
mangle: true, // 변수 및 함수 이름 변경 (난독화)
compress: {
drop_console: true, // 콘솔 로그 제거
},
output: {
comments: false, // 주석 제거
},
},
}),
],
},
};
Tree shaking은 dead code(= 실제로 사용하지 않는 코드, import 되지 않는 코드)를 제거하는 기능이다. 이를 위해서는 ES6 모듈을 사용하고 코드에서 불필요한 코드를 없애는 것이 중요하다. 기본적으로 Webpack 5는 mode: 'production'
에서 Tree shaking을 활성화된다.
Code splitting은 큰 애플리케이션을 작은 청크로 나누어 필요한 부분만 로딩하는 것을 말한다. 동적 import(dynamic import)를 이용하거나 Webpack의 splitChunks 옵션을 사용하면 Code splitting을 구현할 수 있다.
동적 import(dynamic import)
동적 import를 적용하면 해당 페이지에 필요한 컴포넌트만 불러온다.
const Home = lazy(() => import("./pages/Home/Home"));
const Search = lazy(() => import("./pages/Search/Search"));
import NavBar from "./components/NavBar/NavBar";
import Footer from "./components/Footer/Footer";
import "./App.css";
import { lazy, Suspense } from "react";
import LoadingBar from "./components/LoadingBar/LoadingBar";
const App = () => {
return (
<Router basename={"/"}>
<NavBar />
<Suspense fallback={<LoadingBar />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/search" element={<Search />} />
</Routes>
<Footer />
</Suspense>
</Router>
);
};
splitChunks 옵션 사용
//Webpack.config.js
module.exports = {
//....
optimization: {
splitChunks: {
chunks: "all",
},
},
};
압축 방식으로는 Gzip, 구글에서 개발한 Gzip보다 압축륙이 높은 Brotli가 있다.
모든 주요 브라우저(Chrome, Firefox, Edge, Safari등)는 Gzip과 Brotli 압축을 지원하며, 브라우저의 Accept-Encoding
헤더 값에 따라 서버는 압축된 파일을 전송하고 브라우버는 이를 압축 해제해 사용한다.
CloudFront 자동 압축 활용하기
CloudFront 배포 설정에서 Compress objects automatically
옵션을 키면 압축이 활성화된다.
압축 방식은 브라우저의 Accept-Encoding
헤더에 따라 결정되며, Brotli가 Gzip보다 우선적으로 적용된다.
Accept-Encoding: br, gzip, deflate
Accept-Encoding: gzip, deflate
Webpack을 사용한 Brotli 압축
//Webpack.config.js
const CompressionPlugin = require("compression-webpack-plugin");
module.exports = {
plugins: [
new CompressionPlugin({
filename: "[path][base].br",
algorithm: "brotliCompress", // Brotli 압축 사용
test: /\.(js|jsx|ts|tsx|css|html|svg|ico)$/, // 압축할 파일 유형
threshold: 8192, // 8KB 이상의 파일만 압축
minRatio: 0.8, // 압축 후 80% 이하로 줄어든 파일만 압축
compressionOptions: {
level: 11, // 압축 수준 (0~11, 기본값: 11)
},
deleteOriginalAssets: false, //Brotli 압축을 지원하지 않는 브라우저를 위해 원본 파일을 삭제하지 않음
}),
],
};
CloudFront에서 실시간으로 모듈을 압축하는 것은 S3에 압축된 파일을 올리는 것보다 속도가 느리다. 그러나 브라우저의 Accept-Encoding
헤더에 따라 적절한 방식의 응답을 제공할 수 있어 동적 콘텐츠나 자주 변경되는 캐시를 사용하는 파일에 유리하다.
따라서 정적 파일이라면 Webpack에서 미리 압축을 하고, 동적 콘텐츠라면 CloudFront의 실시간 압축을 사용하는 게 좋다. 단 Webpack에서 압축을 한다면 서비스 지원 브라우저에서 사용하려는 압축 방식을 지원하는 지 살펴봐야한다. 지원하는 브라우저에서 Gzip만을 지원하는데, S3에는 Brotli 압축 파일만 있다면 CloudFront는 응답을 보내지 않는다.
CSS-in-JS는 JS최적화에서 진행되기 때문에 여기서는 .css
확장자를 사용하는 CSS 파일 기반 스타일링에 대한 최적화 방법에 대해 살펴보자.
Webpack에서 style-loader를 사용하면 CSS는 JS가 번들링된 bundle.js에 인라인으로 포함된다.
CSS 파일을 별도로 추출해야하는 이유?
브라우저 렌더링 과정을 떠올리면, JS파일과 CSS 파일의 로딩 방식은 다르며, 각각 렌더링 과정에 끼치는 영향도 다르다. CSS와 JS가 함께 bundle.js에 묶여 있으면 bundle.js를 모두 다운로드하고 파싱하는 과정이 끝나기 전까지 렌더링이 지연될 수 있다. 반면에 CSS 파일을 별도로 추출하면, 브라우저는 CSS와 JS를 병렬적으로 다운로드해 초기 페이지 로딩 성능이 개선된다. 또한 CSS가 별도의 파일로 추출되면 CSS 파일을 빠르게 로드할 수 있기 때문에 렌더링 차단을 줄일 수 있다.
CSS 파일을 bundle.js에 포함하면, JS파일이 조금만 변경되어도 전체 bundle.js가 다시 다운로드된다. 반면 CSS 파일을 따로 추출하면 CSS나 JS 중 하나가 변경되어도 각각의 파일만 새로 캐싱하여 다운로드할 수 있다. 이는 캐싱 효율성을 높여 성능 개선에도 도움이 된다.
비교 항목 | CSS가 JS와 함께 있을 때 (bundle.js) | CSS가 별도로 있을 때 |
---|---|---|
로딩 방식 | JS와 함께 다운로드되며, JS 파싱이 끝날 때까지 렌더링 지연 | CSS와 JS가 병렬로 다운로드되어 빠른 렌더링 가능 |
렌더링 차단 | JS 파싱이 끝날 때까지 CSS 적용이 지연됨 | CSS가 빠르게 로드되어 렌더링 차단을 줄일 수 있음 |
병렬 다운로드 | 병렬 다운로드 불가능, JS 파싱 후 CSS 적용 | CSS와 JS가 병렬로 다운로드되어 초기 로딩 성능 개선 |
캐싱 효율성 | JS가 변경되면 CSS도 함께 다시 다운로드 | CSS나 JS 중 하나만 변경되면 해당 파일만 다시 다운로드 |
코드 스플리팅 | 어려움 | 페이지별로 필요한 CSS만 로드 가능 |
성능 측정 지표 (LCP, TTI 등) | LCP 및 TTI 저하 가능성 있음 | LCP 및 TTI 개선 가능 |
렌더링 속도 | 느림 | 빠름 |
별도 추출 방법
MiniCssExtractPlugin
을 사용하여 CSS를 별도의 파일로 추출할 수 있다.
// Webpack.config.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].[contenthash].css", // 캐싱을 위해 contenthash 사용
}),
],
};
css-minimizer-webpack-plugin
을 사용하여 CSS 파일을 압축할 수 있다.
purgecss-webpack-plugin
을 사용하여 사용하지 않는 CSS를 제거할 수 있다.
CSS 파일을 압축하거나 중복 CSS를 제거하면 CSS 파일 크기가 줄어서 로딩 속도가 빨라진다.
// Webpack.config.js
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const PurgeCSSPlugin = require("purgecss-webpack-plugin");
const glob = require("glob-all");
const path = require("path");
module.exports = {
//....
plugins: [
new PurgeCSSPlugin({
paths: glob.sync([
path.join(__dirname, "src/**/*.js"),
path.join(__dirname, "public/index.html"),
]),
}),
],
//....
optimization: {
minimize: true,
minimizer: [
`...`, // 기본적으로 제공되는 minimizer 설정을 확장
new CssMinimizerPlugin(),
],
},
};
변경할 이미지 포맷 선정하기
이미지 포맷 중 파일 크기가 작은 포맷들은 WebP, AVIF, 그리고 JPEG이다.
이미지 포맷 | 파일 크기 | 장점 | 단점 | 지원 수준 |
---|---|---|---|---|
AVIF | 가장 작음 | - 매우 높은 압축률로 파일 크기 작음 - 고품질 이미지 유지 - HDR 지원 | - 인코딩 속도가 느림 - 지원 브라우저 및 도구가 아직 제한적 | 최신 브라우저 일부 (Chrome, Firefox 등) |
WebP | 작음 | - JPEG보다 25~34% 더 작은 파일 크기 - 투명도 및 애니메이션 지원 | - 일부 오래된 브라우저에서 지원 부족 | 최신 브라우저 대부분 (Chrome, Firefox, Edge, Safari 등) |
JPEG | 중간 | - 널리 사용됨 - 모든 브라우저에서 지원 - 손실 압축으로 크기 조정 가능 | - 무손실 압축 미지원 - WebP 및 AVIF보다 파일 크기 큼 | 모든 브라우저 및 기기에서 지원 |
PNG | 큼 | - 무손실 압축 - 투명도 지원 | - 파일 크기가 큼 - 손실 압축 미지원 | 모든 브라우저 및 기기에서 지원 |
GIF | 중간~큼 | - 애니메이션 지원 - 간단한 이미지에 적합 | - 색상 제한(256색) - 큰 파일 크기 | 모든 브라우저 및 기기에서 지원 |
이미지 파일 크기는 AVIF < WebP < JPEG < PNG
순으로 크다. AVIF는 이미지 파일 크기가 가장 작지만,아직 지원하는 브라우저와 툴이 제한적이다. 그래서 무손실 압축이며 GIF도 지원하고 대부분의 최신 브라우저가 지원하는 WebP가 좋은 대안이다.
단, WebP를 지원하지 않은 브라우저가 있기 때문에 picture
태그나 img
태그와 srcSect 조합과 함께 모든 브라우저에서 사용하는 JPEG를 같이 사용하는 것을 권한다.
이미지 포맷 및 압축하기
ImageMinimizerPlugin
을 사용하면 이미지 파일을 크기가 작은 포맷으로 바꾸고 이미지 파일을 압축할 수 있다.
압축률은 quality
옵션을 통해 결정된다. 압축률이 커질 수록 이미지 파일 크기는 줄지만 그 만큼 이미지의 해상도는 낮아지기 때문에, 무작정 높은 압축률은 피해야한다. 이미지 파일의 쓰임에 따라 그에 맞는 압축률을 선정해야한다.
상황 | 압축률(%) |
---|---|
디테일이 중요한 이미지 | 80~85 |
단순한 색상과 선으로 구성된 이미지 | 75~80 |
일반적으로 많이 사용되는 압축률 | 75-85 |
//Webpack.config.js
module.exports = {
//...
optimization: {
minimize: true,
minimizer: [
//...
new ImageMinimizerPlugin({
generator: [
{
// webp 포맷으로 변경할 이미지를 사용할 때, '?as=webp'를 넣어서 해당 파일을 webp로 변경 (자세한 사용예시는 'picture 태그 사용 예시'를 참고)
preset: "webp",
implementation: ImageMinimizerPlugin.sharpGenerate,
options: {
encodeOptions: {
webp: {
quality: 70,
},
},
},
},
{
//jpeg 포맷으로 변경할 이미지를 사용할 때, '?as=jpeg'를 넣어서 해당 파일을 jpeg로 변경
preset: "jpeg",
implementation: ImageMinimizerPlugin.sharpGenerate,
options: {
encodeOptions: {
jpeg: {
quality: 70,
},
},
},
},
],
minimizer: {
implementation: ImageMinimizerPlugin.sharpMinify,
options: {
encodeOptions: {
webp: { quality: 70 },
jpeg: { quality: 70 },
gift: { quality: 70 },
},
},
},
}),
],
},
};
//App.jsx
import webpImg from "./file.jpg?as=webp";
import jpegImg from "./file.jpg?as=jpeg";
//...
const App = () => {
return (
<div>
<picture>
{/* WebP 포맷이 지원되는 브라우저에서 이 이미지를 로드 */}
<source srcSet={webpImg} type="image/webp" />
{/* WebP 포맷이 지원되지 않으면 JPEG 이미지를 로드 */}
<img src={jpegImg} alt="Sample" />
</picture>
</div>
);
};
import webpImg from "./file.jpg?as=webp"; // WebP 이미지
import jpegImg from "./file.jpg?as=jpeg"; // JPEG 이미지
const App = () => {
return (
<div>
<img
src={jpegImg} // 기본 이미지 폴백(JPEG)
srcSet={`${webpImg} 1x, ${jpegImg} 2x`} // WebP를 우선 제공
type="image/webp"
alt="Sample"
/>
</div>
);
};
export default App;
반응형 이미지는 사용자 화면 크기나 해상도에 맞춰 적절한 이미지를 제공하는 것을 말한다.
다양한 화면 크기와 해상도에 맞춰 최적화된 이미지를 제공하여 페이지 로딩 시간과 데이터 사용량을 줄일 수 있다.
responsive-loader
를 사용하면 빌드 시 원하는 이미지 포맷,이미지 사이즈로 반응형 이미지 파일을 만들 수 있다.
반응형 이미지 방법
//Webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.responsive.(jpg|jpe?g|png|webp)$/i,
type: "javascript/auto",
use: [
{
// webp로 변경하고, size에 맞는 반응형 이미지 파일 생성
loader: "responsive-loader",
options: {
adapter: require("responsive-loader/sharp"),
format: "webp",
name: "[name]-[width]w.[ext]",
sizes: [1440, 1020, 768, 425],
placeholder: true,
placeholderSize: 20,
quality: 60,
outputPath: "static",
},
},
],
},
],
},
};
//App.jsx
import homeImage from "../../assets/images/home.responsive.png";
//homeImage.srcSect: "home-1440w.webp 1440w, home-1020w.webp 1020w,...,home-425w.webp 425w"
const App = () => {
return (
<picture>
<source
srcSet={homeImage.srcSet}
type="image/webp"
sizes="(max-width: 425px) 425px, (max-width: 768px) 768px,(max-width: 1024px) 1024px, (max-width: 1440px) 1440px, 100vw"
/>
<img className={styles.heroImage} src={heroJpgImage} alt="Hero" />
</picture>
);
};
img
에 loading="lazy"
를 설정하면, 화면에 보일 때 해당 이미지가 로드된다.
즉, 화면에 보이는 이미지들만 로드되고 화면에 아직 보일 필요가 없는 이미지는 로드되지 않아 렌더링 시간이 단축된다.
<img src="image.jpg" alt="Lazy Loaded Image" loading="lazy" />
'리뷰미' 프로젝트에서 성능 최적화를 진행하며, 각 작업이 성능 지표에 긍정적인 영향을 미치는 것을 확인할 수 있었다. 하지만 작업을 하나씩 추가하는 과정에서는 사용자 경험이 얼마나 개선될지 직접적으로 와닿지 않았다. 주요 기능에 대한 최적화가 마무리되었을 때, dev 페이지와 production 페이지 간의 로딩 속도 차이가 확연히 느껴졌다.
디바이스와 네트워크 성능이 점차 발전하고 있지만, 모든 사용자가 항상 좋은 환경에서 웹을 사용하는 것은 아니다. 개발자는 기능을 구현할 때, 다양한 사용자 환경을 고려하여 최적화된 경험을 제공해야 한다. 성능 최적화는 사용자에게 빠르고 쾌적한 웹 경험을 선사하기 위한 필수적인 과정이며, 이를 위해 지속적인 노력이 필요하다고 생각한다.