use
훅은 그 자체로도 혁신적이었지만, 그 진정한 힘은 Suspense
와 함께일 때 비로소 완성되었다. 이 둘은 처음부터 하나의 목표를 위해 설계된, 완벽하게 협력하는 파트너였다.
“use
훅은 신호를 보내는 역할이고, Suspense
는 그 신호를 받아 처리하는 역할입니다.”
앤드류 클라크는 팀원들에게 두 기능의 관계를 명확히 설명했다. 그는 간단한 컴포넌트 트리를 화이트보드에 그렸다.
<App>
<Suspense fallback={<PageSpinner />}>
<Layout>
<Header />
<Suspense fallback={<PostSkeleton />}>
<PostDetails />
</Suspense>
</Layout>
</Suspense>
</App>
그리고 PostDetails
컴포넌트의 내부 코드를 옆에 적었다.
// PostDetails.js
function PostDetails() {
const post = use(fetchPost()); // <--- 여기서 Promise가 처리 중이라면?
const comments = use(fetchComments()); // <--- 또는 여기서
// ... 렌더링 로직
}
“PostDetails
가 렌더링되는 도중에, use(fetchPost())
를 만났다고 가정합시다. 이 fetchPost
Promise는 아직 처리 중(pending)입니다. 이때 use
훅은 무엇을 할까요?”
앤드류가 질문을 던졌다.
“use
훅은 그 자리에서 특별한 종류의 에러, 정확히는 Promise 자체를 ‘던집니다(throw)’. 이것은 일반적인 자바스크립트 에러와는 다릅니다. React만이 이해할 수 있는 특별한 신호죠. ‘작업이 아직 끝나지 않았으니, 이 렌더링을 계속 진행할 수 없음’이라는 의미입니다.”
그의 설명에 따라 팀원들은 코드의 실행 흐름을 머릿속으로 따라갔다.
use
훅이 신호를 던지는 순간, PostDetails
컴포넌트의 실행은 즉시 중단된다. 그 아래에 있는 use(fetchComments())
나 다른 렌더링 로직은 아예 실행될 기회조차 얻지 못한다.
그러면 React는 무엇을 하는가?
“React는 이 특별한 신호를 ‘잡습니다(catch)’. 그리고는 컴포넌트 트리를 거슬러 올라가기 시작합니다. 가장 가까운 <Suspense>
경계를 찾기 위해서죠.”
앤드류는 화이트보드의 PostDetails
에서 바깥쪽으로 향하는 화살표를 그렸다. 화살표의 끝은 PostDetails
를 직접 감싸고 있는 <Suspense fallback={<PostSkeleton />}>
을 향했다.
“React는 가장 가까운 Suspense
를 찾으면, 그곳에 정의된 fallback
UI, 즉 <PostSkeleton />
을 대신 렌더링합니다. 그리고 원래 렌더링하려던 PostDetails
작업은 잠시 보류해두죠.”
이것이 바로 둘의 관계였다.
use
: “아직 준비 안 됐어!”라고 소리치며 작업을 중단시키는 현장 작업자.Suspense
: 그 소리를 듣고 달려와, 임시 안전 펜스(fallback
)를 치고 상황을 정리하는 현장 관리자.
만약 PostDetails
를 감싸는 Suspense
가 없었다면, React는 트리를 더 거슬러 올라가 App
레벨에 있는 <Suspense fallback={<PageSpinner />}>
를 찾아냈을 것이다. 어떤 fallback
을 보여줄지는 가장 가까운 Suspense
경계가 결정한다. 이 예측 가능한 동작 덕분에 개발자는 로딩 UI의 범위를 매우 세밀하게 제어할 수 있었다.
use
훅은 단독으로 존재할 수 없었다. Suspense
라는 파트너가 없다면, use
가 던진 신호는 그저 처리되지 않은 에러로 남아 앱을 중단시킬 뿐이었다. 반대로 Suspense
역시 use
훅이나 React.lazy
처럼 자신에게 신호를 보내줄 존재가 없다면 아무 의미 없는 빈 껍데기였다.
이 둘은 분리할 수 없는 한 몸이었다. 이 완벽한 파트너십을 통해, React는 비동기 로딩이라는 예측 불가능한 작업을, 선언적이고 구조적인 방식으로 완전히 정복해 나가고 있었다.