본문 바로가기
Front-end/웹성능 최적화

Lazy loading 해서 중요한 리소스 순서 보장하기

by img 2022. 5. 3.

웹사이트 페이지를 볼 때 많은 리소스들이 있는데, 그 중 중요하지 않은 리소스들이 먼저 네트워크를 통해 다운이 되고 그 이후에서야 여유가 될때 중요한 리소스가 다운되는 경우가 있습니다. 리소스들의 다운로드 순서가 보장이 되지 않기 때문인데요.(크롬 개발자 도구의 Network 탭에서 리소스들의 다운로드 상태가 pending인 경우 다른 리소들의 다운로드가 너무 많아 순서가 미뤄진 것입니다) 
이럴 경우 사용자가 제일먼저 마주치는 최상위의 리소스가 제일 먼저 다운로드 되어서 사용자에게 가장 빨리 보여지도록 하위의 리소스들의 다운로드를 잠시 대기시켜놓는 것이 Lazy loading입니다. 말그대로 게으른(느린) 로딩이죠. 

특정 리소스가 다른 리소들에게 밀려 느리게 로딩되는지 확인하기 위해서는 크롬 개발자도구의 Network탭에서 status가 pending이 걸렸었는지 확인해보면됩니다. (로딩이 너무 빠르다면 No throttling에서 인터넷 속도를 조절할 수 있습니다. ) 

그럼 어떤 리소스가 pending이 걸렸었는지 확인 한 후에, 리소스들의 다운로드 순서를 다음과 같이 lazy loading을 통해 조절해줄 수 있습니다. 

1. Intersection Observer 객체 사용
2. lazyloading 라이브러리 사용


Intersection Observer란, 특정 element가 스크롤에 의해 화면에 보여지는지 안보여지는지 판단할 수 있도록 걸어놓는 '관찰자'같은 역할을 하는 브라우저 자체 기능입니다. 

https://developer.mozilla.org/ko/docs/Web/API/Intersection_Observer_API

 

Intersection Observer API - Web API | MDN

Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법입니다.Intersection Observer API는 타겟 요소와 상위 요소 또는

developer.mozilla.org

function createObserver() {
  let observer;

  let options = {
    root: null,
    rootMargin: "0px",
    threshold: buildThresholdList()
  };

  observer = new IntersectionObserver(handleIntersect, options);
  observer.observe(boxElement);
}

사용방법은 요약하면 다음과 같습니다 :
new IntersectionObserver(콜백함수, 옵션) 로 객체를 생성해준 후에, 
.observe(DOM element) 하게 되면 해당 element는 observer에 의해 관찰되는 상태가 되어서 스크롤하다가 해당 요소가 브라우저에 보여지는 순간 콜백함수가 실행이 되는 구조입니다. 

그럼 이제 리액트에 적용해보겠습니다. 

1. 이미지에 observer걸어주기 

function MyComponent(props){

    //React에서 직접 DOM요소에 useRef hook으로 접근
    const imgRef = useRef(null);


    useEffect(()=>{

        const callback = () => {
            console.log('callback');
        }
        const options = {};

        const observer = new IntersectionObserver(callback, options);

        observer.observe(imgRef.current);
    })

    return(
        <img src={props.image} ref={imgRef} />
    )

}

export default MyComponent

'callback' 콘솔이 찍히는 순간은
[1]observer 객체가 생성되었을 때 
[2] element가 화면에 들어왔을 때, 
[3] element가 화면 밖으로 나갔을 때
입니다. 우리는 [2]의 경우만 useEffect hook을 타면 되는데 현재는 세 경우 모두 다 useEffect hook을 타버리기 때문에 그다음에 저희가 해줘야 할 단계는 : 

2. 해당 element가 화면 안에 들어와 있을 때만 useEffect를 타도록 만들어주기

