티스토리 뷰

Frontend/react.js

[React] Suspense 와 React.lazy

blueprint-12 2023. 5. 28. 20:23

Suspense with React.lazy

서스펜스를 사용하면 컴포넌트의 렌더링을 어떤 작업이 끝날 때까지 잠시 중단시키고 다른 컴포넌트를 먼저 렌더링할 수 있다. 이 작업이 꼭 어떠한 작업이 되어야 한다는 특별한 제약사항은 없지만 REST API나 GraphQL을 호출하여 네트워크를 통해 비동기로 데이터를 가져오는 작업을 가장 먼저 떠오르게 한다.

서스펜스는 어떤 컴포넌트가 읽어야 하는 데이터가 아직 준비되지 않았다고 리액트에게 알려주는 새로운 매커니즘이다. 

기본적으로 리액트는 JSX 코드 안에 들어있는 모든 컴포넌트를 즉시 호출하여 바로 렌더링을 진행한다. 

하지만, 컴포넌트를 Suspense로 감싸주면 컴포넌트의 렌더링을 특정 작업 이후로 미루고 그 작업이 끝날 때까지는 fallback컴포넌트를 대신 보여줄 수 있다.

 

lazy를 사용하면 현재 필요하지않은 컴포넌트의 로드를 필요할 때에만 비동기적으로 가져와서 처음 로드되는 번들의 양을 줄일 수 있다. 주의사항이 있다면, React.lazy는 우리가 import 하려는 컴포넌트가 default export 되어있다고 전제한다.(그래서 주로 page단위로 lazy로드를 하여 코드 스플리팅을 한다.)

lazy 컴포넌트가 있는 컴포넌트가 리퀘스트(방문)될 때, 비동기적으로 import해서 필요할 때 불러오게 된다.

이때, 비동기식으로 부럴오면서 잠깐 동안(네티워크 사정에 따라 빈 화면이 보일 수 있다)이때 뭔가 로딩되고 있다고 유저에게 알려줄 수 있는데 바로 Suspense를 사용하면 된다.

 

데이터 fetching과 렌더링 그 사이 

React 에서 일반적으로 렌더링시, 비동기 작업 처리를 크게 3가지 방법으로 제시

  1. Fetch-on-render (ex. fetch in useEffect): 컴포넌트 렌더링(마운트상태)를 먼저 시작하고 useEffect나 componentDidMount로 비동기 처리를 한다.
  2. Fetch-then-render(ex. Relay without Suspense): useEffect나 componentDidMount로 화면을 그리는데 필요한 데이터를 모두 조회한 후 렌더링을 시작(1번이랑 무슨 차이인지..?)
  3. Render-as-you-fetch(ex. Relay with Suspense): 비동기 작업과 렌더링을 동시에 시작한다. 즉시 초기 상태를 렌더링(fallback rendering)하고, 비동기 작업이 완료되면 다시 렌더링한다. 

1,2 방식은 비동기 작업을 useEffect나 컴포넌트 생명주기에서 처리한 후 렌더링을 시작하거나 다시 렌더링을 진행한다. 하짐나 이러한 처리 방식에는 여러 어려움이 있다.

  • 예를 들어 React 컴포넌트들의 자신만의 생명 주기를 가지고, 마찬가지로 비동기 작업들도 각각 자신만의 생명주기를 가지기 때문에 발생하는 문제가 있다. 이 문제를 해결하기 위해서 컴포넌트를 만들 때 데이터 fetching과 렌더링 사이를 동기화하는 작업을 추가하게 된다. => 해결은 가능하지만 컴포넌트의 복잡성을 증가시킬 뿐만아니라 waterfall problem(동시에 수행할 수 없고 순차적으로 수행하는 것을 의미)을 유발할 수 있다. (비동기 작업 사용의 가장 큰 이유는 동시성과 효율성을 얻는 것인데, 화면에서 동기화처리를 하다보면 동시성을 포기하고 효율성이 떨어지게 된다.) 또한, 비동기 작업들의 동시성을 보장한다면 (Promise.all()사용) 비동기 작업들을 한 곳에서 동시적으로 처리하고, 그 결과를 각 컴포넌트에게 전달하는 방식을 취하게 된다. 이 방식은 컴포넌트들 간의 역할 분담이 불명확하게 하고 높은 결합도를 만들 수 있다. 

3번의 Suspense는 뭐가 다를까?

Suspense 동작 방식

서스펜스와 비동기 작업(Promise)의 동작 방식을 살펴보면 아래와 같다.

  1. children에 속하는 컴포넌트가 예외 처리로 Promise를 던지면 (throw) fallback 프로퍼티로 전달받은 컴포넌트를 렌더링한다. 
  2. 예외로 던져진 Promise가 완료되면(fullfiled) children을 다시 렌더링한다.

Suspense로 비동기 작업과 렌더링 처리

예시 코드 

function Publisher({ uid }: { uid: number }) {
    const publisher = useFetch<number, IPublisher>(fetchPublisher, uid)
    console.log("Render - Rendering Publisher")
    return (
        <>
            <h1>{publisher?.displayName}</h1>
            <Suspense fallback={<p>Loading...</p>}>
                <Operators puid={uid} />
            </Suspense>
        </>
    )
}

export default function Operators({ puid }: { puid: number }) {
    // react-query 사용시
    const operators = useQuery(
        ["queryKey"], ()=>fetchOperators(puid), {suspense: true}
    )
    console.log("Render - Rendering Operators")
    return (
        <ul>
            {map(
                (operator) => (
                    <li key={operator.id}>{operator.displayName}</li>
                ),
                operators
            )}
        </ul>
    )
}

export default App() {
    const [uid, setUid] = useState<number>(1)
    return (
        <Suspense fallback={<p>Loading...</p>}>
            <Publisher uid={uid}/>
        </Suspense>
    )
}
  • 비동기 작업들이 진행중일 때(pending) Promise를 예외로 던지도록하면, Suspense를 사용해서 아주 손쉽게 비동기 작업과 렌더링 작업을 동기화할 수 있다.
  • 이전 비동기 처리와 비교하면, 컴포넌트들이 각자 fetching을 동시에 시작하게 되어 waterfall문제가 발생하지 않는다. 또한 Suspense의 계층 구조와 fallback 렌더링으로 컴포넌트들이 더 이상 경쟁 상태를 신경쓰지 않아도 된다. 
  • 컴포넌트 간의 결합도도 낮아지고 동기화 코드가 사라지며 데이터 fetching과 렌더링에만 신경 쓰면 되기 때문에 컴포넌트들의 복잡도가 낮아진다.

all ref: 카카오 FE 기술 블로그 

댓글