코드 스플리팅부터 데이터 가져오기까지, React Suspense의 여정
Emily Parker
Product Engineer · Leapcell

소개
빠르게 진화하는 프런트엔드 개발 환경에서 성능과 사용자 경험은 무엇보다 중요합니다. 사용자들은 빠르고 반응성이 좋으며 데이터가 풍부한 애플리케이션을 원하고, 동시에 끊김 없는 브라우징 경험을 기대합니다. 이를 달성하는 데 지속적인 과제 중 하나는 애플리케이션의 다양한 부분에 대한 로딩 상태를 관리하는 것이었으며, 특히 데이터 검색 및 코드 제공을 처리할 때 더욱 그렇습니다. 이 과제는 종종 복잡한 로딩 스피너, 워터폴, 그리고 이상적이지 않은 사용자 여정으로 이어집니다. 역사적으로 React는 이러한 로딩 상태를 단순화하기 위한 중요한 기능으로 React.Suspense를 도입했으며, 초기에는 코드 분할에 중점을 두었습니다. 그러나 React 생태계가 성숙하고 React 서버 컴포넌트(RSC)와 같은 새로운 패러다임이 등장하면서, Suspense는 훨씬 더 큰 임무의 중심에 자리 잡게 되었습니다. 바로 애플리케이션 스택 전체에 걸친 데이터 가져오기 조율입니다. 이러한 진화는 단순히 기술적인 일화가 아니라, React에서 비동기 작업을 인식하고 관리하는 방식에 대한 근본적인 변화를 나타내며, 궁극적으로 더 나은 성능과 개발자 친화적인 애플리케이션을 위한 길을 열어줍니다. 번들 최적화를 위한 유틸리티에서 RSC 세계의 핵심 데이터 가져오기 메커니즘으로 React.Suspense가 어떻게 전환되었는지 이해하며 이 흥미로운 여정을 자세히 살펴보겠습니다.
Suspense의 진화
Suspense의 여정을 제대로 이해하려면 먼저 관련된 핵심 개념과 상호 작용 방식을 이해하는 것이 중요합니다.
핵심 개념
- React.Suspense: 코드가 로드되거나 데이터가 도착할 때까지 "기다리게" 하고, 기다리는 동안 대체 UI(예: 로딩 스피너)를 선언적으로 표시할 수 있게 해주는 React 내장 컴포넌트입니다. 로딩 상태를 덜 혼란스럽고 더 예측 가능하게 만드는 것을 목표로 합니다.
 - React.lazy: 동적 가져오기(dynamic import)를 일반 컴포넌트처럼 렌더링할 수 있게 해주는 함수입니다. 일반적으로 
React.Suspense와 함께 사용하여 코드 분할을 구현하며, React가 필요할 때만 컴포넌트를 로드할 수 있도록 합니다. - 코드 분할(Code Splitting): 애플리케이션의 코드를 더 작은 덩어리로 나누는 기술로, 나중에 필요에 따라 로드할 수 있습니다. 이렇게 하면 처음에 다운로드해야 하는 코드 양을 줄여 애플리케이션의 초기 로드 시간을 개선합니다.
 - 데이터 가져오기(Data Fetching): 애플리케이션 내에 표시하기 위해 서버나 외부 API에서 데이터를 검색하는 프로세스입니다. 전통적으로 이는 
useEffect또는 다른 부수 효과 메커니즘으로 처리되었으며, 종종 경쟁 조건과 복잡한 로딩 로직으로 이어졌습니다. - React 서버 컴포넌트(RSC): 컴포넌트가 서버에서만 렌더링될 수 있는 혁신적인 React 패러다임으로, 번들 크기를 크게 줄이고 데이터베이스와 같은 서버 측 리소스에 대한 직접적인 액세스를 가능하게 합니다. RSC는 서버에서 한 번 실행되고 렌더링된 출력만 클라이언트에 보냅니다.
 - 스트리밍(Streaming): 전체 응답이 준비될 때까지 기다리는 대신, 데이터가 사용 가능하게 되는 대로 서버에서 클라이언트로 더 작고 연속적인 덩어리로 전송하는 기술입니다. 이는 인지된 로딩 성능을 향상시킵니다.
 
