Web/[JS] FrontEnd

웹 성능 최적화 - Code Split

ihl 2021. 5. 9. 22:45

로딩페이지와 사용자반응

  웹 로딩 속도는 고객 경험에 매우 중요한 요소이다. KissMetrics 연구에 의하면 47%의 소비자는 웹 페이지가 2초 이내에 로드되기를 기대하며 3초가 되면 40%의 소비자가 웹 페이지를 이탈한다고 한다. 기사에 의하면 웹 로딩 속도 1초가 빨라지면 아마존 판매량이 1%증가 한다고 한다.

 

  개발자가 웹 성능을 높이는 방법은 무엇일까? 우선 웹 성능 결정요소는 크게 로딩 성능과 렌더링 성능으로 나뉜다. 로딩 성능은 필요한 리소스를 불러오는 것이고 렌더링 성능은 불러온 리소스를 화면에 보여주는 것이다. 이 글에선 로딩 성능을 최적화하는 Code Split을 해보려 한다.

 

1. 페이지 분석

퍼포먼스 탭

  최근 프로젝트인 땅땅마켓의 Search페이지에서 개발자 도구의 퍼포먼스 탭을 이용하여 분석해보았다. 그 결과 지금 당장 필요하지 않은 모듈까지 해당 페이지에서 불러오고 있는 것을 알 수 있었다. bundle-analyzer도 한 번 분석해보자.

 

main.chunk

  React를 빌드하면 WebPack은 비슷한 파일을 묶는 Bundling을 수행한다. 웹 페이지는 번들링된 결과 단위로 코드를 불러온다. 그런데 번들링 결과가 너무 크다면 페이지 접속 시 소스 코드를 불러오는데 오랜 시간이 걸린다.

 

  위 사진은 cra-bundle-analyzer를 이용하여 프로젝트를 분석한 결과의 일부이다. main.chunk라는 번들에 64개의 모듈이 함께 들어있는 것을 확인할 수 있다. 즉, index.tsx를 보여주기위해 당장 필요하지 않은 RegisterForm, FilterBtn 등의 모듈들을 함께 불러오므로 웹 로딩 시간이 느려질 수밖에 없다.

 

2. React lazy, preload

Modal

  페이지에서 당장 필요하지 않은 대표적인 모듈은 모달이다. 모달은 숨겨져 있다가 사용자가 특정 컨텐츠를 선택했을 때에 비로소 필요해지기 까닭이다. 즉, 페이지를 로드할 때 꼭 필요한 모듈은 아니다. 이 부분을 Code Split 해보자

 

import React, {Suspense, lazy} from 'react';

const Modal = lazy(() => import('../Modal/index'));
const ItemDetail = lazy(() => import('./ItemDetail'));

const ItemCard: React.FC<Props> = ({item}) => {
//생략..
return (
    <>
      <Suspense fallback={<div>로딩중...</div>}>
        <Modal visible={isOpenPopup} closeCb={closePopUp} className={'sidemodal'}>
          <ItemDetail item={item} requestBid={requestBid}></ItemDetail>
        </Modal>
      </Suspense>
      {/*생략...*/}
    </>
  );
};

  React에서 Code Split을 하기 위해 Suspense, lazy 가 필요하다. lazy는 늦게 모듈을 import 하기 위함이고 Suspense는 해당 모듈이 import 되지 않았을 때 대신 보여줄 요소를 의미한다. 이렇게 하면 위 컴포넌트를 로드할 때 Modal과 ItemDetail은 함께 로드되지 않고, 해당 컴포넌트가 필요할 때 로드를 시작한다.

 

import React, {Suspense, lazy, useEffect} from 'react';

const Modal = lazy(() => import('../Modal/index'));
const ItemDetail = lazy(() => import('./ItemDetail'));

const ItemCard: React.FC<Props> = ({item}) => {
//생략..

useEffect(() => {    
    import('../Modal/index');
    import('./ItemDetail');
  }, []);

return (
    <>
      <Suspense fallback={<div>로딩중...</div>}>
        <Modal visible={isOpenPopup} closeCb={closePopUp} className={'sidemodal'}>
          <ItemDetail item={item} requestBid={requestBid} ></ItemDetail>
        </Modal>
      </Suspense>
      {/*생략...*/}
    </>
  );
};

  그런데 모달의 내용이 너무 많거나 용량이 커서 미리 로드하는 것이 나을 수도 있다. 이 때는 개발자가 별도로 모달을 미리 로드하는 것이 좋다. 위 코드는 ComponentDidMount 후에 Modal과 ItemDetail을 미리 로드하는 코드이다. 이렇게 하면 위 컴포넌트(Search)와 모달을 분리하면서도 사용자 경험을 해치지 않는다.

 

  이 외에 Code Split을 하기 좋은 장소는 라우트이다. A페이지에 있다면 B페이지의 요소들과는 상호작용할 일이 없기 때문이다. 이 부분들도 위와 같은 방식으로 lazy-pre load를 할 수 있다.

 

3. 결과

Code Split 결과

    Code Split 결과 index.tsx + 64 modules 였던 번들이 index.tsx + 26 modules로 변경되었다. 퍼포먼스 탭에서도 서치 페이지에서 ItemDetail을 불러오지 않는 것을 확인하였다.

 

  React.lazy, Suspense를 쓸 때 주의할 점은 서버 사이드 렌더링을 지원하지 않는다는 점이다. 서버 사이드 렌더링을 한다면 Loadable Components를 사용하는 것이 좋다.