성능최적화 - 올림픽 통계 서비스 최적화

두밥비·2025년 5월 16일

article

목록 보기
13/23
post-thumbnail

목차


  1. 개요
  2. CSS 애니메이션 최적화
  3. 컴포넌트 지연로딩
  4. 컴포넌트 사전로딩
  5. 이미지 사전 로딩

개요

실습 서비스 설명

리우 올림픽과 런던 올림픽 정보와 사진을 비교하여 보여 주고, 하단에는 그에 대한 설문 조사 결과를 보여줍니다. 여기에서 설문조사 결과는 막대 그래프로 나타나는데, 항목을 클릭하면 해당 항목에 대해 필터링되어 그래프가 애니메이션과 함께 변합니다.

🤔 버벅거리는 애니메이션… 왜일까…?

🤔 첫 로딩 시 깨져 있는 모달… 왜일까…?

✔️ 어떻게 해결할 수 있는지 알아봅시다!

학습할 최적화 기법

  1. CSS 애니메이션 최적화
  2. 컴포넌트 지연로딩
  3. 컴포넌트 사전로딩
  4. 이미지 사전 로딩

개념 소개

컴포넌트 지연 로딩

1장에서 학습한 코드 분할 기법이랑 유사하지만 아주 살짝 다릅니다.

이번에는 페이지 코드 자체를 분할하는 대신, 단일 컴포넌트를 분할해서 컴포넌트가 쓰이는 순간에 불러오도록 만들어 봅시다!!

컴포넌트 사전 로딩

컴포넌트 코드를 분할하여 지연 로딩을 적용하면, 첫 화면 진입 시 분할된 코드 중 당장 필요한 코드만 다운로드하기 때문에 첫 화면을 더 빠르게 그릴 수 있게 됩니다. 하지만 서비스 이용 과정에서 분할된 컴포넌트를 사용하려고 할 때, 다운로드되어 있지 않은 코드를 추가로 다운로드하는 시간만큼 서비스 이용에 지연이 발생합니다.
위와 같은 문제를 어떻게 해결할 수 있을까요? 바로 컴포넌트 사전 로딩 기법을 활용하면 됩니다!

코드를 분할하여 첫 화면 진입 시에는 다운로드하지 않지만, 이후 해당 코드가 필요한 시점보다는 먼저 코드를 로드하여 해당 코드를 지연 없이 사용할 수 있도록 하는 컴포넌트 사전 로딩 기법을 살펴 보겠습니다.

이미지 사전 로딩
이미지도 컴포넌트처럼 이미지를 필요한 시점에 로드하면 이미지가 로드되는 시간만큼 기다려야 합니다. 그래서 이미지도 필요한 시점보다 먼저 다운로드해 두고, 필요할 때 바로 이미지를 보여 줄 수 있도록 하는 이미지 사전 로딩 기법을 적용해 보겠습니다.

분석 툴

  • 크롬 Network 패널
  • 크롬 Performance 패널
  • Webpack Bundle Analyzer

실습 코드

깃허브 링크

CSS 애니메이션 최적화

애니메이션의 원리부터 이해해 봅시다!!

애니메이션의 원리

여러 장의 이미지를 빠르게 전환하여 우리 눈에 잔상을 남기고, 그로 인해 연속된 이미지가 움직이는 것처럼 느껴지게 하는 것

❓그런데 만약에 연속된 이미지들 중에 한 장이 빠진다면?

=> 2번에서 4번으로 넘어갈 때 어색한 느낌이 들 것이다!

일반적으로 사용하는 디스플레이의 주사율 - 60Hz
→ 1초에 60장의 정지된 화면을 빠르게 보여 줌
→ 브라우저도 동일하게 최대 60FPS로 1초에 60장의 화면을 새로 그림

끊김 현상(jank)가 발생하는 원인 - 브라우저가 초당 60장의 화면을 그리지 못했기 때문

초당 60장의 화면을 그리지 못하는 이유를 알아보기 위해 브라우저 렌더링 과정을 다시 살펴봅시다!


브라우저의 렌더링 과정

DOM+CSSOM렌더트리레이아웃페인트컴포지트DOM+CSSOM → 렌더트리 → 레이아웃 → 페인트 → 컴포지트

렌더링 과정을 통해 화면이 전부 그려진 후, 일부 요소의 스타일을 변경/추가/제거한다면 주요 렌더링 경로에서 거친 과정을 다시 한 번 실행하면서 새로운 화면을 그리게 됩니다. 이렇게 새로운 화면을 그리는 것을 리플로우 혹은 리페인트라고 합니다.


1. 리플로우

DOM+CSSOM렌더트리레이아웃페인트컴포지트DOM+CSSOM → 렌더트리 → 레이아웃 → 페인트 → 컴포지트
  • 리플로우의 경우 렌더링 경로의 모든 단계를 재실행하므로 리소스를 많이 사용한다.

2. 리페인트

DOM+CSSOM렌더트리페인트컴포지트DOM+CSSOM → 렌더트리 → 페인트 → 컴포지트
  • 리페인트의 경우 CSSOM을 새로 생성하지만 레이아웃 단계는 실행하지 않아 비교적 괜찮다.

