---
type: note
kind: Recipe
tags: ['react', 'suspense', 'declarative']
status: release
ctime: 2026-06-23
mtime: 2026-09-01
generated: { by: claude/opus-5, at: 2026-09-01T00:00:00Z }
sources:
  - id: algebraic-effects
    resource: https://overreacted.io/algebraic-effects-for-the-rest-of-us/
    title: "Algebraic Effects for the Rest of Us"
---

Suspense는 **비동기를 동기처럼 보이게** 만든다. 데이터가 아직 없으면 promise를 던지고, React가 그걸 받아 기다렸다가 다시 그린다. 그래서 상태를 조율하는 대신 **입력을 미루고 → 질의하고 → 그리는** 순서로 끝난다.

**언제**: 비동기 데이터를 그리는데 로딩·에러가 늘면서 화면이 튈 때. 또는 새로 만들면서 어느 쪽으로 갈지 고를 때.

**절차**

1. 검색어·결과·로딩 중인지·에러인지를 **각각** 들고 있는지 본다. 넷이 따로 켜지고 꺼지니 **실제로는 있을 수 없는 조합까지 코드가 표현할 수 있다** — "로딩 중이면서 동시에 에러" 같은 것. 그 조합이 한 프레임이라도 그려지면 화면이 튄다.
2. 분기를 `if/else`에서 **트리로 옮긴다** — 로딩은 `<Suspense>`, 에러는 `<ErrorBoundary>`, 성공은 본문.
3. 본문에 loading·error를 인자로 넘기지 않는다. 안 넘겨도 **가장 가까운 경계가 대신 잡기** 때문이다 — `render`는 결과 하나만 받는다.

**확인**: 있을 수 없는 조합을 적을 수 있는가. 적을 수 **없으면** 된 것이다 — 상태가 로딩이거나, 에러거나, 성공이거나 셋 중 하나가 된다.

**함정**: 스피너가 너무 빨리 뜨고 사라지는 건 이 축으로 안 고쳐진다 — 다른 축이다 → [573](/memo/573)

---

곁가지 — 인자를 위로 들어올려 바깥이 처리하게 하는 이 모양은 대수적 효과(algebraic effects)의 handler와 같다.[^algebraic-effects]

[^algebraic-effects]: `try/catch`와 비슷하다 — 중간 함수들은 몰라도 되고 가장 가까운 handler가 받는다. 다른 점은 **처리하고 끝나지 않는다**는 것이다. handler가 값을 돌려주면 효과를 일으킨 자리로 되돌아가 실행이 이어진다. ([Algebraic Effects for the Rest of Us](https://overreacted.io/algebraic-effects-for-the-rest-of-us/))

stale-while-revalidate(두 시계·isStale) 부분은 [570](/memo/570)으로.
