본문 바로가기

Frontend/성능 최적화

[올림픽 통계 서비스 컴포넌트 지연 로딩, 컴포넌트 사전 로딩, 이미지 사전 로딩] 웹 성능 최적화까지 해보자-8

컴포넌트 지연 로딩

블로그 서비스에서는 페이지를 기준으로 코드를 분할하고 분할된 코드를 필요한 시점, 즉 페이지가 변경되는 시점에 로드하도록 했다.

이번에도 비슷하게 해보자.

 

번들 파일 분석

컴포넌트 지연 로딩 기법을 적용하기 전에 번들 파일을 분석해서 서비스에 어떤 문제가 있는지 파악해보자.

cra-bundle-analyzer를 실행해보자.

 

아래 사진은 올림픽 통계 서비스의 번들 분석 결과이다.

하나씩 살펴보면 왼쪽의 static/js/2.chunk.js 블록은 node_modules에 있는 라이브러리 코드를 담고 있는 청크이고, 오른쪽 파란색 블록은 올림픽 통계 서비스의 코드임을 알 수 있다.

 

그 중 2.chunk.js의 내용을 보면 react-dom, styled-components뿐만 아니라 react-image-gallery라는 라이브러리가 들어 있다.

react-image-gallery 라이브러리는 서비스 첫 화면부터 필요한 건 아니다.

이 라이브러리가 필요한 시점은 사진 갤러리가 있는 모달 창을 띄울 때이다.

 

26KB면 그렇게 큰 용량은 아니지만 조금이라도 효율적으로 사용하기 위해서 이 라이브러리의 코드를 분할하고 지연 로딩을 적용해보자.

 

모달 코드 분리하기

이번에 해볼 작업은 블로그 서비스에 적용했던 방법과 거의 동일하다.

App.js 파일에서 코드 분할과 지연 로딩을 위해서 리액트 라이브러리의 Suspense 컴포넌트와 lazy 함수를 불러온다.

이후, 분할하고자 하는 컴포넌트인 ImageModal 컴포넌트를 import 함수와 함께 lazy 함수의 인자로 넘겨주면 된다.

 

이때, react-image-gallery 라이브러리만 분할하지 않는 이유는 모달 컴포넌트도 첫 페이지 로딩 시 바로 필요한 코드가 아니기 때문에 함께 묶어서 분할하는 것이다.

import React, { Suspense, lazy, useState } from "react";
import styled from "styled-components";
import Header from "./components/Header";
import InfoTable from "./components/InfoTable";
import SurveyChart from "./components/SurveyChart";
import Footer from "./components/Footer";

const LazyImageModal = lazy(() => import("./components/ImageModal"));

function App() {
  const [showModal, setShowModal] = useState(false);

  return (
    <div className="App">
      <Suspense fallback={null}>
        <Header />
        <InfoTable />
        <ButtonModal
          onClick={() => {
            setShowModal(true);
          }}
        >
          올림픽 사진 보기
        </ButtonModal>
        <SurveyChart />
        <Footer />
        {showModal ? (
          <LazyImageModal
            closeModal={() => {
              setShowModal(false);
            }}
          />
        ) : null}
      </Suspense>
    </div>
  );
}

const ButtonModal = styled.button`
  border-radius: 30px;
  border: 1px solid #999;
  padding: 12px 30px;
  background: none;
  font-size: 1.1em;
  color: #555;
  outline: none;
  cursor: pointer;
`;

export default App;

이렇게 하면 번들 파일에 포함되었던 ImageModal 컴포넌트와 그 안에서 사용되고 있는 react-image-gallery 라이브러리가 청크 파일에서 분리된다.

 

또한, ImageModal이 로드되기 전에 발생하는 에러를 방지하기 위해 Suspense 컴포넌트로 LazyImageModal 컴포넌트를 감싸줘야 한다.

이렇게 하면 처음 ImageModal 컴포넌트가 완전히 로드되지 않은 상태에서는 fallback에 넣어준 null로 렌더링되고 로드가 완료되면 제대로 된 모달이 렌더링될 것이다.

 

첫 페이지를 로드했을 때는 2.chunk.js, 3.chunk.js 파일이 로드되지 않고, 올림픽 사진 보기 버튼 클릭 시 모달이 뜨면서 두 파일이 로드된다.

이 두 파일이 바로 ImageModal 컴포넌트와 react-image-gallery 라이브러리 파일이다.

 

아래 사진은 cra-bundle-analyzer로 다시 분석해본 것이다.

결과를 확인해보니 파란색 블록으로 react-image-gallery 라이브러리가 분리되어 있고 아래 하늘색 블록으로 ImageModal 컴포넌트가 분리되어 있다.

 

react-image-gallery만 분할될 것이라고 예상했었는데, react-image-gallery가 참조하고 있는 모든 라이브러리가 함께 묶여 분할되었다.

