Suspense & Lazy With Transition

NB·2021년 5월 5일
3
post-thumbnail

기존 Suspense & Lazy 사용

React를 사용하면서 Code Splitting을 적용하려면 다음과 같이 Dynamic import를 사용해서 구현할 수 있다. 아래 예시에서는 트랜지션 적용 방법을 빠르게 확인하기 위해서 간단하게 지연 로딩을 일반 컴포넌트처럼 사용할 수 있도록 React.lazy()를 함께 사용하였다.

예시 코드 with (suspense & Lazy)

import { lazy, Suspense } from 'react'
import Loading from './components/Loading'

const LazyComponent = lazy(() => import(
  /* webpackChunkName: "lazy" */ 
  "./components/Lazy"
));

const App = () => {
  return (<>
    <Suspense fallback={Loading} >
      <LazyComponent />
    </Suspense>
  </>);
}

위 코드는 다음과 같이 실행될 것이다.

  1. ./components/Lazy 동적으로 가져오기
  2. ./components/Lazy 가 로딩되는 동안 Loading 컴포넌트를 렌더링한다.
  3. ./components/Lazy 가 로딩이 완료되면, LazyComponent 컴포넌트를 렌더링한다.

정말 간단하고 쉬운 코드이다. 하지만 여기서 의문이 한 가지 발생한다.


"로딩이 완료되면, 로딩 컴포넌트에 자연스럽게 사라지는 효과를 줄 수 없을까?"


위 의문으로부터 여러가지 방법 중 하나가 동적으로 import가 완료되었을 시, 그 타이밍을 알 수 있는 방법이 없을까? 라는 것이다. 하지만 위 코드를 보게 되면, LazyComponentApp 컴포넌트와 동일 레벨에 위치하고 있다. 그렇기에 변수를 하나 생성하는 것도 전혀 깔끔하지 않은 방법이라고 생각이 든다. 그렇기에 다른 방법이 필요했다.


지연시간이 포함된 Lazy & Suspense 사용

Suspense에서는 위 LazyComponent가 로드될 시,Loading 컴포넌트가 사라지게 된다. 이 점을 착안하여 훅으로 만들어볼까? 라는 방법이 떠올라 구현하게 되었다. 먼저 전체 구현 코드는 다음과 같다.

useTransitionSuspense Hook 전체 코드

import { 
  useState, 
  useEffect, 
  useCallback,
  useRef,
  Suspense 
} from 'react'

/**
 * 지연 로딩 트랜지션 추가 Hook
 * @param {object} props
 * @param {number} props.delay 로딩이 끝나고, 전환 되기 전 딜레이 시간 (ms) 
 * @returns 현재 로딩여부, 실제 로드가 끝났는지 여부, Custom Suspense
 */
const useTransitionSuspense = ({ delay }) => {
  const [isPending, setIsPending] = useState(true);

  const [isFullfilled, setIsFullfilled] = useState(false);

  const fallback = useRef(
    <FallbackComponent 
      delay={delay} 
      setIsPending={setIsPending} 
      setIsFullfilled={setIsFullfilled} 
    />
  );

  const suspense = useCallback(
    ({ children }) => DelayedSuspense(isPending, fallback.current, children)
  , [isPending]);

  useEffect(() => {
    if (!isFullfilled) return;
    const timer = window.setTimeout(() => setIsPending(false), delay);
    return () => window.clearTimeout(timer);
  }, [isFullfilled, delay]);

  useEffect(() => {
    if (!fallback.current) return;
    fallback.current = <FallbackComponent 
      delay={delay} 
      setIsPending={setIsPending} 
      setIsFullfilled={setIsFullfilled} 
    />;
  }, [delay]);

  return {
    isPending,
    isFullfilled,
    DelayedSuspense: suspense
  };
}

/** Suspense fallback에 넣어줄 Component */
const FallbackComponent = ({ delay, setIsPending, setIsFullfilled }) => {
  useEffect(() => {
    setIsPending(true);
    setIsFullfilled(false);
    return () => setIsFullfilled(true);
  }, [delay, setIsPending, setIsFullfilled]);
	
  return null;
};

