[성능개선] Lazy Loading_intersectionObserver & context api

이나현·2023년 5월 27일
0

오라운드

목록 보기
16/18
post-thumbnail

문제상황

오라운드 메인 페이지를 다양한 상품을 노출하는 방향으로 개편하면서 많은 양의 이미지 로드에 따라 메인이 늦게 노출되는 문제가 발생했다.

해결방법

아래의 리스트에 해당하는 이미지는 lazy loading을 통해 유저가 스크롤을 내려 화면에 해당 리스트가 노출될 필요가 있을 때 로드되도록 해결했다. lazy loading을 위해 intersectionObserver와 context api를 사용했다.

  1. 기본 image Lazy loading
// 옵션 객체
const options = {
  // null을 설정하거나 무엇도 설정하지 않으면 브라우저 viewport가 기준이 된다.
  root: null,
  // 타겟 요소의 20%가 루트 요소와 겹치면 콜백을 실행한다.
  threshold: 0.2
}

// Intersection Observer 인스턴스
const observer = new IntersectionObserver(function(entries,observer) {
  entries.forEach(entry => {
    // 루트 요소와 타겟 요소가 교차하면 dataset에 있는 이미지 url을    타겟요소의 src 에 넣어준다.
    if (entry.isIntersecting) {
      entry.target.src = entry.target.dataset.src
      
      // 지연 로딩이 실행되면 unobserve 해준다.
      observer.unobserve(entry.target)
    }
  })
}, options)

const imgs = document.querySelectorAll('img')
imgs.forEach((img) => {
  // img들을 observe한다.
  observer.observe(img)
})

참조: 실무에서 느낀 점을 곁들인 Intersection Observer API 정리

⇒ image의 경우 src를 html 속성에 넣어두었다가 관찰대상이 교차되었을 때, src를 대체한다.

2. 오라운드 메인에서 해야할 lazy loading

⇒ 오라운드에서 해야할 lazy loading은 mock component를 화면에 보이는 다음 컴포넌트로 넣어두었다가, 스크롤에 해당 컴포넌트가 교차되는 순간 데이터를 패치한다.

그리고 패치가 완료되면, 보여주고자 한 데이터가 있는 컴포넌트를 보여준다.

(데이터 패치 중이면 loading 카드들이 보이고, 데이터 패치에 실패했다면 해당 섹션을 날려준다.)

(1) useOnScreen hook

import { useState, useEffect, useRef, MutableRefObject } from 'react';

/**
 * ref 객체가 화면에 출력되는 시점을 감지하는 훅
 *
 * @param ref           감지 할 대상
 * @param rootMargin    해당 값만큼 미리 감지 한다.
 */
export function useOnScreen<T extends Element>(ref: MutableRefObject<T>, rootMargin = '0px'): boolean {
  // State and setter for storing whether element is visible
  const [isIntersecting, setIntersecting] = useState<boolean>(false);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        setIntersecting(entry.isIntersecting);

        if (entry.isIntersecting && ref.current) observer.unobserve(ref.current);

      },
      {
        rootMargin
      }
    );

    if (ref.current) {
      observer.observe(ref.current);
    }

    return () => {
      if (ref.current) observer.unobserve(ref.current);
    };
  }, []);

  return isIntersecting;
}

⇒ 해당 훅은 관촬되는 대상이 화면에 들어올 경우 boolean 값을 내놓는 훅이다.

(2)LazyLoadingLayOut 컴포넌트

interface LazyLoadingLayoutProps {
  children: React.ReactElement
  skeletonView?: React.ReactNode
}

interface GetSkeletonProps {
  skeletonType: 'basic' | 'tag';
}

const Loading = () => (
  <SkeletonWrapper>
    <LoadingWrapper>
      <Title.Loading/>
    </LoadingWrapper>
    <OroundSwiper freeModeYn={true}>
      {[...Array(5)].map((value, index) => (
        <ProductBox key={`Skeleton_${index}`}>
          <ProductCard.Loading/>
        </ProductBox>
      ))}
    </OroundSwiper>
  </SkeletonWrapper>
);

export const LazyLoadingContext = createContext(true);
// section 별로 skeleton을 Props로 내려받을지 고민중
// skeleton을 props로 받는다면 common한 skeleton 컴포넌트 만든 뒤, default로 사용하고 props 여부 조건문 필요
const LazyLoadingLayout: React.FC<LazyLoadingLayoutProps> = (props) => {
  const { children, skeletonView } = props;
  const mockTarget: any = useRef<HTMLDivElement>();
  const isScreen: boolean = useOnScreen<HTMLDivElement>(mockTarget, '20%');

  return (
  <LazyLoadingContext.Provider value={isScreen}>
    <div ref={mockTarget}>
      {children}
      {/*{!isScreen ? <Loading/> : (children)}*/}
    </div>
  </LazyLoadingContext.Provider>
  );
};

export default LazyLoadingLayout; 

⇒ children으로 각 섹션의 리스트가 관찰대상이 되고, 20%의 margin 내에 관찰대상이 들어오게 되면 boolean 값을 내놓아주는 레이아웃 컴포넌트이다.

[문제상황]

  • 처음에는 조건문으로 화면에 해당 섹션이 노출되지 않았으면 loading 컴포넌트가 나오고 노출이 되었다면 관찰대상인 섹션이 나오는 로직을 만들었다. (주석처리 된 부분!)
  • 하지만 로딩 화면이 2번 나오다보니, 데이터가 있어도 컴포넌트 자체가 렌더링 되어서 보이는 시간이 상당히 오래걸렸고, 로딩화면에도 skeleton 기능이 있다보니, 부하가 있는 듯 기대했던 속도만큼 나오지 않았다.

[해결 방법]

  • 해당 로직에서 isOnScreen 여부를 prop로 자식 컴포넌트에 전달해주고 loading 레이아웃을 자식 컴포넌트에서 재사용하고자 했다.
  • 자식 뎁스와 상관없이 props로 내려주려고 하다보니 컴포넌트안에서 전역적으로 데이터를 공유할 수 있는 context api 를 사용하고자 했다.

Context API

  • context api를 사용해서 스크린에 노출되었는지 확인하고, 해당 값을 자식의 각 컴포넌트에서 사용할 수 있게 되었다.
const Section = () => {
  const isOnScreenYn = useContext(LazyLoadingContext);
  const router = useRouter();
  const { loading, data, error } = useData(isOnScreenYn);

  if (!isOnScreenYn || loading) {
    return (
      <Loading/>
    );
  }
  if (!list?.length) return null;
  return (
    <Wrapper>
      <Title>
        <Title.TitleName title={data.title}/>
      </Title>
      <List productList={data?.productlist}/>
    </Wrapper>

  );

};

export default Section;
profile
technology blog

0개의 댓글