본문 바로가기

Frontend/성능 최적화

[이미지 갤러리 레이아웃 이동 피하기, 이미지 지연 로딩] 웹 성능 최적화까지 해보자-15

레이아웃 이동 피하기

레이아웃 이동이란?

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

 

이미지 갤러리 서비스를 새로고침해보면 레이아웃 이동 현상을 볼 수 있다.

이미지가 로드될 때 아래 이미지보다 늦게 로드되는 경우, 뒤늦게 아래 이미지를 밀어내면서 화면에 그려진다.

 

Lighthouse에서는 웹 페이지에서 레이아웃 이동이 얼마나 발생하는지를 나타내는 지표로 CLS(Cumulative Layout Shift)라는 항목을 두고 성능 점수에 포함했다.

CLS는 0부터 1까지의 값을 가지고, 레이아웃 이동이 전혀 발생하지 않은 상태를 0, 그 반대를 1로 계산한다.

또한 권장하는 점수는 0.1 이하이다.

 

Performance 패널을 좀 더 살펴보면 Layout Shift라는 막대가 표시된다.

이는 해당 시간에 레이아웃 이동이 발생했다는 의미이다.

 

레이아웃 이동의 원인

레이아웃 이동을 발생시키는 흔한 경우를 한 번 알아보자.

  • 사이즈가 미리 정의되지 않은 이미지 요소
  • 사이즈가 미리 정의되지 않은 광고 요소
  • 동적으로 삽입된 콘텐츠
  • 웹 폰트 (FOIT, FOUT)

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

브라우저는 이미지를 다운로드하기 전까지 이미지 사이즈가 어떤지 알 수 없기 때문에 미리 해당 영역을 확보할 수 없다.

이미지나 광고가 화면에 표시되기 전까지는 해당 영역의 높이 또는 너비가 0이다.

그러다가 이미지가 로드되면 높이가 해당 이미지의 높이로 변경되면서 그만큼 다른 요소들을 밀어내는 것이다.

 

레이아웃 이동 해결

이 문제는 레이아웃 이동을 일으키는 요소의 사이즈를 지정하면 된다.

해당 요소의 사이즈를 미리 예측할 수 있다면 또는 이미 알고 있다면 공간을 미리 확보해놓으면 된다.

이미지 갤러리의 이미지 사이즈는 브라우저의 가로 사이즈에 따라 변하는데, 단순히 너비와 높이를 고정하는 것이 아니라 이미지의 너비, 높이 비율로 공간을 잡아두면 된다.

이미지 리스트에서 사용하는 이미지 비율은 16:9이다.

 

다음과 같이 padding과 absolute 속성을 이용해서 코드를 수정해주면 된다.

import React from 'react';
import styled from 'styled-components';
import { useDispatch } from 'react-redux';
import { showModal } from '../redux/imageModal';

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

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

  return (
    <ImageWrap>
      <Image src={urls.small + '&t=' + new Date().getTime()} alt={alt} onClick={openModal} />
    </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;

새로고침 후 확인해보면 이미지가 밀리는 현상 없이 고정적인 위치에서 이미지가 렌더링되는 것을 볼 수 있다.

 

이제 Lighthouse와 Performance 패널에서도 확인해보자.

확인해보면 Lighthouse의 CLS가 거의 0에 가까운 것을 볼 수 있다.

 

이미지 지연 로딩

이번에는 Intersection Observer API가 아닌 react-lazyload라는 라이브러리를 이용해서 빠르게 이미지 지연 로딩을 적용해볼 것이다.

아래와 같이 지연 로드하고자 하는 컴포넌트를 감싸 주면 된다.

LazyLoad의 자식으로 들어간 요소들은 화면에 표시되기 전까지는 렌더링되지 않다가 스크롤을 통해 화면에 들어오는 순간 로드된다.

또한, 이미지뿐 아니라 일반 컴포넌트도 지연 로드할 수 있다.

import LazyLoad from 'react-lazyload';

function Component() {
	return (
		<div>
			<LazyLoad>
				<img src="이미지 주소" />
			</LazyLoad>
		</div>
	);
}

 

지연 로딩을 이제 한 번 적용해보자.

이미지를 포함하고 있는 PhotoItem 컴포넌트에 적용하면 된다.

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

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

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

  return (
    <ImageWrap>
      <LazyLoad>
        <Image src={urls.small + '&t=' + new Date().getTime()} alt={alt} 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;

이렇게 하니까 이미지 지연 로드는 잘 되지만, 스크롤을 내려 화면에 이미지가 들어올 때 이미지를 로드해서 처음에는 이미지가 보이지 않고 시간이 지나야 이미지가 보이게 된다.

 

이 문제를 해결하기 위해서 이미지가 화면에 들어오는 시점보다 조금 더 미리 이미지를 불러와 화면에 들어온 시점에는 이미지가 준비되어 있도록 해야 한다.

이는 react-lazyload 라이브러리에서 제공하는 offset 옵션으로 할 수 있다.

offset을 100으로 설정하면 화면에 들어오기 100px 전에 이미지를 로드할 수 있다.

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

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

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

  return (
    <ImageWrap>
      <LazyLoad offset={1000}>
        <Image src={urls.small + '&t=' + new Date().getTime()} alt={alt} 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;