지금은 이 모달에 많은 콘텐츠나 라이브러리가 들어가지 않아 성능이 크게 다르게 느껴지지 않을 수 있지만 더 많아진다면 지금 적용한 컴포넌트 지연 로딩이 꽤 의미 있을 것이다.

 

컴포넌트 사전 로딩

지연 로딩의 단점

앞 장에서는 컴포넌트 지연 로딩 기법을 적용해 봤다.

이 기법을 적용하면 최초 페이지를 로드할 때 당장 필요 없는 모달과 관련된 코드가 번들에 포함되지 않아서 로드할 파일의 크기가 작아지고 초기 로딩 속도나 자바스크립트의 실행 타이밍이 빨라져서 화면이 더 빨리 표시된다는 장점이 있다.

 

하지만, 이 기법은 초기 화면 로딩 시에는 효과적이지만 모달을 띄우는 시점에는 한계가 있다.

모달 코드를 분리해서 모달을 띄울 때 네트워크를 통해 모달 코드를 새로 로드해야 하고 로드가 완료되어야만 모달을 띄울 수 있기 때문이다.

즉, 모달이 뜨기까지 약간의 지연이 발생할 수 있다.

 

이 문제는 사전 로딩 기법을 이용하면 해결할 수 있는데, 사전 로딩이란 나중에 필요한 모듈을 필요해지기 전에 미리 로드하는 기법이다.

모달 코드가 필요한 시점은 사용자가 버튼을 클릭하는 시점이다.

사용자가 버튼을 클릭하기 전미리 모달 코드를 로드해두면, 네트워크를 통해 코드를 불러오는 시간과 준비하는데 드는 시간을 단축할 수 있어 빠르게 모달을 띄울 수 있을 것이다.

 

하지만 이 방법에는 한 가지 문제가 있는데, 사용자가 언제 버튼을 클릭할지 모르니 모달 코드를 언제 미리 로드해둘지 정하기 어렵다는 것이다.

여기서 고려할 수 있는 타이밍이 두 가지 있는데, 하나는 사용자가 버튼 위에 마우스를 올려놨을 때(mouseenter)이고, 다른 하나는 최초에 페이지가 로드되고 모든 컴포넌트의 마운트가 끝났을 때이다.

 

컴포넌트 사전 로딩 타이밍

버튼 위에 마우스를 올려놨을 때 사전 로딩

버튼을 클릭하기 위해서는 선행적으로 마우스를 버튼 위에 올려두어야 한다.

그래서 마우스가 버튼에 올라오면 사용자가 버튼을 클릭해서 모달을 띄울 것이라고 예측할 수 있다.

아직 버튼을 클릭하지는 않았지만 곧 클릭할 것이기 때문에 모달 컴포넌트를 미리 로드해두는 것이다.

 

리액트에서 마우스가 버튼에 올라왔는지 아닌지는 Button 컴포넌트의 onMouseEnter 이벤트를 통해서 알 수 있다.

이 이벤트에서 ImageModal 컴포넌트를 import해서 로드하면 된다.

이렇게 하면 모달 코드가 필요한 시점보다 전인 마우스가 버튼에 올라온 시점에 ImageModal을 로드할 수 있다.

import React, { Suspense, lazy, useState } from "react";
import styled from "styled-components";
import Header from "./components/Header";
import InfoTable from "./components/InfoTable";
import SurveyChart from "./components/SurveyChart";
import Footer from "./components/Footer";

const LazyImageModal = lazy(() => import("./components/ImageModal"));

function App() {
  const [showModal, setShowModal] = useState(false);

  const handleMouseEnter = () => {
    const component = import("./components/ImageModal");
  }

  return (
    <div className="App">
      <Suspense fallback={null}>
        <Header />
        <InfoTable />
        <ButtonModal
          onClick={() => {
            setShowModal(true);
          }}
          onMouseEnter={handleMouseEnter}
        >
          올림픽 사진 보기
        </ButtonModal>
        <SurveyChart />
        <Footer />
        {showModal ? (
          <LazyImageModal
            closeModal={() => {
              setShowModal(false);
            }}
          />
        ) : null}
      </Suspense>
    </div>
  );
}

const ButtonModal = styled.button`
  border-radius: 30px;
  border: 1px solid #999;
  padding: 12px 30px;
  background: none;
  font-size: 1.1em;
  color: #555;
  outline: none;
  cursor: pointer;
`;

export default App;

 

컴포넌트 마운트 완료 후 사전 로딩

모달 컴포넌트의 크기가 커서 로드하는데 1초 또는 그 이상의 시간이 필요할 수도 있다.

이런 경우에는 마우스 커서를 버튼에 올렸을 때보다 더 먼저 파일을 로드해야 한다.

 

이때 생각해볼 수 있는 타이밍은 모든 컴포넌트의 마운트가 완료된 후로, 브라우저에 여유가 생겼을 때 뒤이어 모달을 추가로 로드하는 것이다.

useEffect 훅을 사용하면 된다.

