티스토리 뷰

 

사용자는 기다려주지 않습니다. 통계에 따르면 페이지 로딩이 3초를 넘어가면 50% 이상의 사용자가 이탈한다고 하죠. 하지만 데이터가 방대해진 현대 웹 앱에서 '로딩'은 피할 수 없는 숙명입니다.

중요한 것은 **"얼마나 빨리 불러오는가"**만큼이나 **"기다리는 시간을 어떻게 느끼게 하는가"**입니다. 오늘은 React의 Suspense를 활용해 사용자에게 심리적 안정감을 주는 세련된 로딩 전략을 딥다이브해 보겠습니다.


1. 왜 로딩 처리가 UX의 핵심인가?

전통적인 방식에서는 데이터가 도착하기 전까지 빈 화면을 보여주거나, 화면 전체에 회전하는 스피너(Spinner)를 띄웠습니다. 하지만 이는 흐름을 뚝 끊어버리는 요소가 됩니다.

비유로 이해하기: 레스토랑의 서빙 방식

  • 전통적인 방식: 손님이 주문했는데 요리가 다 나올 때까지 주방 문을 닫아두고 아무것도 보여주지 않습니다. 손님은 요리가 만들어지고 있는지 불안해하죠.
  • Suspense 방식: 요리가 준비되는 동안 식전 빵을 먼저 내어주고, 메인 요리가 나올 자리를 미리 세팅해 둡니다. 손님은 "아, 내 요리가 곧 나오겠구나"라고 인지하며 즐겁게 기다립니다.

Suspense는 컴포넌트가 읽어 들여야 할 데이터가 아직 준비되지 않았음을 React에게 알리고, 그동안 보여줄 '대체 UI(Fallback)'를 우아하게 렌더링하는 도구입니다.


2. 핵심 개념: Suspense의 작동 원리 (Deep Dive)

Suspense는 단순한 if (loading) return <Spinner />의 문법적 설탕이 아닙니다. 핵심은 선언적 프로그래밍에 있습니다.

  1. 중단(Suspend): 컴포넌트 렌더링 중 데이터 비동기 요청을 만나면, React는 렌더링을 일시 중단합니다.
  2. 상위 전파: 중단된 상태는 상위 트리로 전파되며, 가장 가까운 Suspense 경계(Boundary)를 찾습니다.
  3. Fallback 노출: 데이터가 준비될 때까지 fallback 속성에 정의된 컴포넌트를 대신 보여줍니다.
  4. 재개: 데이터 로딩이 완료되면 React는 중단했던 지점부터 다시 렌더링을 시도합니다.

3. 실전 예제: 이커머스 상품 상세 페이지

단순한 스피너 대신, 실제 콘텐츠의 윤곽을 미리 보여주는 스켈레톤(Skeleton) UI를 Suspense와 결합해 보겠습니다.

Step 1: 스켈레톤 UI 컴포넌트 구성

사용자가 보게 될 결과물과 유사한 형태의 뼈대를 만듭니다.

JavaScript
 
// ProductSkeleton.jsx
const ProductSkeleton = () => (
  <div className="skeleton-wrapper" style={{ padding: '20px', border: '1px solid #eee' }}>
    {/* 상품 이미지가 들어갈 자리 */}
    <div style={{ width: '100%', height: '300px', backgroundColor: '#f0f0f0', marginBottom: '20px' }} />
    {/* 제목과 가격이 들어갈 자리 */}
    <div style={{ width: '60%', height: '24px', backgroundColor: '#f0f0f0', marginBottom: '10px' }} />
    <div style={{ width: '40%', height: '20px', backgroundColor: '#f0f0f0' }} />
  </div>
);

export default ProductSkeleton;

Step 2: 비즈니스 로직에 Suspense 적용

메인 페이지 컴포넌트에서 데이터 로딩이 필요한 부분만 Suspense로 감쌉니다.

JavaScript
 
import React, { Suspense, lazy } from 'react';
import ProductSkeleton from './ProductSkeleton';

// 상품 상세 컴포넌트를 Lazy Loading으로 가져옵니다.
const ProductDetail = lazy(() => import('./ProductDetail'));

const ProductPage = ({ productId }) => {
  return (
    <div className="container">
      <h1>상품 정보</h1>
      
      {/* Suspense 경계 설정: 
          ProductDetail 내부에서 API 호출이 끝날 때까지 
          ProductSkeleton을 화면에 띄웁니다. 
      */}
      <Suspense fallback={<ProductSkeleton />}>
        <ProductDetail productId={productId} />
      </Suspense>

      <section className="reviews">
        <h2>사용자 리뷰</h2>
        {/* 리뷰 섹션은 별도의 경계를 가져갈 수 있어, 상품 정보와 독립적으로 로딩됩니다. */}
        <Suspense fallback={<p>리뷰를 불러오는 중...</p> Loading...}>
          <ReviewList productId={productId} />
        </Suspense>
      </section>
    </div>
  );
};

4. 트러블슈팅: 무한 로딩과 워터폴 현상

개발 중 흔히 겪는 두 가지 골칫덩이를 해결해 봅시다.

1) "데이터가 안 들어왔는데 Fallback이 안 보여요!"

Suspense는 Promise를 던지는(Throw) 라이브러리(예: TanStack Query, SWR, 또는 Relay)와 함께 사용할 때 진가를 발휘합니다. 일반적인 useEffect 내부의 fetch는 Suspense가 감지하지 못하므로, 반드시 useSuspenseQuery 같은 전용 훅을 사용하세요.

2) 워터폴(Waterfall) 현상

여러 개의 Suspense를 중첩해서 사용하면, A가 끝나야 B가 시작되는 직렬 로딩이 발생할 수 있습니다.

  • 해결책: 서로 의존성이 없는 데이터라면 상위에서 Promise.all로 데이터를 한 번에 가져오거나, 레이아웃에 맞게 Suspense 경계를 적절히 분리해야 합니다.

5. 기술적 고려사항 (Trade-offs)

모든 기술에는 비용이 따릅니다. Suspense 도입 전 아래 내용을 체크해 보세요.

장점 단점 및 고려사항
코드 가독성: 로딩 상태를 분기문(if) 없이 선언적으로 관리 가능. SSR 복잡도: 서버 사이드 렌더링(Next.js 등) 환경에서 스트리밍 설정 필요.
사용자 경험: 화면 전체가 깜빡이지 않고 필요한 부분만 자연스럽게 로드됨. 라이브러리 의존성: Suspense를 지원하는 데이터 페칭 라이브러리 선택이 필수적임.
관심사 분리: UI 구성 요소와 로딩 로직을 완전히 격리함. 네트워크 지연: 너무 짧은 로딩에 Fallback을 보여주면 오히려 화면이 번쩍거리는 느낌을 줄 수 있음.

요약 및 제언

React Suspense는 단순한 기능이 아니라 **'기다림의 미학'**을 설계하는 철학입니다. 사용자는 무조건 빠른 것을 원하기보다, 현재 무슨 일이 일어나고 있는지 명확히 아는 것에 더 안도감을 느낍니다.

현재 프로젝트의 로딩 화면을 점검해 보세요. 단순히 돌아가는 동그라미(Spinner)만 보여주고 있지는 않나요? 가장 중요한 콘텐츠가 들어갈 자리에 스켈레톤 UI를 배치하는 것만으로도 서비스의 완성도는 한 차원 높아질 것입니다.

반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함