Tech Books/프론트엔드 성능 최적화 가이드

4장 이미지 갤러리 최적화

Nomad Kim 2023. 3. 24. 07:01

Repository

 

학습할 최적화 기법

  • 이미지 지연 로딩
    • 이미지 지연 로딩 라이브러리 적용
  • 레이아웃 이동 피하기
    • 화면상의 요소 변화로 레아이웃이 밀리는 현상
  • 리덕스 렌더링 최적화
  • 병목 코드 최적화
    • 메모이제이션 적용

이미지 지연 로딩

react-lazyload라는 라이브러리를 이용하여 빠르게 이미지 지연 로딩을 적용해 볼 것입니다.

일반 컴포넌트도 이 안에 넣어 지연 로드할 수 있다는 것입니다. 

 

 

이미지가 화면에 들어오는 시점보다 조금 더 미리 이미지를 불러와 화면에 들어온 시점에는 이미지가 준비되어 있도록 해야 합니다. 

다행히 react-lazyload 라이브러리에서는 그렇게 할 수 있는 옵션을 제공해 주는데, 바로 offset이라는 옵션입니다.

이 옵션에는 얼마나 미리 이미지를 로드할지 픽셀 값으로 넣어 줍니다.

 

 


레이아웃 이동 피하기

레이아웃 이동이란 화면상의  요소 변화로 레이아웃이 갑자기 밀리는 현상을 말합니다.

 

 

Lighthouse에서는 웹 페이지에서 레이아웃 이동이 얼마나 발생하는지를 나타내는 지표로 CLS(Cumulative Layout Shift)라는 항목을 두고 성능 점수에 포함했습니다. 레이아웃 이동이 전혀 발생하지 않으면 0, 그 반대는 1 이고 권장하는 점수는 0.1 이하 입니다.

 

 

Performance panel 을 확인해보면 검사 결과의 Experience 섹션에서 Layout Shift라는 이름의 빨간 막대가 표시되는데, 이것은 해당 시간에 레이아웃 이동이 발생하였다는 의미입니다. 

 

 

레이아웃 이동의 원인

- 사이즈가 미리 정의되지 않은 이미지 요소

- 사이즈가 미리 정의되지 않은 광고 요소

- 동적으로 삽입된 콘텐츠

- 웹 폰트(FOIT, FOUT)

 

이미지 갤러리 서비스에서는 이 네 가지 중 사이즈가 미리 정의되지 않은 이미지 요소 때문에 레이아웃 이동이 발생했습니다.

 

레이아웃 이동 해결

이미지의 너비, 높이 비율로 공간을 잡아 두면 됩니다. 이미지 리스트에서 사용하는 이미지 비율은 16:9입니다.

 

 

1. padding을 이용하여 박스를 만든 뒤, 그 안에 이미지를 absolute로 띄우는 방식입니다.

 

 

2. aspect-ratio라는 CSS 속성을 이용하는 방법입니다.

 

 

aspect-ratio 속성도 브라우저의 일부 버전에서는 지원하지 않을 수 있어서 호환성을 잘 체크한 후 적용해야 합니다.

 


리덕스 렌더링 최적화

렌더링에 시간이 오래 걸리는 코드가 있거나 렌더링하지 않아도 되는 컴포넌트에서 불필요하게 리렌더링이 발생하면 메인 스레드의 리소스를 차지하여 서비스 성능에 영향을 줍니다.

 

리렌더링의 원인

리덕스 상태를 구독하고 있는 컴포넌트는 리덕스 상태 변화에 따라 불필요하게 리렌더링될 수 있습니다.
테스트 케이스의 경우, useSelector를 사용하고 있는 컴포넌트에 신호를 보냅니다. 신호를 받은 컴포넌트는 리덕스의 상태 변화에 따라 컴포넌트를 리렌더링하게 되는 것입니다.

useSelector 의 반환 값이 이전 값과 같다면 해당 컴포넌트는 리덕스 상태 변화에 영향이 없다고 판단하여 리렌더링을 하지 않고, 반환 값이 이전 값과 다르면 영향이 있다고 판단하여 리렌더링을 합니다.

 

 

객체 내부의 photos와 loading의 값을 보면 달라진 게 없어 보일 수 있지만, 

객체를 새로 만들어서 새로운 참조 값을 반환하는 형태(즉, 참조가 바뀐다.)

이므로 useSelector는 리덕스를 통해 구독한 값이 변했다고 판단합니다. 따라서 useSelector를 사용할 때 함수가 객체 형태를 반환하게 하면 매번 새로운 값으로 인지하여 상관없는 리덕스 상태 변화에도 리렌더링이 발생하는 것입니다.

 

useSelector 문제 해결

1. 객체를 새로 만들지 않도록 반환 값을 나누기

  객체로 묶어서 한 번에 반환하던 것을 단일 값으로 반환하고 있습니다. 이렇게 하면 참조 값이 바뀌는 것이 아니므로, useSelector가 반환하는 값은 다른 상태 변화에 영향을 받지 않을 것이고, 리렌더링을 발생시키지 않을 것입니다.

 

 

2. Equality Function을 사용

객체를 얕은 비교하는 함수입니다. 즉, 참조 값을 비교하는 것이 아니라 객체 내부에 있는 modalVisible, bgColor, src와 alt를 직접 비교하여 동일한지 아닌지 판단합니다. 

 

 

하지만 다른 카테고리에서 이미지 모달을 띄워 보면 여전히 모달과 상관없는 이미지 리스트가 리렌더링되는 것을 볼 수 있습니다. 

