Suspense, 잠재력의 해방

212025년 09월 05일3

커뮤니티의 피드백을 수용하며 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 아키텍처의 심장부로 화려하게 복귀를 선언하고 있었다. 수년간 잠들어 있던 거인의 잠재력이 마침내 해방되는 순간이었다.