# 리액트 18 서스펜스 (Suspense)

- Author: @baealex
- Published: 2024-07-06
- Updated: 2025-12-20
- Source: http://blex.me/@baealex/what-is-react-suspense
- Tags: 리액트, 프론트엔드

---

#### React Suspense 기본 개념

**Suspense** 는 React에서 비동기 작업을 처리할 때, 컴포넌트의 렌더링을 잠시 멈추고, 그 작업이 완료될 때까지 대기하는 기능을 제공해주는 도구이다. 쉽게 말해서, 데이터가 준비될 때까지 기다리게 하고, 그동안 로딩 상태를 보여준다.

#### 주요 개념

1. **Suspense 컴포넌트**: - Suspense 컴포넌트는 비동기 작업이 완료될 때까지 대기 상태를 관리한다.
  - `fallback` 속성을 사용해서 비동기 작업이 완료될 때까지 보여줄 UI를 정의할 수 있다. ```typescript
  import React, { Suspense } from 'react';
  
  const MyComponent = React.lazy(() => import('./MyComponent'));
  
  function App() {
   return (
   <Suspense fallback={<div>Loading...</div>}>
   <MyComponent />
   </Suspense>
   );
  }
  
  export default App;
  ```
2. **React.lazy**: - React.lazy를 사용하면 컴포넌트를 동적으로 로드할 수 있다.
  - React.lazy는 보통 코드 스플리팅을 위해서 사용된다. ```typescript
  const MyComponent = React.lazy(() => import('./MyComponent'));
  ```
3. **API Fetch**: - React Query의 suspense 옵션을 사용하면 간단하게 API가 응답되기 전에 Suspense의 fallback이 실행되도록 만들 수 있다.

#### 응용 해보기

###### 어떻게 동작하는 걸까?

기본적으로 컴포넌트에서 Promise가 Throw되면 Suspense는 fallback의 컴포넌트를 렌더링 한다. 아래는 기본적인 구조를 나타낸 것이지만 기본적으로 리액트에서 컴포넌트는 계속해서 실행되는 개념이기 때문에 Promise가 반복되서 실행되지 않도록 해야한다.

```typescript
// Don't
function LazyComponent() {
    const data = fetchSomething(); // 'data' type is inferred to be Promise.

    if (data instanceof Promise) {
        throw data;
    }

    return (
        <div>
            {data}
        </div>
    )
}
```

###### 메모라이즈

기본적으로는 메모리에 저장해두고 같은 키를 바탕으로 같은 객체를 반환하도록 해준다. Promise 타입일 때는 Promise를 Throw 시켜서 Suspense의 fallback을 실행시키는 전략이다.

```typescript
const cache = new Map<string, unknown>()

function useMemorizedSuspensePromise<T>(keys: string[], fn: () => Promise<T>): T | null {
    const key = keys.join('.')

    if (!cache.has(key)) {
        const promise = fn().then((data) => {
            cache.set(key, data);
        });
        cache.set(key, promise);
    }

    const cachedData = cache.get(key);
    if (cachedData instanceof Promise) {
        throw cachedData;
    }

    return cachedData as T;
}

function LazyComponent() {
    const data = useMemorizedSuspensePromise(['fetch', 'something'], () => fetchSomething());

    return (
        <div>
            {data}
        </div>
    )
}
```

###### 프로미스 상태 판별

프로미스의 처리 상태를 직관적으로 파악하기 위해서 간단한 함수를 만들어보자.

```typescript
function createSuspensePromise<T>(promise: Promise<T>) {
    let status = 'pending';
    let result: T;
    const suspender = promise.then(
        r => {
            status = 'success';
            result = r;
        },
        e => {
            status = 'error';
            result = e;
        }
    );

    return {
        read() {
            if (status === 'pending') {
                throw suspender;
            } else if (status === 'error') {
                throw result;
            } else if (status === 'success') {
                return result;
            }
        }
    };
}
```

위 함수를 이용한 기본적인 사용 방법은 아래와 같다. 컴포넌트에서는 read() 함수만 실행하면 데이터를 가져오거나 Suspense 컴포넌트의 fallback이 실행된다.

```typescript
const suspensePromise = createSuspensePromise(fetchSomething())

function LazyComponent() {
    const data = suspensePromise.read()

    return (
        <div>
            {data}
        </div>
    )
}
```

컴포넌트 내부에서 훅처럼 사용하도록 개선해보자.

```typescript
function useSuspensePromise<T>(promise: Promise<T>, dependents: string[] = []) {
    const [resource, setResource] = useState<ReturnType<typeof suspensePromise<T>> | null>(null)

    useEffect(() => {
        setResource(suspensePromise(promise))
    }, dependents)

    return resource?.read();
}

function LazyComponent() {
    const data = useSuspensePromise(fetchSomething());

    return (
        <div>
            {data}
        </div>
    )
}
```
