본문 바로가기

FE/React

(React.js + Next.js) Skeleton 적용

프로덕트에서 UX 개선 항목으로 SkeletonUI 와 ProgressBar 요청사항이 들어왔다.

 

처음 진행해보는 작업이라 많은 검색을 해봤고, 절대 다수의 기술 블로그에서는 다음과 같이 설명하고 있었다.

 

1. 페이지 이동 시에 state 를 통해서 content 영역에 skeletonUI 를 렌더링한다. 

2. fetch 이후 state 를 통해서 content 영역에 정상 데이터를 렌더링한다.

 

대충 코드로 나타내면 이렇게 되겠다. 

const [content, setContent] = useState(<Skeleton />);

request('url', (resData) => {
	setContent(<div>~~~~</div>);
});

유명한 스타트업의 기술블로그도 위와 같은 방식을 사용하고 있었다.

 

하지만 몇 가지 이유로 사뭇 다른 방식으로 개발하게 되었다. 그 이유는 

 

첫째, 우리 프로덕트는 eCommerce로, 특성상 많은 이미지가 AWS를 통해 전달받아야 했다.

따라서 이미지 로딩에 많은 시간이 걸리는 상황이었다. 

스켈레톤이 제거되는 시점이 fetch 이후가 아닌, image 가 전부 로딩이 되고 난 후가 되기를 원했다.

 

둘째, 일일히 추가하지 않더라도 _app.jsx 를 통해서 한번에 관리되기를 바랬다.

 

따라서 다음과 같은 방식이 되었다.

 

라우팅 시작 시에 >

1. initialProps 로 각 페이지가 어떤 타입의 스켈레톤을 그릴지 _app.jsx 로 전달

2. state를 통해 skeletonUI 생성

3. 각 페이지 접근 완료 후, 이미지 로딩이 완료되었는지 체크하여

4. 콜백으로 skeletonUI 를 제거하는 setState 실행 

 

 

코드는 다음과 같다

 

// _app.jsx

funtion MyApp(props) {
	const [isDOMImageLoaded, setIsDOMImageLoaded] = useState(false);
    const [skeletonUI, setSkeletonUI] = useState();
    
    ...
    
    const routeChangeStart = () => {
		setIsDOMImageLoaded(false);
    }
    
    useEffect(() => {
        Router.events.on('routeChangeStart', routeChangeStart);

        return () => {
            Router.events.off('routeChangeStart', routeChangeStart);
        }
    }, []); 
    
    useEffect(() => {
    	if (pageProps.skeletonType) {
        	if (pageProps.skeletonType === 'type1') {
            	setSkeletonUI(<SkeletonType 1/>);
            }
            ...
        }
    }, [asPath]);
    
    useEffect(() => {
    	if (isDOMImageLoaded === true) {
      		setSkeletonUI('');
        }
    }, [isDOMImageLoaded]);

	...
	
    return (
    	<>
        ...
        	<Component
            	{...pageProps}
                isDOMImageLoaded={isDOMImageLoaded}
          		setIsDOMImageLoaded={setIsDOMImageLoaded}
            />
            {skeletonUI}
        ...
        </>
    )
}

 

// imageLoadingFinish.js

const imageLoadingFinish = (callback = false) => {
  if (typeof window === 'undefined') {
    return false;
  }

  const imgs = document.images;
  const len = imgs.length;
  let counter = 0;

  const incrementCounter = () => {
    counter += 1;
    if (counter === len) {
      document.body.style.backgroundColor = 'white';
      if (callback) {
        callback();
      }
    }
  };

  [].forEach.call(imgs, (img) => {
    if (img.complete) incrementCounter();
    else img.addEventListener('load', incrementCounter, false);
  });

  return true;
};

export default imageLoadingFinish;

 

// somePage.jsx

const Main = ({setIsDOMImageLoaded}) => {
	fetch('url', () => {
    	setPageContent(<div>~~~</div>);
        
        imageLoadingFinish(() => {
        	setIsDOMImageLoaded(true);
      	});
    });

}

Main.getInitialProps = () => {
	...
	
    return {
    	skeletonType: 'type1'
    };
};

 

 

곧 퇴근시간이라 설명은 나중에 . .

'FE > React' 카테고리의 다른 글

Refactor[1] - Typescript  (0) 2021.11.08
useState 가 아무튼 안될 때  (0) 2021.10.18
배열을 useState에서 사용할 때  (4) 2021.08.23
functions are not valid as a react child  (1) 2021.05.18
useEffect dynamic depth  (0) 2021.05.13