프로젝트 기록

[Waffle-market] 랜딩페이지 성능 개선하기

lerrybe 2023. 7. 27. 14:25

문제상황 및 프리뷰

🧨 프론트엔드에서 성능을 향상시킬 수 있는 두 가지는 크게 로딩 개선과 렌더링 개선이 있을 것입니다.

이번 포스트에서는 로딩 기준으로 와플마켓의 랜딩 페이지를 개선해봅시다.

 

와플마켓 랜딩 페이지

 

 

  1. 🧐 사이트에 접속 했을 때 요소들이 보이기(First Painting)까지 오래 걸리는데, 이는 사용자의 이탈로 이어질 가능성이 높다.
  2. 🧐 네트워크의 페이로드가 크다.
  3. 🧐 코드 분할을 하면 필요한 모듈을 또 로드해야하고, 이를 기다려야한다.

 

[Expected solution]

  1. 🔮 현재 서비스는 SPA CSR로 되어있어서, 초기에 큰 크기의 번들 파일을 가져온다. 코드 분할 및 지연 로딩으로 초기 번들 파일 크기를 줄여보는 건 어떨까?
  2. 🔮 이미지 확장자 변환을 통해 용량을 감소시켜보는 건 어떨까?
  3. 🔮 사전 로딩을 통해 모듈의 필요 순간을 예측하고 미리 로드해보는 건 어떨까?

 

 


 

원인파악 및 해결

위에서 언급한 1번, 2번, 3번을 각각 살펴봅시다.

 

1. 코드 분할 및 지연 로딩으로 초기 번들 파일 크기 줄이기

(1) Problem

  • 초기 번들 파일이 너무 커서 리소스 로드 속도가 오래 걸립니다.
  • 첫 페인트까지 2075.0 ms.

 

네트워크: 제한 없음 ver.
첫 페인트까지 걸리는 시간: 2075.0ms

 

(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 (중고거래 관련 파일)는 이후에 로딩이 됨을 확인할 수 있습니다.