왜 그럴까요?

 

 

카테고리가 all이 아니면 filter 메소드를 통해 필터링된 이미지 리스트를 가져오는데, 이때 가져온 이미지 리스트,

즉 배열은 새롭게 만들어진 배열이기 때문에 이전에 만들어진 배열과 참조 값이 달라집니다. 이는 리렌더링을 발생시킵니다.

따라서 filter로 새로운 배열을 꺼내는 대신 state.photos.data와 state.category.category를 따로 꺼낸 후, useSelector 밖에서 필터링해야 합니다. 왼쪽에서 오른쪽으로 개선합니다.

 


병목 코드 최적화

이미지 모달 분석

이미지를 클릭해서 이미지 모달을 띄웠을 때는 이미지도 늦게 뜨고 배경 색도 늦게 변하는 것을 볼 수 있습니다. 이미지가 늦게 뜨는 것은 이미지의 사이즈 때문에 어쩔 수 없겠지만, 배경 색이 늦게 뜨는 원인은 살펴볼 필요가 있겠습니다.

 

모달이 뜨는 과정에서 메인 스레드의 작업을 확인하려면, 화면이 완전히 로드된 상태로 Performance 패널의 새로고침 버튼이 아닌 기록 버튼을 클릭합니다. 이미지를 클릭하여 모달을 띄운 뒤 기록 버튼을 다시 누르면 기록이 종료됩니다. 필요에 따라 네트워크 및 CPU에 throttling 옵션을 설정해도 됩니다.

 

 

2번에서 이미 모달이 뜨고 이미지가 로드된 다음에 실행되는 작업이 딱 하나 있음을 알고 있습니다. 바로 getAverageColorOfImage 함수입니다. 작업의 제일 마지막을 보면 Image Decode라는 작업이 보이는데요. 이 작업에서 이미지에 관한 처리 작업을 하고 있음을 알 수 있습니다. 실제로 이 Image Decode 작업은 drawImage 함수의 하위 작업입니다.

getAverageColorOfImage 함수 분석

이 함수는 이미지의 평균 픽셀 값을 계산하는 함수로 캔버스에 이미지를 올리고 픽셀 정보를 불러온 뒤 하나씩 더해서 평균을 내고 있습니다.
즉, 큰 이미지를 통째로 캔버스에 올린다는 점과 반복문을 통해 가져온 픽셀 정보를 하나하나 더하고 있다는 점에서 느린 것입니다.

 

최적화 방법엔 두가지가 있습니다. 함수에 메모이제이션을 적용하는 방법과 함수 자체의 로직을 개선하는 방법입니다.

1. 함수에 메모이제이션 기법 적용

  메모이제이션이란 한 번 실행된 함수에 대해 해당 반환 값을 기억해 두고 있다가 똑같은 조건으로 실행되었을 때 함수의 코드를 모두 실행하지 않고 바로 전에 기억해 둔 값을 반환하는 기술입니다. 조건이란 함수에서 인자 값을 의미합니다. 동일한 인자가 들어오면 결국 반환 값도 같을 테니 가능한 방법입니다.

여기서 메모이제이션을 적용할 때 주의할 점은 인자 값이 문자열이나 숫자 형태가 아니라 객체 형태라는 점입니다.
즉, 인자 객체가 가지고 있는 고유의 값인 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,
  };

  중략. averageColor 에 r,g,b 설정부분.
  // 캐시에 배경색 저장.
  cache[imgElement.src] = averageColor;

  return averageColor;
}

메모이제이션 사용의 주의점

만약 항상 새로운 인자가 들어오는 함수는 메모이제이션을 적용해도 재활용할 수 있는 조건이 충족되지 않기 때문에 오히려 메모리만 잡아먹는 골칫거리가 될 뿐입니다. 따라서 메모이제이션을 적용할 때는 해당 로직이 동일한 조건에서 충분히 반복 실행되는지 먼저 체크해야 합니다.

 

2. 함수의 로직 개선

메모이제이션의 단점인 첫 번째 실행 시간도 단축될 수 있도록 getAverageColorOfImage 함수의 로직 자체를 수정해 볼 것입니다.

이 함수에서 느린 코드는 캔버스에 이미지를 올리고 픽셀 정보를 불러오는 drawImage와 getImageData 함수, 그리고 모든 픽셀에 대해 실행되는 반복문입니다. 그리고 이 세 가지 코드는 이미지 사이즈에 따라 작업량이 결정됩니다.

즉, 이미지가 작으면 캔버스에 그리는 이미지의 사이즈도 작아져 더 빠르게 처리할 수 있으며, 픽셀 수도 줄어들어 반복문의 실행 횟수도 줄어들 것입니다.

 

섬네일 이미지로 배경 색을 계산하게 한다면 작업량이 많이 단축될 것입니다.

그리고 섬네일 이미지를 사용하면 원본 이미지가 다운로드되기 전에 계산할 수 있어 더욱 빠르게 배경 색을 적용할 수 있을 것입니다.

즉, 원본 이미지가 다운로드되지 않아도 배경 색을 설정할 수 있기 때문에 병렬적으로 배경 색이 설정됩니다.

 

 

표시된 부분이 getAverageColorOfImage의 작업인데, 이전에는 작업 시간이 매우 길었던 것과 달리 작업이 매우 빠르게 완료된 것을 볼 수 있습니다.

 


-알라딘 eBook <프론트엔드 성능 최적화 가이드> (유동균 지음) 중에서