티스토리 뷰

사용자는 기다려주지 않습니다. 통계에 따르면 페이지 로딩이 3초를 넘어가면 50% 이상의 사용자가 이탈한다고 하죠. 하지만 데이터가 방대해진 현대 웹 앱에서 '로딩'은 피할 수 없는 숙명입니다.
중요한 것은 **"얼마나 빨리 불러오는가"**만큼이나 **"기다리는 시간을 어떻게 느끼게 하는가"**입니다. 오늘은 React의 Suspense를 활용해 사용자에게 심리적 안정감을 주는 세련된 로딩 전략을 딥다이브해 보겠습니다.
1. 왜 로딩 처리가 UX의 핵심인가?
전통적인 방식에서는 데이터가 도착하기 전까지 빈 화면을 보여주거나, 화면 전체에 회전하는 스피너(Spinner)를 띄웠습니다. 하지만 이는 흐름을 뚝 끊어버리는 요소가 됩니다.
비유로 이해하기: 레스토랑의 서빙 방식
- 전통적인 방식: 손님이 주문했는데 요리가 다 나올 때까지 주방 문을 닫아두고 아무것도 보여주지 않습니다. 손님은 요리가 만들어지고 있는지 불안해하죠.
- Suspense 방식: 요리가 준비되는 동안 식전 빵을 먼저 내어주고, 메인 요리가 나올 자리를 미리 세팅해 둡니다. 손님은 "아, 내 요리가 곧 나오겠구나"라고 인지하며 즐겁게 기다립니다.
Suspense는 컴포넌트가 읽어 들여야 할 데이터가 아직 준비되지 않았음을 React에게 알리고, 그동안 보여줄 '대체 UI(Fallback)'를 우아하게 렌더링하는 도구입니다.
2. 핵심 개념: Suspense의 작동 원리 (Deep Dive)
Suspense는 단순한 if (loading) return <Spinner />의 문법적 설탕이 아닙니다. 핵심은 선언적 프로그래밍에 있습니다.
- 중단(Suspend): 컴포넌트 렌더링 중 데이터 비동기 요청을 만나면, React는 렌더링을 일시 중단합니다.
- 상위 전파: 중단된 상태는 상위 트리로 전파되며, 가장 가까운 Suspense 경계(Boundary)를 찾습니다.
- Fallback 노출: 데이터가 준비될 때까지 fallback 속성에 정의된 컴포넌트를 대신 보여줍니다.
- 재개: 데이터 로딩이 완료되면 React는 중단했던 지점부터 다시 렌더링을 시도합니다.
3. 실전 예제: 이커머스 상품 상세 페이지
단순한 스피너 대신, 실제 콘텐츠의 윤곽을 미리 보여주는 스켈레톤(Skeleton) UI를 Suspense와 결합해 보겠습니다.
Step 1: 스켈레톤 UI 컴포넌트 구성
사용자가 보게 될 결과물과 유사한 형태의 뼈대를 만듭니다.
// 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로 감쌉니다.
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를 배치하는 것만으로도 서비스의 완성도는 한 차원 높아질 것입니다.
'Frontend > Next.js' 카테고리의 다른 글
| Dynamic Routes: 동적 파라미터([id]) 처리와 상세 페이지 (0) | 2026.03.13 |
|---|---|
| Error Handling: error.js와 not-found.js 구현 (0) | 2026.03.13 |
| page.js에서 데이터 페칭(Fetching)과 캐싱 전략 (0) | 2026.03.12 |
| 서버 컴포넌트(Server)와 클라이언트 컴포넌트(Client)의 구분과 조합 (0) | 2026.03.12 |
| Link 컴포넌트와 프로그래밍 방식의 페이지 이동 (useRouter) (0) | 2026.03.12 |
- Total
- Today
- Yesterday
- TypeScript
- 구글
- HTML
- It용어
- SSR
- on-device ai
- Nextjs
- 스마트안경
- Javascript
- AI
- sLLM
- CSR
- react
- LLM
- 엣지컴퓨팅
- prompt engineering
- java
- HBM
- 카카오
- 웹기초
- 멀티모달
- Rag
- MSA
- 협력
- CSS
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |