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> </>); }
위 코드는 다음과 같이 실행될 것이다.
./components/Lazy
동적으로 가져오기./components/Lazy
가 로딩되는 동안 Loading
컴포넌트를 렌더링한다../components/Lazy
가 로딩이 완료되면, LazyComponent
컴포넌트를 렌더링한다.정말 간단하고 쉬운 코드이다. 하지만 여기서 의문이 한 가지 발생한다.
"로딩이 완료되면, 로딩 컴포넌트에 자연스럽게 사라지는 효과를 줄 수 없을까?"
위 의문으로부터 여러가지 방법 중 하나가 동적으로 import가 완료되었을 시, 그 타이밍을 알 수 있는 방법이 없을까? 라는 것이다. 하지만 위 코드를 보게 되면, LazyComponent
가 App
컴포넌트와 동일 레벨에 위치하고 있다. 그렇기에 변수를 하나 생성하는 것도 전혀 깔끔하지 않은 방법이라고 생각이 든다. 그렇기에 다른 방법이 필요했다.
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의 역할
Suspense
의 fallback
속성에 컴포넌트로 들어가게 된다.useEffect
의 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
속성을 통해서 특정 트랜지션 또는 애니메이션이 발동되도록 구현한다.
커스텀 훅을 적용시킨 위 코드는 다음과 같은 실행될 것이다.
./components/Lazy
동적으로 가져오기./components/Lazy
가 로딩되는 동안 Loading
컴포넌트를 렌더링한다../components/Lazy
가 로딩이 완료되면, isFullfiled
가 true로 변경된다.delay
의 지연 후에 isPending
이 false로 변경된다.isPending
이 false가 되면 Loading
컴포넌트가 언마운트되고, LazyComponent
컴포넌트가 렌더링된다.위에 작성된 코드에서 훅을 사용한 방법과 사용하지않은 방법을 내부구조를 모른다고 가정한다고 했을 때, 사용방법에서 몇 가지 차이점이 존재한다. 바로 로딩 컴포넌트가 fallback
으로 들어가냐, 들어가지 않고 훅에서 나온 isPending
으로 컨트롤하냐이다. 물론 위에 작성된 컴포넌트 중 DelayedSuspense
에 fallback을 props로 받게 할 수도 있지만 그렇게 작성하게 되면, 훅에서 내부 복잡도가 더 커질 것이라고 생각하고 위 방법을 택한 것 같다.
또한, 내가 작성한 코드보다 더 명확하고 가독성 좋은 코드가 존재할 것이다. 더 나은 방법을 가진 분이 계시다면 언제든지 댓글로 알려주시길 바랍니다! :)