function MyComponent(props){

    const imgRef = useRef(null);


    useEffect(()=>{

        const callback = (entries, observer) => {
        
        // 첫번째는 element배열, 두번째는 observer 객체
            entries.forEach( entry => {
            	if (entry.isIntersecting) { 
                
                // entry.isIntersecting은  화면 안에 해당 요소가 들어와있는지 아닌지를 나타내는 값
                	console.log("해당 element가 화면안에 있으면 콘솔을 찍어주세요");
                }
            }
        }
        const options = {};

        const observer = new IntersectionObserver(callback, options);

        observer.observe(imgRef.current);
  
    })

    return(
        <img src={props.image} ref={imgRef} />
    )

}

export default MyComponent

콜백함수 인자로 entries와 observer가 넘어오는데, entries는 observe한 element들의 배열이고 ( 요소 하나만 observe했기 때문에 entries도 length 1짜리 배열이 들어옴 ), observer는 new IntersevtionObserver와 동일한 객체입니다. 
entries안의 entry.isIntersecting을 통해 화면 안에 해당 element가 들어와있는지 아닌지를 확인할 수 있어서 if문을 걸어놓으면 해당 element가 화면안에 들어와 있을 때만 따로 컨트롤 할 수 있게 됩니다. 

3. 이미지 네트워크 다운로드하기 

function MyComponent(props){

    const imgRef = useRef(null);


    useEffect(()=>{

        const callback = (entries, observer) => {
        
            entries.forEach( entry => {
            	if (entry.isIntersecting) { 
                	
                    // dataset에 있던 src를 진짜 src에다가 넣어줌 
                    // entry.target이 <img /> element를 가르킴
                	entry.target.src = entry.target.dataset.src 
                    
                     // 다시 스크롤이 돌아와도 다신 안타도록 .unobserve() 시켜줌 
                    observer.unobserve(entry.target)
                }
            }
        }
        const options = {};

        const observer = new IntersectionObserver(callback, options);

        observer.observe(imgRef.current);
    })

    return(
        // <img src={props.image} ref={imgRef} />
        <img data-src={props.image} ref={imgRef} />
    )

}


export default MyComponent

일반적으로 이미지의 lazy loading을 하기 위해서는 data-src속성을 이용합니다. <img>의 src값을 따로 주지 않고 data-src에 값을 주었기 때문에, 해당 <img> element는 어떠한 이미지도 로딩하지 않고 그냥 DOM요소로 공간만 차지할 뿐입니다. 
그리고 그 후에 useEffect hook을 통해 해당 element가 화면 안으로 들어왔을 때, dataset.src에 들어있던 src에다가 넣어주면 그때서야 <img>의 src를 로딩하기 시작합니다. 그리고 스크롤이 나갔다가 다시 돌아오더라도 해당 로직 중복해서 타지 않도록 .unobserve( ) 를 걸어줌으로써 위에서 언급한 [3]번의 경우를 방지합니다.

이렇게 나중에보이는 이미지들을 lazy-loading을 걸어줄 경우 좀 더 빨리 메인 화면을 로딩할 수 있게 됨 


이렇게 IntersectionObserver를 이용하는 방법도 있지만 오픈소스 라이브러리를 이용하는 방법도 있습니다. 리액트에서는 react-lazyload 라는 라이브러리를 활용하면 더 간편해집니다. 위에서 설명한 IntersectionObserver와의 차이는 react-lazyload라이브러리는 observer객체가 아닌 scroll event를 이용해서 구현한 라이브러리라고 합니다. 

https://www.npmjs.com/package/react-lazyload

 

react-lazyload

Lazyload your components, images or anything where performance matters.. Latest version: 3.2.0, last published: a year ago. Start using react-lazyload in your project by running `npm i react-lazyload`. There are 398 other projects in the npm registry using

www.npmjs.com

npm install --save react-lazyload

우선 npm install을 해주고, <LazyLoad>로 lazyloading을 원하는 element를 감싸주면 해당 element가 화면에 보여질 때에 로드를 시작합니다. 

만약 스크롤을 내리고 해당 element에 도달해서야 이미지 로딩을 하기 시작해서 잠깐이라도 그 공백이 있는것이 싫다면 offset이라는 옵션을 사용해서 스크롤이 닿지 않더라도 얼마나 미리 이미지를 로드하겠다고 정해줄 수 있습니다. 

offset={100} 이렇게 주면 100px만큼의 이미지들을 미리 로드하기 시작합니다. 해당 offset 수치를 조절해서 그만큼 이미지의 공백 노출을 최소화할 수 있습니다. 

댓글