import React, { Suspense, lazy, useEffect, useState } from "react";
import styled from "styled-components";
import Header from "./components/Header";
import InfoTable from "./components/InfoTable";
import SurveyChart from "./components/SurveyChart";
import Footer from "./components/Footer";

const LazyImageModal = lazy(() => import("./components/ImageModal"));

function App() {
  const [showModal, setShowModal] = useState(false);

  useEffect(() => {
    const component = import('./components/ImageModal');
  }, []);

  return (
    <div className="App">
      <Suspense fallback={null}>
        <Header />
        <InfoTable />
        <ButtonModal
          onClick={() => {
            setShowModal(true);
          }}
        >
          올림픽 사진 보기
        </ButtonModal>
        <SurveyChart />
        <Footer />
        {showModal ? (
          <LazyImageModal
            closeModal={() => {
              setShowModal(false);
            }}
          />
        ) : null}
      </Suspense>
    </div>
  );
}

const ButtonModal = styled.button`
  border-radius: 30px;
  border: 1px solid #999;
  padding: 12px 30px;
  background: none;
  font-size: 1.1em;
  color: #555;
  outline: none;
  cursor: pointer;
`;

export default App;

초기 페이지 로드에 필요한 파일을 우선 다운로드 후 페이지 로드가 완료된 후에 모달 코드를 다운로드한다.

어느 타이밍에 사전 로드하는 것이 해당 서비스에서 가장 합리적인지 판단하는 것이 가장 중요하다.

 

이미지 사전 로딩

느린 이미지 로딩

이번에는 컴포넌트가 아니라 이미지를 사전 로드해보자.

이미지의 사이즈가 크면 제때 뜨지 않는 현상이 생기기도 한다.

이 현상은 웹 개발을 할 때 흔히 발생하는 현상이라 다양한 해결 방법이 있는데, 여기서는 이미지가 화면에 제때 뜰 수 있도록 미리 다운로드하는 기법인 이미지 사전 로딩 기법을 적용해볼 것이다.

 

이미지 사전 로딩

컴포넌트는 import 함수를 이용해서 로드했는데, 이미지는 이미지가 화면에 그려지는 시점, 즉 HTML 또는 CSS에서 이미지를 사용하는 시점에 로드된다.

하지만 이런 경우 외에 자바스크립트로 이미지를 직접 로드하는 방법이 한 가지 있는데, 자바스크립트의 Image 객체를 사용하는 방법이다.

 

Image 객체는 다음과 같이 new 연산자를 이용해서 생성할 수 있다.

그런 다음 생성된 인스턴스의 src 속성에 원하는 이미지의 주소를 입력하면 해당 이미지를 로드할 수 있다.

const img = new Image();
img.src = `{이미지 주소}`;

 

여기서 사전 로드할 이미지는 모달에서 가장 먼저 보이는 이미지로 넣어줄 것이다.

코드는 모달을 사전 로드하는 타이밍인 useEffect에 넣어준다.

import React, { Suspense, lazy, useEffect, useState } from "react";
import styled from "styled-components";
import Header from "./components/Header";
import InfoTable from "./components/InfoTable";
import SurveyChart from "./components/SurveyChart";
import Footer from "./components/Footer";

const LazyImageModal = lazy(() => import("./components/ImageModal"));

function App() {
  const [showModal, setShowModal] = useState(false);

  useEffect(() => {
    const component = import('./components/ImageModal');

    const img = new Image();
    img.src = 'https://stillmed.olympic.org/media/Photos/2016/08/20/part-1/20-08-2016-Football-Men-01.jpg?interpolation=lanczos-none&resize=*:800';
  }, []);

  return (
    <div className="App">
      <Suspense fallback={null}>
        <Header />
        <InfoTable />
        <ButtonModal
          onClick={() => {
            setShowModal(true);
          }}
        >
          올림픽 사진 보기
        </ButtonModal>
        <SurveyChart />
        <Footer />
        {showModal ? (
          <LazyImageModal
            closeModal={() => {
              setShowModal(false);
            }}
          />
        ) : null}
      </Suspense>
    </div>
  );
}

const ButtonModal = styled.button`
  border-radius: 30px;
  border: 1px solid #999;
  padding: 12px 30px;
  background: none;
  font-size: 1.1em;
  color: #555;
  outline: none;
  cursor: pointer;
`;

export default App;

이렇게 코드를 작성해주면 모달 코드와 함께 이미지가 다운로드되는 것을 볼 수 있다.

 

여기서 추가로 할 고민이 있는데, 몇 장의 이미지까지 사전 로드해둘 것인가이다.

모달의 첫 화면으로 보이는 이미지는 대표 이미지뿐만 아니라 하단 썸네일 이미지도 있다.

 

썸네일 이미지까지 사전 로딩할 수 있기는 하지만 그렇게 하면 페이지가 로드될 때, 즉 사전 로딩을 하는 순간 브라우저의 리소스를 그만큼 많이 사용해서 성능 문제가 생길 수 있다.