개선 전
이번 과제는 움짤을 볼 수 있는 'memegle' 사이트 개선하기이다.
깃허브 페이지 배포 후 라이트하우스로 성능을 측정하였다.
배포
... 그전에 개념
S3란?
Simple Storage Service로, AWS에서 제공하는 인터넷 스토리지 서비스이다.
S3를 사용하면 데이터를 안전하게 저장하고 필요할 때 언제든지 접근할 수 있다.
S3의 장점
✦ 확장성
무제한으로 확장 가능한 객체를 저장할 수 있다.
✦ 내구성
추후 어떤 데이터가 손상되더라도, 복제본을 통해 내구성을 높일 수 있다.
하지만, 내부 복제가 모두 완료되지 않은 상태라면 S3는 각각 다른 객체로 요청에 대해 응답해 사용자별로 받은 응답 결과가 다를 수 있다.
S3 버킷은 내부 복제를 통해 안정적으로 보관을 제공한다.
✦ 보안
암호화와 접근 제어 기능을 통해 데이터의 보안을 강화한다.
CDN이란?
✦ 캐싱
CDN 서버는 웹 사이트의 정적 파일을 원본 서버로부터 가져와 자체 서버에서 캐싱한다.
✦ 지역성
전 세계 여러 곳에 서버 네트워크를 가지고 있기 때문에 사용자에게 가장 가까운 위치에서 콘텐츠를 제공한다.
✦ 부하 분산
트래픽 부하를 분산시켜 원본 서버의 성능을 향상시킨다.
✦ 장애 극복
한 지역의 CDN 서버에 장애가 발생하더라도 다른 지역의 CDN 서버에서 콘텐츠를 제공받을 수 있다.
CloudFront란?
AWS에서 제공하는 CDN 서비스이다.
CloudFront를 사용하면 정적 및 동적 콘텐츠를 빠르고 안정적으로 사용자에게 전달할 수 있다.
CloudFront는 여러 개의 Edge Loction을 가지고 있어 사용자에게 가장 가까운 위치에서 컨텐츠를 제공한다.
은행에 방문하지 않고 atm 기기를 통해 돈을 인출하는 것과 비슷하다.
개선 전 페이지는 깃헙 페이지로 배포하고, 개선할 페이지는 s3로 배포한다.
주의사항에 따라 팀 버킷 내부에 폴더를 만들어 관리한다.
⚠️ 여기서 생긴 문제
기존 코드에서 깃헙 페이지 배포를 위해 App.tsx에 basename이 설정되어 있었다.
s3 배포를 위해서는 basename을 지우고 배포했어야 했는데, 파일 업로드할 때 포함한 채로 업로드했다.
이 경우 배포된 사이트에서 아무런 화면이 나타나지 않는다.
지우고 다시 업로드를 해도 똑같은 화면이 발생한다.
왜 그럴까?
CloudFront에서 캐싱된 내용이 그대로 보여서 그렇다.
빌드할 때마다 내용이 수정되어도 항상 동일한 파일명으로 생성되고
브라우저는 동일한 파일로 인식하기 때문에 캐싱된 파일을 불러오게 된다.
따라서 이전 배포한 내용이 그대로 보이는 것이다.
이를 해결하기 위해서는 해시값을 부여해주어야 한다.
부여해 줌으로써 이전 파일과 다르다는 것을 알려주는 것이다.
webpack에 hash 추가
우리는 bundle.js와 file-loader, html에 해시값을 추가해 줄 예정이다.
file 이름에 ‘[contenthash]’를 붙이면 손쉽게 hash 값을 추가할 수 있다.
이런 식으로 filename에 추가해 주면 빌드 시 아래와 같은 형태로 파일 이름이 변경되어 저장된다.
html의 경우 html webpack plugin 내부에 hash true를 설정해 준다.
재빌드한 파일을 다시 재 업로드 해주면 제대로 뜨는 것을 볼 수 있다.
만약 재 업로드 후에도 내용이 뜨지 않는다면 캐시 비우기 및 강력 새로고침을 해보자.
⁉️ 업로드 내용을 삭제 후 다시 올렸는데 또 해당 오류가 난다면 무효화해주자.
와일드카드를 사용해 전체 파일을 무효화해주면 된다.
개선 목표
자, 이제 개선해 보자.
이번 과제의 개선 목표는 아래와 같다.
- Lighthouse 95점 이상
- Home 페이지에서 불러오는 스크립트 리소스 크기 < 60kb
- 히어로 이미지 크기 < 120kb
- 프랑스 파리에서 Fast 3G 환경으로 접속했을 때 Home 두 번째 이후 로드 시 LCP < 1.2s
- WebPageTest에서 Paris - EC2 Chrome CPU 6x slowdown Network Fast 3G 환경 기준으로 확인
- Chrome CPU 6x slowdown Network Fast 3G 환경에서 화면 버벅거림 최소화
- Dropped Frame 없음. Partially Presented Frame 최소화.
작업 목록
1️⃣ 요청 크기 줄이기
- 소스코드 크기 줄이기
- 이미지 크기 줄이기
도구
- webpack
- CloudFront
키워드
- css/js minify, uglify
- gzip
- image optimization - image format, compression
처음 사이트에 접속하게 되면, 브라우저는 서버에 사이트에 대한 정보를 요청한다.
이때, 요청의 크기가 크게 되면, 브라우저에 내용을 띄우는데 오랜 시간이 걸린다.
당연히 이 시간이 오래 걸릴수록 사용자의 이탈률은 증가한다.
🐶 소스코드 크기 줄이기
요청 크기를 줄이는 방법으로는 크게 2가지가 있다.
첫 번째, 소스 코드의 크기를 줄인다.
코드의 양이 많으면, 당연히 빌드한 양도 많아지게 된다.
이는 웹팩 설정을 통해 해결할 수 있다.
시작하기 전 bundle.js의 크기를 보자.
2.8MB이다.
우리는 이 소스코드의 크기를 줄일 것이다.
소스코드의 크기를 줄이는 방법에는 크게 minify와 uglify, 압축이 있다.
순서대로 알아보자.
1. minify, uglify
✦ minify(압축화/경량화)
코드의 크기를 최소화하기 위해 공백, 줄 바꿈, 주석 등을 제거한다.
이렇게 하면 파일의 용량이 감소하고 다운로드 시간이 단축된다.
일반적으로 HTML, CSS 및 JavaScript 파일에 적용된다.
✦ uglify(난독화)
JavaScript 코드를 난독화하여 가독성을 감소시키고 분석 및 해석을 어렵게 만든다.
변수나 함수명 등이 줄어들어 용량이 감소한다.
소스 코드의 보안성을 향상할 수 있지만, 난독화 단계가 높으면 코드 해석과 실행 속도가 느려질 수 있다.
webpack4 버전부터 mode를 설정할 수 있다.
mode를 production(배포)이나 development(개발)로 설정해 두면 minify와 uglify를 자동으로 도와준다.
이렇게 package.json에서 명령어에 따라 mode를 설정해 줄 수 있다.
2. 압축
CloudFront에서 압축을 지원한다.
해당 CloudFront에서 동작으로 간 후 편집을 누르면
이렇게 객체 압축을 선택할 수 있다.
3. minimize
webpack의 minimize 옵션을 통해 코드를 압축하고 최소화할 수 있다.
false로 설정하면, 소스 코드가 압축되지 않고 그대로 번들링 된다.
개발 환경에서는 가독성이 좋은 형태로 유지하기 위해 false로,
배포 환경에서는 소스 코드를 압축하고 최소화하기 위해 true로 설정하는 것이 좋다.
만약 다른 minimizer를 추가할 일이 있다면 추가 전에 ‘…’를 추가해 주자.
그래야 덮어씌워지지 않고 작동한다.
4. CSS minimizer
css에서도 불필요한 코드를 제거하고 압축할 수 있다.
CssMinimizerWebpackPlugin을 사용해 아래와 같이 설정해 보자.
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
optimization: {
minimize: true,
minimizer: [
'...',
new CssMinimizerPlugin()
]
}
5. source map
webpack의 source-map을 사용하면 bundle.js.map이라는 디버깅 용도의 파일이 생긴다.
하지만 이 파일은 용량도 크고, 디버거 툴을 사용해 일반 사용자가 코드에 접근할 수 있어 보안상 좋지 않다.
따라서 false로 설정해 주자.
(webpack-cli를 5 버전으로 업데이트하니 false 입력 시 오류가 나 devtool 자체를 삭제했다)
여기까지 수행했을 때 배포 후 번들의 크기는 아래와 같다.
(수정 전 배포 bundle 크기)
(수정 후 배포 bundle 크기)
🐶 이미지 크기 줄이기
다음으로는 이미지 크기를 줄여보자.
평균적으로 웹 페이지 용량에서 이미지는 60% 이상을 차지하고 있다.
즉, 이미지 요청 사이즈를 개선하면 요청 사이즈를 줄이는데 크게 기여할 수 있다.
1. Webp & AVIF 변환
Webp는 구글이 발표한 포맷으로 무손실 압축과 손실 압축을 통해 파일 크기를 줄인다.
(무손실 압축은 데이터가 손실되지 않고, 손실 압축은 파일 크기가 줄어들지만 품질이 저하될 수 있다)
특히 사진 이미지 압축 효과가 높다.
일반적으로 파일 크기가 25~35% 정도 감소한다고 한다.
Webp는 위 사진과 같은 브라우저에서 지원하고 있다.
그래도 많은 브라우저에서 지원 중이라 고민해야 하나 싶지만
‘펀잇’에서 사파리 옛날 버전에서 dialog 오류가 있었기에 고려해야 할 요소다.
AVIF는 AOMedia에서 개발한 포맷으로 무손실 압축과 손실 압축 모두 지원한다.
AVIF의 경우 평균적으로 Webp 이미지보다 약 50% 정도 더 압축된다고 한다.
따라서 브라우저가 AVIF를 지원하면 이를, 그렇지 않다면 Webp를, 둘 다 없다면 기존 파일 포맷을 사용하도록 설정할 수 있다.
convertio를 사용해 Webp와 AVIF 파일을 만들어주자.
picture 태그와 source 태그, img 태그를 사용해 아래와 같이 설정할 수 있다.
<picture>
<source type="image/avif" srcSet={heroImageAvif} />
<source type="image/webp" srcSet={heroImageWebP} />
<img className={styles.heroImage} src={heroImage} alt="hero image" />
</picture>
그리고 webpack에도 Webp와 AVIF을 추가해 준다.
test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif|webp|avif)$/i,
png일 때 10.7MB였던 파일이
638KB으로 확 줄어들었다.
⚠️ 여기서 발생한 오류
Webp 파일과 AVIF 파일을 다른 이미지 파일과 같은 경로에 넣어줬는데 파일 인식을 못하는 오류가 발생했다.
실행시키면 위와 같이 제대로 받아져 오는데 react 내부에서는 오류가 떠 빌드를 할 수 없다.
아마 webpack 빌드 전 플러그인이 Webp 파일을 생성하기 전까지는 해당 경로가 존재하지 않아 에러가 발생하는 것 같다.
따라서 image-minimizer-webpack-plugin를 통해 설정해 주기로 했다.
image-minimizer-webpack-plugin을 사용하려면 webpack-cli를 5 버전 이상 설치해야 한다.
5 버전 이하를 사용할 경우 imagemin-xxx 관련 모듈이 없다는 에러가 뜨게 된다.
또한, webpack-dev-server 또한 버전을 올려주어야 한다.
그렇지 않으면…
위와 같은 에러를 만나게 된다.
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
2. 이미지 압축과 webp 자동 변환
ImageMinimizerPlugin을 사용하면 이미지 크기를 압축 및 리사이징 할 수 있다.
먼저 ImageMinimizerPlugin와 관련한 것들을 설치해 준다.
나는 imagemin과 lossy optimization을 선택할 예정이다.
npm install image-minimizer-webpack-plugin imagemin --save-dev
npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo --save-dev
그리고 webpack 파일에 해당 코드를 추가해 준다.
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
module.exports = {
optimization: {
minimizer: [
"...",
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.imageminMinify,
options: {
plugins: [
"imagemin-gifsicle",
"imagemin-mozjpeg",
"imagemin-pngquant",
"imagemin-svgo",
],
},
},
generator: [
{
preset: "webp",
implementation: ImageMinimizerPlugin.imageminGenerate,
options: {
plugins: ["imagemin-webp"],
},
},
],
}),
],
},
};
이후 빌드하면 10MB가 넘는 파일이 2.2MB로 확연하게 줄어든 것을 볼 수 있다.
⚠️ 여기서 발생한 오류
png 파일은 압축을 잘하는데 png 파일을 webp 파일로 변환을 못한다.
정확하게 말하자면, webp로 변환은 하지만 type을 이상하게 읽는다.
네트워크 탭을 보면 type을 ‘webp’로 인식해야 하는데 ‘text/html’ 형태로 인식해
사진을 제대로 받아오지 못하고 엑박 오류가 뜬다.
따라서 위와 다른 방식으로 webp 변환을 해주었다.
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
optimization: {
minimize: true,
minimizer: [
'...',
new ImageMinimizerPlugin({
deleteOriginalAssets: false,
minimizer: {
implementation: ImageMinimizerPlugin.imageminGenerate,
options: {
plugins: [['webp', { preset: 'photo', quality: 40 }]]
}
}
})
]
}
또한, 이미지를 사용할 html 코드를 수정해 준다.
아래와 같이 작성하면 webp 이미지가 없을 때 png 이미지를 사용한다.
<picture>
<source type="image/webp" srcSet={heroImage} />
<img className={styles.heroImage} src={heroImage} alt="hero image" />
</picture>
추가로 asset modules에 webp를 추가하고 type을 수정해 준다.
(type의 경우 webp 변환과는 상관없지만 webp를 추가하면서 같이 수정했음)
{
test: /\.(eot|svg|ttf|webp|woff|woff2|png|jpg|gif)$/i,
type: 'asset'
}
*여기서 잠깐 asset modules란?
asset modules를 사용해 asset 파일을 사용할 수 있다.
webpack 5 이전에는 raw-loader, url-loader, file-loader와 같은 로더를 사용하는 것이 일반적이었다.
이러한 로더를 대체하기 위해 4가지 모듈 유형이 추가되었는데 아래와 같다.
- asset/resource: 별도의 파일을 내보내고 URL을 추출한다 (file-loader)
- asset/inline: asset의 data URL을 내보낸다 (url-loader)
- asset/source: asset의 소스 코드를 내보낸다 (raw-loader)
- asset: data URL와 별도 파일 내보내기 중 자동으로 선택한다 (url-loader)
이중 asset을 선택한 이유는 용량 크기에 따라 모듈 종류를 자동으로 선택해 줘서 사용했다.
용량 크기가 8KB보다 작으면 inline module로 처리해 번들에 삽입해 주고, 크면 resource module로 처리해 파일로 분리해준다고 한다.
지금은 8KB 이하인 이미지가 없긴 하지만 추후 추가될 수도 있으니…!
현재 로드되는 이미지는 asset/resource로 들어간다.
asset/resource는 파일을 내보낼 때 기본적으로 [hash][ext][query] 파일명을 사용해 따로 hash 값을 부여할 필요가 없다.
webp로 변환시키니 이렇게나 파일 크기가 줄었다.
혹시 AVIF로 확장자로 바꾸어보면 어떨까?
plugins: [
['avif', { preset: 'photo', quality: 40 }],
['webp', { preset: 'photo', quality: 40 }]
]
<picture>
<source type="image/avif" srcSet={heroImage} />
<source type="image/webp" srcSet={heroImage} />
<img className={styles.heroImage} src={heroImage} alt="hero image" />
</picture>
115KB까지 줄어든 것을 볼 수 있다.
그런데 문제가 생긴다.
gif 파일도 같이 AVIF로 바뀌고 움짤이 멈춰버린다.
따로 설정해 준 것도 없는데 왜 이러는지 모르겠다…
그렇다면 webp로 가되, 이미지의 사이즈를 수정해 주자.
3. 이미지 사이즈 수정
렌더링 할 영역에 비해 이미지 자체 사이즈가 크다면 용량 낭비이다.
따라서 이미지 사이즈를 줄여 최적화할 수 있다.
ImageMinimizerPlugin를 사용하면 이미지 사이즈를 쉽게 수정할 수 있다.
plugins: [
['webp', { preset: 'photo', quality: 40, resize: { width: 1920, height: 1280 } }]
]
webp 파일이어서 그런지 줄어드는 용량은 크지 않군…
4. Gif 파일 mp4로 변환
gif 파일은 많은 용량을 차지한다.
따라서 웹 페이지에 움직이는 이미지를 넣고 싶다면 훨씬 용량이 적은 mp4 파일로 변환하자.
gif를 mp4로 변환한다.
Convertio에서 손쉽게 변환할 수 있다.
gif파일을 대체한 후 img 태그를 video 태그로 바꾼다.
<video className={styles.featureImage} autoPlay loop muted playsInline>
<source src={imageSrc} type="video/mp4" />
</video>
*참고
- autoPlay: 자동 재생
- loop: 반복 재생
- muted: 음소거
- playsInline: 재생시작 자동으로 전체화면 전환되는 것 방지
아래와 같던 용량이
이렇게 줄어들었다.
🐶 1단계 완료 후 점수
93점으로 올랐다.
2️⃣ 필요한 것만 요청하기
- Home 페이지에서 불러오는 스크립트 리소스에 gif 검색을 위한 giphy 모듈이 포함되어 있지 않아야 한다.
- react-icons 패키지에서 실제로 사용하는 아이콘 리소스만 빌드 결과에 포함되어야 한다.
도구
- webpack
- Chrome DevTools > Network
키워드
- Code Splitting
A라는 페이지에 접속한다고 가정해 보자.
이때, B나 C에 대한 페이지도 같이 요청한다면 불필요한 성능 저하가 발생한다.
우리의 번들 파일도 마찬가지이다.
A, B, C 파일 모두 하나의 번들 파일에 묶여있기 때문에 필요하지 않은 코드를 요청하게 되는 것이다.
🐶 Home 페이지에서 불러오는 스크립트 리소스에 gif 검색을 위한 giphy 모듈이 포함되어 있지 않아야 한다.
1. code splitting
코드 분할이란 하나의 코드 덩어리를 잘게 쪼개는 것을 의미한다.
코드 분할 작업을 통해 사용자는 현재 필요한 코드만 불러올 수 있게 된다.
로딩에 필요한 비용을 줄이고 사용성 또한 올라간다.
code splitting을 하는 방법에는 크게 2가지가 있다.
하나씩 적용해 보도록 하자.
- react lazy
react에서 react lazy를 사용해 code splitting을 할 수 있다.
가장 기본적인 방법으로는 Route-based code splitting이 있다.
라우트별로 lazy를 걸어두면 원하는 페이지만 우선적으로 불러올 수 있다.
먼저, 페이지를 import 하는 부분에 React.lazy로 감싸줘 동적 import를 하도록 한다.
const Home = React.lazy(() => import('./pages/Home/Home'));
const Search = React.lazy(() => import('./pages/Search/Search'));
그다음 suspense와 fallback을 사용한다.
const App = () => {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<NavBar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/search" element={<Search />} />
</Routes>
<Footer />
</Suspense>
</Router>
);
};
suspense는 lazy 컴포넌트가 로드될 때까지 로딩 화면을 보여줄 수 있고,
fallback의 경우 로드될 때까지 렌더링 할 React 엘리먼트를 받아들인다.
lazy를 적용하니 bundle.js 파일이 분리되었다.
chunk 된 파일이 각 페이지마다 분리되어 뜨는 게 맞는지 헷갈려서 이름을 설정했다.
chunkFilename: '[name].[contenthash].bundle.js',
Home일 경우에
Search일 경우에
이렇게 나눠서 뜨는 것을 볼 수 있다.
각 페이지에 접근할 때 해당 번들만 나타난다.
- preload
preload를 사용하면 현재 페이지에서 당장 필요한 리소스를 브라우저에게 알릴 수 있다.
예를 들어 폰트를 보자.
우리는 현재 화면에서 폰트가 반드시 사용될 것임을 예측할 수 있다.
따라서 preload 속성을 주어 브라우저가 빠르게 가져오게 한다.
<link
rel="preload"
href="https://fonts.googleapis.com/css2?family=Josefin+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap"
rel="stylesheet"
/>
또한, 폰트를 가져오는 사이트에 preconnect 속성을 주면 브라우저가 외부 도메인과 미리 연결할 수 있다.
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+CSS minify
번들 되어 있는 css 파일을 MiniCssExtractPlugin을 통해 분리할 수 있다.
css 파일을 분리하게 되면 물론 번들 파일의 크기가 줄어들지만
css 파일의 요청이 한 번 더 일어나게 된다.
따라서 css 파일을 분리하는 것이 적절한지 고려하고 적용하는 것이 좋다.
현재 과제에서는 css 파일과 번들링 개수가 적어 적용하지 않기로 했다.
🐶 react-icons 패키지에서 실제로 사용하는 아이콘 리소스만 빌드 결과에 포함되어야 한다.
🎸 webpack-bundle-analyzer 설치
react-icons가 전체 bundle에서 얼마나 차지하고 있는지 알기 위해 해당 플러그인을 설치해 주었다.
npm i -D webpack-bundle-analyzer
그리고 webpack에 들어가 설정해 주면 된다.
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
plugins: [
//...앞의 플러그인,
new BundleAnalyzerPlugin()
],
아래 이미지와 같이 번들에 어떤 것이 어느 정도의 양을 차지하고 있는지 그림으로 쉽게 볼 수 있다.
react-icons의 경우 bundle에서 많은 양을 차지하고 있다.
그 이유는 사용하는 아이콘과 상관없이 모든 아이콘을 다 불러오기 때문이다.
이를 개선한 @react-icons/all-files라는 별도의 라이브러리도 존재한다.
그런데 나의 번들에서는 딱 사용하는 ai 아이콘만 불러오는 것을 볼 수 있다.
심지어 용량도 약 600KB 정도밖에 되지 않는다.
다른 사람들을 보니 4MB를 훌쩍 넘었다는데 어떻게 된 일일까?
이유를 알아보기 전에 tree shaking에 대해 먼저 알아보자.
🌳 tree shaking
이름 그대로 나무를 흔들면 어떻게 될까?
잎이 우수수 떨어질 것이다.
코드도 마찬가지이다.
코드를 털어서 쓰지 않는 코드를 제거하는 것이다.
webpack 4 버전 이후부터는 production 모드에서 자동으로 tree shaking을 적용시켜 준다.
optimization의 usedExports을 보자.
usedExports는 사용되지 않는 import를 번들링 하지 않는다.
기본값이 true로 되어 있기에 tree shaking이 적용이 되었고, 사용하지 않는 react-icons를 털어낸 것으로 보인다.
한번, usedExports를 false로 설정하고 차이를 확인해 보자.
usedExports false 일 때
usedExports 기본값(true) 일 때
size 차이가 엄청 나는 모습을 볼 수 있다.
심지어 차지하는 비율도 달라졌다.
🐶 2단계 완료 후 점수
길어서 1,2 단계와 3,4 단계 나눠서 작성하도록 하겠다.
계속-
'우아한 테크코스' 카테고리의 다른 글
재사용 가능한 컴포넌트 Grid 오류 (0) | 2023.09.18 |
---|---|
프론트엔드 성능개선2 (0) | 2023.09.06 |
생각해보기🤔 - 장바구니 (0) | 2023.06.25 |
생각해보기🤔 - 페이먼츠 (0) | 2023.06.20 |
생각해보기🤔 - 다시, 점심 뭐 먹지 (0) | 2023.06.19 |
댓글