개발일지

[이미지 렌더링: 01] 무한스크롤과 이미지 렌더링

dmdwn3979 2022. 7. 1. 21:34

타 사이트들을 돌아다니며, 크롬 개발자 도구의 Ligthouse 를 사용하여 성능 측정을 해보았다. 꽤나 많은 사이트들이 성능 점수에서 낮은 것을 확인할 수 있었고, 이에 대한 이유를 파악해보고자 하였다.

 

모 사이트의 Lighthouse 평가 점수

해당 사이트(어디인지는 비밀 ㅎ...)는 성능적으로 매우 아쉬운 점수를 보였다. 체감 상, 이 점수대의 성능에서는 웹 인터렉션이 빠르다 라는 체감을 전혀 받지 못하는 것 같다. 다소 버벅거림이 있는 성능, 주된 원인으로서 이미지라 생각했다. 해당 사이트는 다량의 이미지를 렌더링하는데, lazy loading과 같이 성능적인 설계를 전혀 고려하지 않았었다. 조금 더 근거를 얻기 위해 Lighthouse의 의견을 들어보았다.

 

이미지 인코딩과 크기, 차세대 형식 및 오프스크린 이미지 지연하기를 제외한 부분들은 모두 어느정도 규모가 있는 사이트에서는 등장할 수 있는 부분이라 생각한다. 아무래도 이미지의 화질 또한 좋아지고 있고, 이러한 이미지들은 kb에서 mb단위가 되기도 하기 때문에, 더더욱 신경 쓸 수 밖에 없을 것이다.

 

이미지를 효율적으로 렌더링하는 것을 연습해보기 위해 간단한 토이프로젝트를 만들어보고자 한다.

무한 스크롤로 이미지들을 계속 렌더링하고, 해당 이미지들의 렌더링 방식 및 이미지 인코딩마다 다른 성능을 측정해보고자 한다.

 

해당 프로젝트에서는 다음과 같은 스택을 사용할 것이다.

 

React

Typescript 아직 매우 어색하다...

markup language

 

Infinite Scroll

Custom Hook

 

파일은 https://github.com/EJKim3191/image-loading 에 공유되어있다.

 

우선 각각의 페이지를 비교할 수 있도록 만들어보고자 한다.

 

현재 프로젝트 구조

메인 페이지는 각각의 기술이 도입된 페이지의 라우팅을 담고있기 위한 페이지이며 

노멀 페이지는 비교하기 위한 무한 스크롤을 제외한 어떠한 기술도 들어가있지 않은 페이지이다.

추후에 기술이 도입될 때 마다 해당 명으로 페이지를 생성하고자 한다.

 

Custom Hook은 우선 useInfiniteScroll이라는 무한스크롤 커스텀 훅을 생성해주었다.

// src/hooks/useInfiniteScroll.ts

import { useEffect, useRef } from "react";

const useInfiniteScroll = <T extends HTMLDivElement>(
    callback: () => void,
    ) => {
    const displayElement = useRef<T>(null);
    useEffect(() => {
        if (displayElement && displayElement.current) {
        const intersectionobserver = new IntersectionObserver(
            (entries, observer) => {
            entries.forEach((entry) => {
                if (entry.isIntersecting) {
                observer.unobserve(entry.target);
                callback();
                }
            });
            }
        );
        intersectionobserver.observe(displayElement.current);
        return () => intersectionobserver.disconnect();
        }
    }, [callback, displayElement]);

    return displayElement;
};

export default useInfiniteScroll;

//https://dev.rase.blog/21-12-07-intersection-observer/ 를 참고하였습니다.

Intersection Observer API를 활용하였다. 해당 커스텀 훅은 ref를 이용하여 해당 HTML Div Element에 observer를 부착한다. 부착 된 observer는 엘리먼트가 뷰포트에 들어 올 시, 콜백을 invoke하게 된다.

 

    // src/pages/Normal/index.tsx 
    
import axios from "axios";
import React, { useState, useEffect } from "react";
import useInfiniteScroll from '../../hooks/useInfiniteScroll'
type RandomImageType = {
    id: string;
    author: string;
    width: number;
    height: number;
    url: string;
    download_url: string;
  };

const Normal = () =>{
    const [randomImageList, setRandomImageList] = useState<RandomImageType[]>([])
    const ref = useInfiniteScroll<HTMLDivElement>(()=>{
        getRandomImages();
    });

    const getRandomImages = async () =>{
        try {
            const { data } = await axios.get('https://picsum.photos/v2/list?page=1&limit=10');
            setRandomImageList((prev) => prev.concat(data));
        } catch (error) {
            console.log(error);
        }

    }

    return (
        <div>
            <h1>노멀 이미지 로딩</h1>
            {randomImageList.map((randomImage) => {
                return(
                    <>
                        <img 
                            alt="random_image"
                            key={randomImage.id}
                            src={randomImage.download_url}
                            style={{width: '500px', height: '500px'}}
                        />
                        <br/>
                    </>
                )
            })}
            <div ref={ref}>
                마지막 부분
            </div>
        </div>
    )
}

export default Normal;

무한스크롤 영역을 위한 div 엘리먼트는 항상 맨 아래 노출 된다(현재로써는). 커스텀 훅은 getRandomImages를 콜백으로 불러온다. 해당 함수는 picsum을 이용하여 랜덤이미지를 불러오게 된다.

 

결과

페이지는 이미지들을 출력하고, ref를 담은 div가 뷰포트에 들어올 때, 이미지들을 추가하며, 이후 update 된 randomImageList 에 렌더링 되어 아래 추가적인 컨텐츠(이미지)가 노출되게 된다.