커뮤니티의 피드백을 수용하며 API와 문서를 개선하는 동안, React Core Team은 다시 내부의 근본적인 문제로 시선을 돌렸다. 서버 컴포넌트가 서버에서 데이터를 가져오는 것은 훌륭했지만, 그 ‘기다리는 시간’ 동안의 사용자 경험을 어떻게 우아하게 처리할 것인가 하는 문제였다.
개발자들은 오랫동안 비슷한 패턴의 코드를 반복해서 작성해왔다.
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchData()
.then(setData)
.catch(setError)
.finally(() => setIsLoading(false));
}, []);
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage />;
return <MyComponent data={data} />;
데이터 하나를 가져오기 위해 세 개의 상태와 하나의 useEffect
, 그리고 두 개의 조건문이 필요했다. 이 보일러플레이트는 컴포넌트의 본질적인 로직을 흐렸고, 여러 데이터를 동시에 가져와야 할 때는 걷잡을 수 없이 복잡해졌다.
“우리는 이 문제를 해결할 도구를 이미 가지고 있습니다.”
회의 중, 앤드류 클라크가 입을 열었다. 그의 시선은 과거를 향해 있었다.
“바로 Suspense
입니다.”
팀원들 사이에서 미묘한 정적이 흘렀다. Suspense
. 그 이름은 낯설지 않았다. 몇 년 전, 코드 스플리팅을 위해 React.lazy
와 함께 도입된 기능이었다. 특정 컴포넌트의 코드가 아직 다운로드되지 않았을 때, 로딩 화면을 보여주는 용도로 사용되던 도구. 대부분의 개발자에게 Suspense
는 그저 코드 스플리팅을 위한 유틸리티, 그 이상도 이하도 아니었다.
“우리는 Suspense
의 잠재력을 너무 좁게 보고 있었습니다.”
앤드류는 말을 이었다.
“Suspense
의 본질은 ‘코드가 준비되지 않았을 때’를 다루는 것이 아닙니다. 더 근본적으로, ‘어떤 자원이 아직 준비되지 않았을 때’를 선언적으로 처리하는 메커니즘입니다. 그리고 웹 애플리케이션에서 가장 흔하게 준비되지 않는 자원이 무엇일까요? 바로 데이터입니다.”
그의 설명은 팀의 뇌리를 강타했다.
그동안 Suspense
는 반쪽짜리 잠재력만 발휘하고 있었다. 이제 그 잠재력을 완전히 해방시킬 때가 온 것이다.
팀의 비전은 명확해졌다. 개발자는 더 이상 isLoading
상태를 직접 관리할 필요가 없다. 데이터 페칭 컴포넌트는 그저 데이터를 요청하기만 하면 된다. 만약 데이터가 아직 도착하지 않았다면? 컴포넌트는 React에게 “나 아직 준비 안 됐어!”라고 알리기만 하면 된다. 기술적으로는 ‘Promise를 던지는(throw)’ 행위였다.
그러면 React가 그 신호를 감지하고, 컴포넌트 트리에서 가장 가까운 Suspense
경계를 찾아 그곳에 정의된 fallback
UI(스피너, 스켈레톤 화면 등)를 대신 보여준다. 데이터가 준비되면, React는 멈췄던 렌더링을 다시 이어간다.
이 모든 과정이 React에 의해 자동으로 처리된다. 개발자는 오직 ‘데이터가 있을 때’의 행복한 상황만 가정하고 코드를 짜면 그만이었다.
- Before:
if (isLoading)...
,if (error)...
- After:
<Suspense fallback={<Spinner />}><MyDataComponent /></Suspense>
이것은 혁명이었다. Suspense
는 더 이상 변방의 유틸리티가 아니었다. 서버 컴포넌트 시대의 데이터 로딩을 책임질 핵심 도구로, React 19 아키텍처의 심장부로 화려하게 복귀를 선언하고 있었다. 수년간 잠들어 있던 거인의 잠재력이 마침내 해방되는 순간이었다.