문제상황 및 프리뷰
🧨 프론트엔드에서 성능을 향상시킬 수 있는 두 가지는 크게 로딩 개선과 렌더링 개선이 있을 것입니다.
이번 포스트에서는 로딩 기준으로 와플마켓의 랜딩 페이지를 개선해봅시다.
- 🧐 사이트에 접속 했을 때 요소들이 보이기(First Painting)까지 오래 걸리는데, 이는 사용자의 이탈로 이어질 가능성이 높다.
- 🧐 네트워크의 페이로드가 크다.
- 🧐 코드 분할을 하면 필요한 모듈을 또 로드해야하고, 이를 기다려야한다.
[Expected solution]
- 🔮 현재 서비스는 SPA CSR로 되어있어서, 초기에 큰 크기의 번들 파일을 가져온다. 코드 분할 및 지연 로딩으로 초기 번들 파일 크기를 줄여보는 건 어떨까?
- 🔮 이미지 확장자 변환을 통해 용량을 감소시켜보는 건 어떨까?
- 🔮 사전 로딩을 통해 모듈의 필요 순간을 예측하고 미리 로드해보는 건 어떨까?
원인파악 및 해결
위에서 언급한 1번, 2번, 3번을 각각 살펴봅시다.
1. 코드 분할 및 지연 로딩으로 초기 번들 파일 크기 줄이기
(1) Problem
- 초기 번들 파일이 너무 커서 리소스 로드 속도가 오래 걸립니다.
- 첫 페인트까지 2075.0 ms.
(2) How to solve
- react에서 제공하는 Suspense, lazy를 이용해 코드 스플리팅을 적용합니다.
- 동적 import 문을 사용해서 런타임에 해당 모듈을 로드할 수 있도록 합니다.
- lazy 함수는 동적 import를 호출하여 그 결과인 promise를 반환하는 함수를 인자로 받습니다.
- 그렇게 lazy 함수가 반환한 값, 즉 import한 컴포넌트는 Suspense 안에서 렌더링 해야하는데, 그러면 동적 import 하는 동안 아직 값을 갖지 못할 때는 Suspense의 fallback prop에 정의된 내용으로 렌더링 됩니다.
(3) Results
- 초기 랜딩 페이지와는 무관한 페이지에 한해 lazy loading이 적용되도록 스플리팅 해주었습니다.
import HomePage from '../pages/home';
import ErrorPage from '../pages/error';
import SignUpPage from '../pages/signup';
import LoginPage from '../pages/login';
import KaKaoLogin from '../pages/login/kakao';
import GoogleLoginPage from '../pages/login/google';
// ...
const MarketPage = lazy(() => import('../pages/market'));
const SendReviewPage = lazy(() => import('../pages/send-review'));
const MyReviewPage = lazy(() => import('../pages/my-review'));
const OthersReviewPage = lazy(() => import('../pages/others-review'));
const NeighborhoodLanding = lazy(() => import('../pages/neighborhood-landing'));
const NeighborhoodPostPage = lazy(() => import('../pages/neighborhood-post'));
const MySellHistoryPage = lazy(() => import('../pages/my-sell-history'));
const OthersSellHistoryPage = lazy(() => import('../pages/others-sell-history'));
const BuyHistoryPage = lazy(() => import('../pages/buy-history'));
const LikeHistoryPage = lazy(() => import('../pages/like-history'));
const NeighborHistoryPage = lazy(() => import('../pages/neighbor-history'));
스플리팅 전과 후 번들 파일 구조 ScreenShots 🔽
(4) Prize
네트워크 상태 | 스플리팅 전 | 스플리팅 후 |
제한없음 | 요청 기간: 896.89ms / FP: 2075.0ms | 요청 기간: 502.46ms / FP: 1626.7ms |
- 첫 번들 파일의 요청 시간 단축: 896.89ms → 502.46ms
- First painting 시간 단축: 2075ms → 1626.7ms
2. 이미지 확장자 변환을 통한 용량 감소
(1) Problem
- 네트워크 페이로드가 커서 로드 시간이 길어졌습니다.
- 랜딩 페이지에서 로딩되어야 하는 이미지가 많은 용량을 차지하고 있음을 알 수 있었습니다.
(2) How to solve
webp는 구글이 웹페이지 로딩 속도를 높이기 위해 개발한 이미지 포맷입니다.
이미지 품질은 유지하면서 파일크기를 더 작게 만들 수 있는 무손실 압축 확장자입니다.
- 이미지 최적화
- JPG, PNG, GIF 보다 크기는 작지만 이미지 품질은 동일하게 유지할 수 있습니다. 이미지 압축 시 손상이 거의 발생하지 않기 때문인데요, 결과적으로 이미지 압축은 웹사이트 속도를 증가시킬 수 있고, 웹사이트 속도 증가는 사용자 경험이 향상됩니다. 이러한 이유를 포함해 구글 역시 WEBP 사용을 권장하기 때문에 검색 엔진 순위에도 영향을 줄 수 있습니다.
- 서버 공간 절약
- 의외로 이미지는 서버에서 많은 공간을 차지합니다. WEBP를 사용하게 되면 JPG, PNG, GIF과 품질은 같아도 크기가 작기 때문에 서버 용량을 절약할 수 있습니다. 구글 자체 데이터에 따르면 WEBP 압축은 PNG 파일보다 26% 작은 크기이며, JPEG보다 25~34% 작은 크기입니다.
- 홈페이지 속도 최적화
- 좋은 홈페이지를 만들려면 홈페이지 속도가 매우 중요합니다. webp 파일은 JPG, PNG, GIF 대비 평균 30%의 이미지 크기를 줄여주기 때문에 홈페이지가 로딩되는 시간을 단축시켜 줍니다.. 기존의 이미지를 webp 파일로 변경할 시, 이미지를 대량으로 사용해야 하는 홈페이지 또는 모바일에 긍정적인 영향을 줄 수 있습니다.
(3) Result
- 이미지 확장자 변환 png, svg → webp
(4) Prize
네트워크 상태 | webp 변환 전 | webp 변환 후 |
제한없음 | 총 크기: 19,756KiB | 총 크기: 9,851KiB |
- 네트워크 페이로드 용량 감소: 총 크기: 19,756KiB → 총 크기: 9,851KiB
Comment
- 플러그인 등을 통해 이미지 확장자 변환 자동화 작업이 필요할 것 같습니다.
3. 사전 로딩을 통해 모듈의 필요순간을 예측, 미리 로드하기
(1) Problem
- 초기 랜딩 페이지의 리소스 로드 속도를 단축 시킨 상황
- 서비스의 핵심적인 부분은 중고거래 물품 부분인데, 중고거래를 누르면 이에 해당하는 리소스들이 로드되길 또 다시 기다려야 합니다.
(2) How to solve
- preloading을 적용합니다.
- 나중에 필요한 모듈을 필요해질 시점 이전에 미리 로드하는 기법입니다.
- preloading을 언제할지, 그 시점 정하기가 중요합니다.
(3) Result
- 버튼 위에 마우스를 올려 놓았을 때 preloading이 가능하게 합니다.
const Navigation = ({ selected }: NavigationProps) => {
const handleMouseEnter = useCallback(() => {
import('../../../pages/market')
}, []);
return (
<S.NavWrapper>
<S.CategoryWrapper>
<Link to="/">
<S.Category selected={selected.intro}>소개</S.Category>
</Link>
<Link to="/market">
<S.Category onMouseEnter={handleMouseEnter} selected={selected.market}>중고거래</S.Category>
</Link>
<Link to="/neighborhood">
<S.Category selected={selected.neighborhood}>동네생활</S.Category>
</Link>
</S.CategoryWrapper>
</S.NavWrapper>
);
};
- landing 페이지 컴포넌트가 마운트 완료된 시점에 preloading합니다.
useEffect(() => {
import('../../../pages/market')
}, []);
(4) Figure
- market 페이지와 관련된 chunk.js (중고거래 관련 파일)는 이후에 로딩이 됨을 확인할 수 있습니다.
'프로젝트 기록' 카테고리의 다른 글
[React-draggable-selector] 셀렉터 라이브러리 제작하기 (1) - 개요 및 기능탐색 (0) | 2023.08.12 |
---|---|
[Meetable] Draggable-time-selector 구현기 (with REACT, TS) (4) | 2023.08.01 |
[Dotting] paint bucket brush mode 구현하기 (0) | 2023.04.30 |
[Waffle-market] react-router에서 Private Route 구현을 통한 권한별 접근제어 설정 (0) | 2023.02.19 |
[Waffle-market] React + stompJS를 이용한 실시간 채팅구현 (0) | 2023.01.31 |