/** Delay 후에 지연 컴포넌트를 표시해주는 Suspense */
const DelayedSuspense = (isPending, fallback, children) => {
  return (
    <Suspense fallback={fallback}>
      <div style={{ display: isPending ? "none" : "block" }}>
        {children}
      </div>
    </Suspense>
  );
}

export default useTransitionSuspense

먼저 useState를 통해서 delay를 포함한 현재 로딩 상태를 파악하는 isPending과 실제로 로딩이 완료되었음을 파악하는 isFullfiled 상태를 추가하고, 그 상태들을 알 수있도록 useEffect를 가지고 있는 FallbackComponent를 생성하였다. 이 컴포넌트는 다음과 같은 역할을 한다.


FallbackComponent의 역할

  • Suspensefallback 속성에 컴포넌트로 들어가게 된다.
  • 로딩이 완료되면 useEffect 의 return문이 실행되게 된다.
  • return문에서는 로딩이 완료됬었음을 true로 바꾸고, 사용자가 원하는 delay 시간만큼 후에 로딩여부를 false로 바꾸게 된다.

이 때, 주의할 점은 isPending은 사용자가 prop로 넘겨준 delay 시간까지 포함한 지연 상태이지만, isFullfiled는 실제로 해당 로딩이 끝난 상태를 의미한다. 이 점을 유의하여서 다음과 같이 코드를 작성할 수 있다.

useTransitionSuspense Hook 활용 코드

import { lazy, Suspense } from 'react'
import Loading from './components/Loading'
import useTransitionSuspense from './hooks/useTransitionSuspense

const LazyComponent = lazy(() => import(
  /* webpackChunkName: "lazy" */ 
  "./components/Lazy"
));

const App = () => {
  const {
    isPending, 
    isFullfilled,
    DelayedSuspense
  } = useTransitionSuspense({ delay: 2000 });

  return (<>
    {isPending && <Loading isDisappear={isFullfilled} />}

    <DelayedSuspense>
      <LazyComponent />
    </DelayedSuspense>
  </>);
}
  • 이 때, Loading 컴포넌트에서는 isDisappear 속성을 통해서 특정 트랜지션 또는 애니메이션이 발동되도록 구현한다.

커스텀 훅을 적용시킨 위 코드는 다음과 같은 실행될 것이다.

  1. ./components/Lazy 동적으로 가져오기
  2. ./components/Lazy 가 로딩되는 동안 Loading 컴포넌트를 렌더링한다.
  3. ./components/Lazy 가 로딩이 완료되면, isFullfiledtrue로 변경된다.
  4. 사용자가 Hook에 넘겨준 delay의 지연 후에 isPendingfalse로 변경된다.
  5. isPendingfalse가 되면 Loading 컴포넌트가 언마운트되고, LazyComponent 컴포넌트가 렌더링된다.

어느 것이 정답?

위에 작성된 코드에서 훅을 사용한 방법과 사용하지않은 방법을 내부구조를 모른다고 가정한다고 했을 때, 사용방법에서 몇 가지 차이점이 존재한다. 바로 로딩 컴포넌트가 fallback으로 들어가냐, 들어가지 않고 훅에서 나온 isPending으로 컨트롤하냐이다. 물론 위에 작성된 컴포넌트 중 DelayedSuspensefallback을 props로 받게 할 수도 있지만 그렇게 작성하게 되면, 훅에서 내부 복잡도가 더 커질 것이라고 생각하고 위 방법을 택한 것 같다.

또한, 내가 작성한 코드보다 더 명확하고 가독성 좋은 코드가 존재할 것이다. 더 나은 방법을 가진 분이 계시다면 언제든지 댓글로 알려주시길 바랍니다! :)

profile
𝙄 𝙖𝙢 𝙖 𝙛𝙧𝙤𝙣𝙩𝙚𝙣𝙙 𝙙𝙚𝙫𝙚𝙡𝙤𝙥𝙚𝙧 𝙬𝙝𝙤 𝙚𝙣𝙟𝙤𝙮𝙨 𝙙𝙚𝙫𝙚𝙡𝙤𝙥𝙢𝙚𝙣𝙩. 👋 💻

0개의 댓글