코드 분할을 위한 Suspense
초기에 React.Suspense는 주로 React.lazy와 함께 코드 분할을 위해 도입되었습니다. 아이디어는 간단했습니다. 컴포넌트를 아직 사용할 수 없으면(해당 코드 덩어리가 아직 로드되지 않았기 때문에), Suspense는 React.lazy가 던지는 Promise를 잡아 코드가 준비될 때까지 대체 UI를 표시합니다.
이 예시를 고려해보십시오:
// src/App.js import React, { Suspense, lazy } from 'react'; // AboutPage 컴포넌트를 지연 로드 const AboutPage = lazy(() => import('./AboutPage')); const HomePage = lazy(() => import('./HomePage')); function App() { return ( <div> <h1>내 앱에 오신 것을 환영합니다</h1> <Suspense fallback={<div>페이지 로딩 중...</div>}> {/* 실제 앱에서는 라우팅에 따라 조건부 렌더링을 사용할 것입니다 */} <HomePage /> <AboutPage /> </Suspense> </div> ); } export default App; // src/AboutPage.js (이것은 별도 덩어리가 될 것입니다) import React from 'react'; function AboutPage() { return ( <h2>회사 소개</h2> ); } export default AboutPage;
이 설정에서 AboutPage의 코드는 초기 번들에 포함되지 않습니다. AboutPage가 처음 렌더링되면 React.lazy는 동적 가져오기를 트리거합니다. 브라우저가 AboutPage의 JavaScript 번들을 가져오는 동안 Suspense는 이 비동기 작업을 감지하고 fallback prop을 렌더링합니다. AboutPage.js가 로드되고 실행되면 Suspense는 자동으로 AboutPage 컴포넌트 렌더링으로 전환합니다. 이는 대규모 애플리케이션의 초기 로드 시간을 크게 개선했습니다.
데이터 가져오기 메커니즘으로서의 Suspense
React 서버 컴포넌트의 등장과 Suspense에 대한 더 넓은 비전으로 인해 진정한 패러다임 전환이 이루어졌습니다. React 팀은 Suspense의 핵심 메커니즘, 즉 Promise가 해결될 때까지 기다리고 대체 UI를 표시하는 것이 코드 로드에만 국한되지 않는다는 것을 깨달았습니다. 이는 데이터 가져오기를 포함한 모든 비동기 작업으로 일반화될 수 있었습니다.
전통적인 클라이언트 측 데이터 가져오기에서는 일반적으로 다음과 같은 패턴을 사용했습니다.
import React, { useState, useEffect } from 'react'; function ProductDetails({ productId }) { const [product, setProduct] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { async function fetchData() { try { setLoading(true); const response = await fetch(`/api/products/${productId}`); if (!response.ok) { throw new Error('네트워크 응답이 정상적이지 않았습니다.'); } const data = await response.json(); setProduct(data); } catch (err) { setError(err); } finally { setLoading(false); } } fetchData(); }, [productId]); if (loading) { return <div>상품 로딩 중...</div>; } if (error) { return <div>오류: {error.message}</div>; } if (!product) { return <div>상품을 찾을 수 없습니다.</div>; } return ( <div> <h2>{product.name}</h2> <p>{product.description}</p> {/* ... 더 많은 상품 상세 정보 */} </div> ); }
이 접근 방식은 상용구 코드, 수동 로딩 상태, 컴포넌트가 순차적으로 데이터를 로드하는 잠재적인 "로딩 워터폴"로 이어집니다.
RSC, 특히 맥락에서 Suspense를 사용하면 모델이 극적으로 변경됩니다. 컴포넌트는 데이터를 직접 await할 수 있으며, 해당 데이터가 아직 준비되지 않은 경우 Suspense는 보류 중인 Promise를 "잡아" 대체 UI를 표시합니다.
RSC(실제 구현 세부 정보는 App Router에서 Next.js와 같은 프레임워크에서 처리됨)를 사용한 단순화된 예시를 고려해보십시오.
// src/components/ProductDetails.js (이것은 RSC일 수 있습니다) import React from 'react'; import { fetchProduct } from '../lib/data'; // 서버 측 유틸리티 async function ProductDetails({ productId }) { // 이 'await'는 데이터가 준비되지 않았다면 컴포넌트를 일시 중지시킵니다. // 그리고 가장 가까운 Suspense 경계가 이를 잡을 것입니다. const product = await fetchProduct(productId); if (!product) { // 참고: RSC에서는 일반적으로 찾을 수 없는 경우를 처리하거나 // 가져오는 동안 오류가 발생하면 부모 Suspense 경계가 // 일시 중지를 처리하도록 합니다. // 단순화를 위해 여기서는 null을 직접 반환합니다. return null; } return ( <div> <h2>{product.name}</h2> <p>{product.description}</p> {/* ... 더 많은 상품 상세 정보 */} </div> ); } export default ProductDetails; // src/app/page.js (루트 역할을 하는 RSC 페이지) import React, { Suspense } from 'react'; import ProductDetails from '../components/ProductDetails'; export default async function Page() { const productId = 'product-123'; // 예시 상품 ID return ( <div> <h1>상품 목록</h1> <Suspense fallback={<div>상품 상세 정보 로딩 중...</div>}> <ProductDetails productId={productId} /> </Suspense> </div> ); }
이 RSC 모델에서:
ProductDetails는async컴포넌트입니다. 서버에서await fetchProduct(productId)가 호출될 때, 데이터가 즉시 사용 가능하지 않은 경우(예: 데이터베이스 쿼리 대기 중) 이 컴포넌트의 서버 측 렌더링이 *일시 중지(suspend)*됩니다.- 서버에 있는 가장 가까운 
Suspense경계(이 경우Page.js에 있음)가 이 일시 중지를 잡습니다(catch). ProductDetails가 완료되기를 기다리는 대신, 서버는 즉시Page.js의 "쉘"과Suspense의fallbackUI를 클라이언트로 스트리밍할 수 있습니다.fetchProduct가 서버에서 해결되면, 서버는 실제 렌더링된ProductDetails컴포넌트를 스트리밍하고, 클라이언트 측 React 런타임은 자동으로fallback을 완전한 렌더링된 콘텐츠로 전환합니다.
Suspense와 RSC가 오케스트레이션하는 이 "필요한 만큼 렌더링(render-as-you-fetch)" 패턴은 클라이언트 측 로딩 워터폴을 제거하고, 인지된 지연 시간을 줄이며, 개발자가 수동으로 loading 및 error 상태를 관리하지 않고도 데이터 가져오기 로직을 필요한 곳(컴포넌트 내)에 직접 작성할 수 있도록 합니다. 컴포넌트 자체는 UI 및 데이터 종속성의 선언적 표현이 됩니다.
RSC를 사용한 Suspense의 주요 이점
- 간소화된 데이터 가져오기: 더 이상 로딩 상태를 위한 
useEffect및useState가 필요 없습니다. 데이터 가져오기는 컴포넌트 렌더링의 자연스러운 부분이 됩니다. - 클라이언트 번들 크기 감소: RSC는 서버 전용 로직, API 호출 및 데이터베이스 쿼리를 포함하여 클라이언트에 도달하지 않도록 합니다.
 - 스트리밍을 통한 인지 성능 향상: 클라이언트는 일부 데이터가 서버에서 여전히 가져오고 있더라도 훨씬 더 빠르게 의미 있는 콘텐츠(Suspense fallback)를 표시할 수 있습니다.
 - UI와 데이터의 동시 배치: 데이터 가져오기 로직은 데이터를 소비하는 컴포넌트 내에 직접 위치하여 유지 관리성과 이해도를 높입니다.
 - 로딩 워터폴 제거: 