하드웨어 가속(GPU 가속)

특정 작업을 CPU가 아닌 GPU에 위임함으로써 성능을 향상시키는 기술


리플로우와 리페인트를 피하는 방법으로, transform, opacity 같은 속성을 사용하여, 해당 요소를 별도의 레이어로 분리하고 작업을 GPU에 위임하여 처리함으로써 레이아웃 단계와 페인트 단계를 건너뛸 수 있다!!

GPU는 애초에 그래픽 작업을 처리하기 위해 만들어진 것이기 때문에 화면을 그릴 때 활용하면 매우 빠릅니다.


🐢 width 의 변경으로 인한 리플로우 발생

const BarGraph = styled.div`
  position: absolute;
  left: 0;
  top: 0;
  width: ${({ width }) => width}%;
  transition: width 1.5s ease;
  height: 100%;
  background: ${({ isSelected }) =>
    isSelected ? "rgba(126, 198, 81, 0.7)" : "rgb(198, 198, 198)"};
  z-index: 1;
`;

🐇 transform 으로 변경하여 최적화

미리 막대의 너비를 100%로 채워 두고 scale을 이용하여 비율에 따라 줄이는 방식을 활용하면 아래와 같이 성능을 개선할 수 있습니다.

책에서 사용한 애니메이션 효과가 적용된 막대 그래프 컴포넌트

const BarGraph = styled.div`
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  transform: scaleX(
    ${({ width }) => width / 100}
  ); // 여기서 직접 width를 쓰는게아니라, scaleX함수를 통해 화면에 표현되는 비율을 수정
  transform-origin: center left;
  transition: transform 1.5s ease;
  height: 100%;
  background: ${({ isSelected }) =>
    isSelected ? "rgba(126, 198, 81, 0.7)" : "rgb(198, 198, 198)"};
  z-index: 1;
`;

기존에는 쟁크 현상(살짝 끊김)이 발생했지만, 하드웨어 가속 기법을 통해 문제 해결 성공!

  • 추가 설명
    • “transform, opacity” 같은 속성을 사용해 해당 요소를 별도의 레이아웃으로 분리하고 이 작업을 GPU에게 위임하여 레이아웃 및 페인트 단계를 건너뛸수있다.
    • 요소의 실제 크기나 위치가 변경되는 것이 아니라, 화면에 표시되는 방식만 변경되기 때문에 브라우저는 transform(translate, scale, rotate) 속성을 변경할 때 다른 요소들에 대한 레이아웃 계산을 다시 할 필요가 없기 때문
    • Transform: translate, scale, rotate와 같은 변형 속성은 요소의 레이아웃에 영향을 주지 않으며, 오직 시각적 표현만 변경. 따라서 이러한 변형을 적용할 때 다른 요소의 레이아웃을 다시 계산할 필요가 없다.
    • Opacity: 불투명도 변경 역시 레이아웃에 영향을 주지 않고, 별도의 레이어를 생성하여 GPU에서 처리

이러한 방식은 렌더트리 생성, 레이아웃 계산, 페인트 등의 복잡한 과정을 건너뛰게 해 성능을 크게 향상시킬 수 있다고 합니다.


❓그러면 transform은 무조건 사용하는 게 좋을까요?

아닙니다!!

  • 움직임/애니메이션 - transform 사용 (성능 개선 가능) 권장
  • 고정 크기/배치 조절- width, margin 등 기본 속성 사용 권장
  1. 애니메이션에 적용

transform은 주로 애니메이션이나 전환 효과에 사용되며, 이 경우에는 레이아웃 단계와 페인팅 단계를 건너뛸 수 있어 성능 이점이 있음.

그러나 정적인 요소의 너비를 설정하는 데 transform을 사용하는 것은 불필요할 수 있으며, 코드의 가독성을 저하시킬 수 있다.

  1. 레이아웃에 영향

transform 속성은 요소의 시각적인 모양만 바꾸며, 실제 레이아웃에는 영향을 주지 않기때문에 이는 레이아웃을 예상한 대로 조절하기 어렵게 만들 수 있다.

위 Bar컴포넌트처럼 정해진 크기 + 애니메이션처리에 대해서는 직접 width를 조절하기보다 transform을 사용하자.


컴포넌트 지연로딩

올림픽 통계 서비스의 번들 분석 결과

위의 이미지에서 Image-gallery.js 라이브러리는 모달창이 띄워질 때만 필요합니다. 따라서 첫 화면부터 필요하지 않습니다.

기존코드 : ImageModal을 App.js에서 바로 import해서 사용

-> Image-gallery.js 라이브러리를 처음부터 가져옴

import ImageModal from './components/ImageModal'

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

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

수정 코드: ImageModal 컴포넌트 로드가 완료되면 그때 제대로 된 모달이 렌더링되도록

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

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

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

위의 번들 분석 결과를 통해 기존과 다르게 분리되었다는 것을 확인할 수 있습니다.

즉 showModal이 true가 되는 시점에 동적으로 라이브러리를 호출하도록 수정하게 되었다.

