본문 바로가기

Frontend/성능 최적화

[이미지 갤러리 병목 코드 최적화] 웹 성능 최적화까지 해보자-17

병목 코드 최적화

이미지 모달 분석

병목 코드를 찾기 위해서 Performance 패널을 이용해보자.

서비스 이용 과정에서 느리거나 문제가 있다고 판단되는 부분을 찾아서 검사해볼 것이다.

 

이미지 갤러리 서비스에서는 페이지가 최초로 로드될 때, 카테고리를 변경했을 때, 그리고 이미지 모달을 띄웠을 때로 크게 3가지를 알아보면 된다.

이미지를 클릭해서 이미지 모달을 띄웠을 때는 이미지도 늦게 뜨고 배경 색도 늦게 변하는 것을 볼 수 있다.

모달이 뜨는 과정에서 메인 스레드의 작업을 확인하려면, 화면이 완전히 로드된 상태로 Performance 패널의 기록 버튼을 클릭하면 된다.

그리고 이미지를 클릭해서 모달을 띄운 뒤 기록 버튼을 다시 누르면 기록이 종료된다.

페이지에서 이미지 클릭을 하면 이미지 모달이 뜬다.

이후, 모달 안의 이미지를 로드해야 하기 때문에 Network 섹션에서 이미지가 다운로드된다.

이미지가 모두 다운로드되면 getAverageColorOfImage 함수가 실행된다.

작업의 제일 마지막을 보면 Image Decode 작업이 보이는데, 여기서 이미지에 관한 처리 작업을 하고 있다.

이 작업은 drawImage 함수의 하위 작업이다.

이후, 새롭게 렌더링되면서 변경된 배경화면이 보이게 된다.

 

getAverageColorOfImage 함수 분석

getAverageColorOfImage 함수는 이미지의 평균 픽셀 값을 계산하는 함수로 캔버스에 이미지를 올리고 픽셀 정보를 불러온 뒤 하나씩 더해서 평균을 내고 있다.

즉, 큰 이미지를 통째로 캔버스에 올린다는 점반복문을 통해 가져온 픽셀 정보를 하나하나 더하고 있다는 점에서 느린 것이다.

 

두 방법으로 최적화해보자.

하나는 메모이제이션을 적용하는 방법이고, 다른 하나는 함수 자체의 로직을 개선하는 방법이다.

 

메모이제이션으로 코드 최적화하기

메모이제이션이란 한 번 실행된 함수에 대해 해당 반환 값을 기억해두고 있다가 똑같은 조건으로 실행됐을 때 함수의 코드를 모두 실행하지 않고 바로 전에 기억해둔 값을 반환하는 것이다.

 

조건이라는 것은 인자 값을 의미한다.

getAverageColorOfImage 함수에 적용해보자.

여기서 주의할 점은 인자 값이 문자열이나 숫자 형태가 아니라 객체 형태라는 점인데, 그래서 인자를 그대로 cache의 Key로 사용하는 것이 아니라 인자 객체가 가지고 있는 고유의 값인 src 값을 키로 사용해야 한다.

const cache = {};

export function getAverageColorOfImage(imgElement) {
  if (cache.hasOwnProperty(imgElement.src)) {
    return cache[imgElement.src];
  }

  const canvas = document.createElement('canvas');
  const context = canvas.getContext && canvas.getContext('2d');
  const averageColor = {
    r: 0,
    g: 0,
    b: 0,
  };

  if (!context) {
    return averageColor;
  }

  const width = (canvas.width =
    imgElement.naturalWidth || imgElement.offsetWidth || imgElement.width);
  const height = (canvas.height =
    imgElement.naturalHeight || imgElement.offsetHeight || imgElement.height);

  context.drawImage(imgElement, 0, 0);

  const imageData = context.getImageData(0, 0, width, height).data;
  const length = imageData.length;

  for (let i = 0; i < length; i += 4) {
    averageColor.r += imageData[i];
    averageColor.g += imageData[i + 1];
    averageColor.b += imageData[i + 2];
  }

  const count = length / 4;
  averageColor.r = ~~(averageColor.r / count); // ~~ => convert to int
  averageColor.g = ~~(averageColor.g / count);
  averageColor.b = ~~(averageColor.b / count);

  cache[imgElement.src] = averageColor;

  return averageColor;
}

이렇게 하면 하나의 이미지에 대해서는 처음 동작 이후 배경 색이 바로바로 적용되는 것을 볼 수 있다.

 

하지만 메모이제이션은 첫 번째 실행에 있어서는 여전히 느리다.

항상 새로운 인자가 들어오는 함수는 메모이제이션을 적용해도 재활용할 수 있는 조건이 충족되지 않기 때문에 메모리만 잡아먹는다.

메모이제이션을 적용하기 전에 해당 로직이 동일한 조건에서 충분히 반복 실행되는지 먼저 체크해야 한다.

 

함수의 로직 개선