Suspense는 서버에서 여러 데이터 조각을 병렬로 가져오고, 모든 데이터가 해결되기를 기다리는 대신 해당 데이터가 준비되는 대로 UI의 일부를 스트리밍할 수 있도록 합니다. 
결론
React.Suspense는 React.lazy와 함께 비동기 코드 로딩을 관리하기 위한 우아한 솔루션으로 시작하여 코드 분할에 대한 개발자 경험을 단순화했습니다. 그러나 React 서버 컴포넌트의 등장과 함께 그 잠재력이 완전히 발휘되었으며, 보류 중인 Promise를 우아하게 처리하는 기본 메커니즘을 통하여 데이터 가져오기 오케스트레이션을 위한 핵심 기본 요소를 만들었습니다. 컴포넌트가 선언적으로 데이터를 await하고 사용 가능한 대로 UI를 스트리밍할 수 있도록 함으로써, RSC 시대의 Suspense는 우리가 성능이 뛰어나고 직관적인 프런트엔드 애플리케이션을 구축하는 방식을 재정의합니다. 복잡한 로딩 상태 관리에 대한 부담을 개발자에서 프레임워크로 옮겨, 보다 간소화되고 효율적인 개발 워크플로우를 가능하게 합니다. 궁극적으로 Suspense는 React의 비동기 오케스트라의 중앙 지휘자로 진화하여, 더 적은 상용구 코드로 더 풍부한 사용자 경험을 가능하게 합니다.

