성능 잠금 해제: 프론트엔드 프레임워크의 렌더링 시 가져오기(Render-as-you-fetch) 패러다임
Olivia Novak
Dev Intern · Leapcell

소개
끊임없이 진화하는 프론트엔드 개발 환경에서 성능과 사용자 경험 최적화는 여전히 가장 중요한 과제입니다. 데이터 가져오기에 대한 기존 접근 방식은 종종 사용자에게 좌절감을 주는 워터폴, 빈 상태 또는 스피너를 초래합니다. 모든 데이터가 도착하기도 전에 애플리케이션이 의미 있는 UI 요소를 렌더링하기 시작하고, 데이터를 사용할 수 있게 됨에 따라 사용자에게 원활하게 콘텐츠를 스트리밍할 수 있는 세상을 상상해 보세요. 이것이 바로 Suspense와 React Server Components(RSC)와 같은 기능이 달성하고자 하는 비전이며, 그 핵심에는 강력한 패러다임이 있습니다: "렌더링 시 가져오기(Render-as-you-fetch)". 이 글은 "렌더링 시 가져오기(Render-as-you-fetch)"가 어떻게 작동하는지 자세히 살펴보고, 응답성이 뛰어나고 효율적인 웹 애플리케이션의 새로운 시대를 열어갈 것입니다.
핵심 개념 설명
"렌더링 시 가져오기(Render-as-you-fetch)"에 대해 자세히 알아보기 전에, 이 혁신적인 패턴의 길을 닦는 몇 가지 기본 개념을 명확히 해 보겠습니다.
데이터 가져오기 워터폴(Data Fetching Waterfalls): 기존 단일 페이지 애플리케이션에서는 컴포넌트가 마운트될 때 데이터를 가져오는 경우가 많습니다. 상위 컴포넌트가 데이터를 가져온 다음, 해당 하위 컴포넌트가 상위 컴포넌트의 데이터를 기반으로 자체 데이터를 가져오면, 요청의 순차적인 체인이 형성되며 이는 데이터 가져오기 워터폴이라 불립니다. 각 단계는 이전 단계가 완료될 때까지 기다리므로 로딩 시간이 길어집니다.
Suspense: React에서 도입된 Suspense는 컴포넌트가 비동기 데이터(일반적으로)를 기다리는 동안 렌더링을 "일시 중단"할 수 있게 해주는 메커니즘입니다. 빈 화면을 표시하거나 불리언 값으로 로딩 상태를 수동으로 관리하는 대신, Suspense를 사용하면 데이터가 가져와지는 동안 표시할 대체 UI(예: 스피너)를 선언적으로 지정할 수 있습니다. 데이터가 해결되면 실제 컴포넌트가 렌더링됩니다.
React Server Components (RSC): RSC는 React의 획기적인 기능으로, 서버에서 컴포넌트를 렌더링하고 결과 HTML 및 클라이언트 컴포넌트를 브라우저로 보낼 수 있습니다. 이것은 클라이언트로 전송되는 JavaScript 양을 줄이고 초기 페이지 로드 시간을 개선하며 SEO를 향상시켜 상당한 성능 이점을 제공합니다. 중요한 것은 RSC는 클라이언트와 서버 간의 경계를 모호하게 하여 더 효율적인 데이터 가져오기 전략을 가능하게 합니다.
렌더링 시 가져오기(Render-as-you-fetch) 메커니즘
"렌더링 시 가져오기(Render-as-you-fetch)" 패턴은 데이터 가져오기가 발생하는 시점과 방법을 근본적으로 변화시킵니다. 컴포넌트 렌더링이 시작된 후에(예: useEffect 훅에서) 데이터를 가져오는 대신, 데이터 가져오기는 렌더링 프로세스 이전 또는 동시에 더 일찍 시작됩니다. 그런 다음 렌더링은 진행 중인 데이터 가져오기에 구독합니다.
작동 방식은 다음과 같습니다.
- 빠른 데이터 가져오기 시작: 데이터 요청은 가능한 한 빨리, 이상적으로는 초기 렌더링 패스 이전 또는 도중에 디스패치됩니다. 이는 사용자 상호 작용, 경로 변경 또는 서버 측 사전 렌더링 중에 트리거될 수 있습니다.
- 대체 UI와 함께 즉시 렌더링(Suspense): 컴포넌트는 렌더링을 시도합니다. 필요한 데이터가 아직 사용 가능하지 않으면 "일시 중단"됩니다. React는 Suspense를 통해 사전 정의된 대체 UI를 표시합니다. 이 중요한 단계는 빈 화면을 제거하고 전체 콘텐츠가 준비되지 않았더라도 애플리케이션이 사용자에게 즉시 응답할 수 있도록 합니다.
- 데이터 및 컴포넌트 스트리밍(RSC): RSC의 맥락에서 서버는 컴포넌트 트리의 일부를 렌더링하고 결과 HTML 및 클라이언트 컴포넌트에 대한 참조를 브라우저로 스트리밍할 수 있습니다. 특정 트리 부분에 대한 데이터를 서버에서 사용할 수 있게 되면 해당 부분은 렌더링되어 스트리밍됩니다. 이를 통해 브라우저는 전체 페이지가 준비될 때까지 기다리는 대신 콘텐츠가 도착하는 대로 점진적으로 표시할 수 있습니다.
- 해결 및 다시 렌더링: 데이터 가져오기가 완료되면 Suspense는 새로 사용 가능한 데이터로 일시 중단된 컴포넌트의 다시 렌더링을 조정합니다. 이는 전체 페이지를 다시 로드하지 않고 부드럽고 동적인 사용자 경험을 제공하며 발생합니다.
Suspense를 사용한 예시
간단한 데이터 가져오기 함수와 함께 React Suspense를 사용하는 실제 예시를 살펴보겠습니다.
// 데이터 가져오기 유틸리티 const wrapPromise = (promise) => { let status = "pending"; let result; let suspender = promise.then( (r) => { status = "success"; result = r; }, (e) => { status = "error"; result = e; } ); return { read() { if (status === "pending") { throw suspender; // 컴포넌트 일시 중단 } else if (status === "error") { throw result; } else if (status === "success") { return result; } }, }; }; const fetchData = () => { console.log("사용자 데이터 가져오는 중..."); return new Promise((resolve) => { setTimeout(() => { resolve({ name: "Alice", email: "alice@example.com" }); }, 2000); // 네트워크 지연 시뮬레이션 }); }; // 가져온 데이터를 보유할 리소스 개체 let userResource; const fetchUser = () => { if (!userResource) { userResource = wrapPromise(fetchData()); } return userResource; }; // 사용자 데이터 컴포넌트 function UserProfile() { const user = fetchUser().read(); // 아직 수행되지 않은 경우 가져오기 시작하거나 일시 중단 return ( <div> <h2>사용자 프로필</h2> <p>이름: {user.name}</p> <p>이메일: {user.email}</p> </div> ); } // Suspense를 사용하는 앱 컴포넌트 export default function App() { // UserProfile이 렌더링되기 전에도 가져오기를 조기에 시작합니다. // 이것은 간단한 예시이며, 실제 앱에서는 라우터나 상위 컴포넌트에서 트리거될 수 있습니다. // fetchUser(); return ( <div> <h1>환영합니다!</h1> <Suspense fallback={<div>사용자 데이터 로딩 중...</div>}> <UserProfile /> </Suspense> </div> ); }
이 예시에서는 다음과 같습니다.
fetchUser()가 호출됩니다. 데이터가 아직 가져와지지 않았다면, Promise를 감싸서 저장합니다.UserProfile이userResource에서read()를 시도할 때, promise가 아직 처리 중이면 promise를 던집니다. 이것이 "일시 중단" 메커니즘입니다.Suspense는 이 던져진 promise를 잡고fallback을 렌더링합니다.- promise가 해결되면(2초 후), React는 실제 데이터로
UserProfile을 다시 렌더링합니다.
여기서 중요한 점은 read()가 렌더링 중에 호출되지만, 가져오기 자체는 이전에 시작되었을 수 있다는 것입니다.
React Server Components와의 적용
RSC를 사용하면 "렌더링 시 가져오기(Render-as-you-fetch)" 개념이 더욱 강력해집니다.
// app/page.js (서버 컴포넌트) import { Suspense } from 'react'; import UserDetails from './UserDetails'; async function getUserData() { // 이것은 서버에서 직접 데이터를 가져옵니다. const response = await fetch('https://api.example.com/users/1'); const data = await response.json(); return data; } export default async function Page() { // 서버 컴포넌트 렌더링 시작 시 즉시 데이터 가져오기 const userDataPromise = getUserData(); return ( <main> <h1>내 대시보드</h1> <Suspense fallback={<p>사용자 세부 정보 로딩 중...</p>}> {/* UserDetails는 prop을 통해 데이터를 받는 클라이언트 컴포넌트입니다 */} <UserDetails userDataPromise={userDataPromise} /> </Suspense> {/* 사용자 데이터에 의존하지 않는 다른 컴포넌트는 즉시 렌더링될 수 있습니다 */} <SomeOtherComponent /> </main> ); } // app/UserDetails.jsx (클라이언트 컴포넌트) // 이 클라이언트 컴포넌트는 서버 컴포넌트에서 전달된 promise를 // 읽기 위해 내부적으로 `use` 훅 또는 유사한 기능을 사용합니다. // (많은 컨텍스트에서 `use` 훅은 여전히 실험적이므로 단순화되었습니다) import { use } from 'react'; export default function UserDetails({ userDataPromise }) { const user = use(userDataPromise); // promise 읽기 (해결되지 않은 경우 일시 중단) return ( <div> <h2>안녕하세요, {user.name}!</h2> <p>귀하의 ID: {user.id}</p> </div> ); }
이 RSC 예시에서는 다음과 같습니다.
Page가 렌더링되기 시작하자마자 서버에서getUserData()가 호출됩니다.userDataPromise는UserDetails클라이언트 컴포넌트로 전달됩니다.- 클라이언트 컴포넌트는
use(userDataPromise)를 사용하며, 이는 promise에서 읽습니다. promise가 아직 해결되지 않았다면(예: 네트워크 지연 또는 데이터베이스 쿼리),UserDetails컴포넌트는 일시 중단됩니다. - 브라우저는 즉시
<h1>내 대시보드</h1>및SomeOtherComponent의 HTML과UserDetails에 대한fallback을 수신합니다. userDataPromise가 서버에서 해결되는 즉시, 서버는 완전히 렌더링된UserDetailsHTML 청크를 클라이언트로 스트리밍하여 원활하게 페이지를 업데이트할 수 있습니다. 이것이 RSC로 HTML 스트리밍의 본질입니다.
장점 및 사용 사례
Suspense 및 RSC로 강화된 "렌더링 시 가져오기(Render-as-you-fetch)" 패러다임은 몇 가지 강력한 이점을 제공합니다.
- 워터폴 제거: 데이터 가져오기를 더 일찍 병렬로 시작하여 누적 대기 시간을 크게 줄입니다.
- 향상된 사용자 경험: 사용자는 일부 데이터가 아직 로딩 중이더라도 의미 있는 콘텐츠와 즉각적인 UI 응답을 더 빨리 볼 수 있습니다. 이는 더 빠른 로딩 인식과 더 부드러운 상호 작용으로 이어집니다.
- 더 나은 성능: 특히 RSC를 사용하면 서버에서 렌더링하여 클라이언트 측 JavaScript를 줄이고 초기 로드 시간을 개선하며 데이터 소스에 더 가깝게 효율적으로 데이터를 가져올 수 있습니다.
- 간소화된 로딩 상태: 개발자는 더 이상 모든 데이터 종속성에 대해
isLoading불리언과 조건부 렌더링을 수동으로 관리할 필요가 없어 더 깨끗하고 선언적인 코드를 만들 수 있습니다. - 점진적 향상: 전통적인 웹사이트가 작동하는 방식과 유사하지만 SPA의 상호 작용성을 갖춘 방식으로 콘텐츠를 스트리밍하고 사용 가능한 대로 점진적으로 표시할 수 있습니다.
이 패턴은 다음을 위해 이상적입니다.
- 복잡한 대시보드: 다양한 패널이 다양한 데이터 소스에 의존하는 경우.
- 전자 상거래 제품 페이지: 일부 데이터가 여전히 로딩되는 동안 제품 세부 정보, 리뷰 및 관련 항목을 표시합니다.
- 소셜 피드: 사용자가 스크롤함에 따라 새 콘텐츠를 지속적으로 로딩합니다.
- 빠른 초기 로드 및 대화형 콘텐츠가 필요한 모든 애플리케이션.
결론
"렌더링 시 가져오기(Render-as-you-fetch)"는 단순한 데이터 가져오기 전략 이상입니다. 이는 대화형 웹 애플리케이션을 구상하고 구축하는 방식의 근본적인 변화입니다. 데이터 가져오기를 컴포넌트의 렌더링 수명 주기에서 분리하고 Suspense 및 React Server Components와 같은 메커니즘을 채택함으로써, 프론트엔드 프레임워크는 개발자가 탁월한 성능과 사용자 친화적인 경험을 만들 수 있도록 지원합니다. 이 패러다임은 애플리케이션이 더 빨리 의미 있는 것을 표시할 수 있도록 하여 진정한 점진적이고 반응적인 상호 작용을 제공하고 웹 개발의 미래를 재편합니다.