이번에는 첫 번째 실행 시간도 단축될 수 있도록 함수의 로직 자체를 수정해볼 것이다.

 

이 함수에서 느린 코드는 캔버스에 이미지를 올리고 픽셀 정보를 불러오는 drawImage와 getImageData 함수, 모든 픽셀에 대해 실행되는 반복문이다.

 

이미지가 작으면 더 빠르게 처리될 것이다.

그래서 이미지를 작은 사이즈의 이미지로 교체하는 방법을 고려해볼 수 있다.

지금은 원본 이미지로 배경 색을 계산하고 있지만 썸네일 이미지로 배경 색을 계산한다면 작업량이 많이 단축될 것이다.

그리고 원본 이미지로 계산할 때는 원본 이미지가 모두 다운로드된 후에야 배경 색 계산이 가능했지만 썸네일 이미지를 사용하면 원본 이미지가 다운로드되기 전에 계산할 수 있어 더 빠르게 배경 색을 적용할 수 있다.

{
      "id": "gok_MXg2Ntk",
      "alt": null,
      "urls": {
        "small": "<https://images.unsplash.com/photo-1580893472468-01373fe4c97e?ixlib=rb-1.2.1&q=100&fm=jpg&cs=tinysrgb&w=800&h=450&fit=crop&ixid=eyJhcHBfaWQiOjExMzQyMX0>",
        "full": "<https://images.unsplash.com/photo-1580893472468-01373fe4c97e?ixlib=rb-1.2.1&q=85&fm=jpg&cs=srgb&ixid=eyJhcHBfaWQiOjExMzQyMX0>"
      },
      "category": "random"
    },
    {
      "id": "Smr11JAjVhU",
      "alt": null,
      "urls": {
        "small": "<https://images.unsplash.com/photo-1580873859752-74f92e2e0990?ixlib=rb-1.2.1&q=100&fm=jpg&cs=tinysrgb&w=800&h=450&fit=crop&ixid=eyJhcHBfaWQiOjExMzQyMX0>",
        "full": "<https://images.unsplash.com/photo-1580873859752-74f92e2e0990?ixlib=rb-1.2.1&q=85&fm=jpg&cs=srgb&ixid=eyJhcHBfaWQiOjExMzQyMX0>"
      },
      "category": "random"
    },

 

ImageModal 컴포넌트에서는 썸네일 이미지의 요소를 가져올 수 없으니 썸네일 이미지의 요소를 직접 넘겨줄 수 있는 PhotoItem 컴포넌트에서 배경 색을 계산하도록 수정해보자.

import React from 'react';
import styled from 'styled-components';
import { useDispatch } from 'react-redux';
import { setBgColor, showModal } from '../redux/imageModal';
import LazyLoad from 'react-lazyload';
import { getAverageColorOfImage } from '../utils/getAverageColorOfImage';

function PhotoItem({ photo: { urls, alt } }) {
  const dispatch = useDispatch();

  const openModal = e => {
    dispatch(showModal({ src: urls.full, alt }));

    const averageColor = getAverageColorOfImage(e.target);
    dispatch(setBgColor(averageColor));
  };

  return (
    <ImageWrap>
      <LazyLoad offset={1000}>
        <Image
          src={urls.small + '&t=' + new Date().getTime()}
          alt={alt}
          crossOrigin={'*'}
          onClick={openModal}
        />
      </LazyLoad>
    </ImageWrap>
  );
}

const ImageWrap = styled.div`
  width: 100%;
  padding-bottom: 56.25%;
  position: relative;
`;

const Image = styled.img`
  cursor: pointer;
  width: 100%;
  position: absolute;
  height: 100%;
  top: 0;
  left: 0;
`;

export default PhotoItem;
import React from 'react';
import styled from 'styled-components';
import Modal from './Modal';
import { useDispatch } from 'react-redux';
import { hideModal } from '../redux/imageModal';

function ImageModal({ modalVisible, src, alt, bgColor }) {
  const dispatch = useDispatch();

  const closeModal = () => {
    dispatch(hideModal());
  };

  return (
    <Modal
      modalVisible={modalVisible}
      closeModal={closeModal}
      bgColor={bgColor}
    >
      <ImageWrap>
        <FullImage crossOrigin="*" src={src} alt={alt} />
      </ImageWrap>
    </Modal>
  );
}

const ImageWrap = styled.div`
  width: 100%;
  height: 100%;
`;
const FullImage = styled.img`
  max-width: 100vw;
  max-height: 75vh;
  box-shadow: 0px 0px 16px 4px rgba(0, 0, 0, 0.3);
`;

export default ImageModal;

이미지가 클릭된 시점인 openModal 함수에서 이미지 요소를 getAverageColorOfImage 함수로 넘겨주고 배경 색을 설정한다.

또한 ImageModal에서 배경 색을 설정하는 코드인 onLoad 코드를 제거해준다.

 

확인해보면 이전과 달리 작업이 매우 빠르게 완료된 것을 볼 수 있다.