네트워크 탭에서도 [올림픽 사진 보기] 버튼을 누른 시점에 청크 파일 두 개가 로드되는 것을 볼 수 있습니다. 두 개의 청크 파일이 바로 ImageModal 컴포넌트와 react-image-gallery 라이브러리 파일입니다.

지연 로딩의 문제점

그러나 지연로딩을 사용하면, 분리했던 컴포넌트를 호출하는 시점에 데이터를 받아오기때문에 약간의 지연이 발생한다는 단점이 있습니다.

→ 해당 단점은 컴포넌트 사전 로딩으로 해결 가능


컴포넌트 사전로딩

로딩 시점
1. 사용자가 누르기 직전 (마우스를 올려두었을때)
2. 최초에 페이지가 로드되고 컴포넌트 마운트가 끝났을때

버튼을 클릭하기 위해서는 먼저 마우스를 버튼 위에 올려 두어야 한다는 점을 이용하여 마우스가 버튼에 올라오면 아직 버튼을 클릭하지는 않았지만, 곧 클릭할 것으로 예측하고 모달 컴포넌트를 미리 로드하는 방식

목적

  • 사용자가 버튼에 마우스를 올리기만 해도
    → ImageModal 컴포넌트를 백그라운드에서 미리 불러옴
  • 실제로 클릭해서 열 때는
    → 이미 로딩이 끝난 상태이므로 훨씬 빠르게 뜸
const handleMouseEnter = () => {
    import("./components/ImageModal").catch();
    // 마우스를 올리는 순간 ImageModal을 백그라운드에서 불러옴
    // (브라우저는 같은 모듈이면 네트워크 요청을 반복하지 않고 캐시를 사용함)

    // import()는 동적으로 모듈을 불러오는 함수
    // 여기서 미리 불러놓으면, 나중에 실제로 쓸 때 빠르게 보여줄 수 있습니다.
  };

  return (
    <div className="App">
      <Header />
      <InfoTable />
      <ButtonModal
        onMouseEnter={handleMouseEnter} // hover시 미리 ImageModal 컴포넌트를 사전에 로딩한다.
        onClick={() => {
          setShowModal(true);
        }}
      >
        올림픽 사진 보기
      </ButtonModal>
      // 마우스를 버튼 위에 올리면 handleMouseEnter가 실행되어 ImageModal을 사전 로딩합니다.
      // 버튼을 클릭하면 setShowModal(true)가 실행되어 모달이 열립니다.
      <SurveyChart />
      <Footer />
      <Suspense fallback={null}>
        {showModal ? (
          <LazyImageModal
            closeModal={() => {
              setShowModal(false);
            }}
          />
        ) : null}
      </Suspense>
useEffect(() => {
  import("./components/ImageModal").catch();
  // Hover보다 더 빠르게, 모든 컴포넌트가 마운트 완료되면 추가로 로드
}, []);

이제 모달창을 클릭한 뒤에 기다리지 않고 바로 데이터를 확인할 수 있습니다!

흐름 정리

  1. 마우스 hover - ImageModal 컴포넌트를 백그라운드에서 미리 로드
  2. 버튼 클릭 - 모달을 실제로 화면에 표시함
  3. 성능 최적화 포인트 - 클릭 전에 미리 불러오므로 로딩 지연이 없음

이미지 사전로딩

컴포넌트와 다르게 이미지는 화면에 그려지는 시점에 로드됩니다. (단순 import X)

import 함수를 이용하지 않고, HTML 또는 CSS에서 이미지를 사용하는 시점에 로드됩니다.
<img src="..." />처럼 사용되는 순간에 다운로드!!

사전로딩을 적용해 봅시다!

useEffect(() => {
  // 컴포넌트는 미리 import로 로드
  import("./components/ImageModal").catch();

  // 이미지는 이렇게 미리 객체로 만들어서 src를 지정
  const img = new Image();
  img.src = "https://...이미지경로";
}, []);

정리

  1. <img src=”…”>로 직접 사용하는 경우
    • 사용자가 버튼을 클릭하거나 모달을 열 때 그때서야 다운로드돼서 느려질 수 있다.
  2. new Image()로 미리 객체를 생성해 src 설정하는 경우
    • 화면에 보이지 않아도 즉시 백그라운드에서 이미지 로딩 시작
    • 사용자가 실제로 이미지를 보러 갔을 땐 이미 브라우저가 받아둔 상태

고민해봐야 할 점

🤔 몇 장의 이미지까지 사전 로드해 둘 것인지..

사전 로딩을 하는 순간, 브라우저의 리소스를 그만큼 많이 사용하기 때문에 다른 성능 문제를 야기할 수 있습니다. 따라서 어떤 콘텐츠를 사전 로드할 때는 정말 사전 로딩이 필요한지 고민해야 합니다.

  • 너무 많은 이미지를 사전 로딩하면 브라우저가 느려질 수 있어요.
  • 이미지 크기(용량), 개수를 고려해서 꼭 필요한 것만 사전 로딩하는 게 좋습니다.
profile
개발새발

0개의 댓글