⏳ Next.js 7장: Suspense와 Streaming — 체감 속도의 마술
📋 개요
Suspense와 Streaming을 활용해 체감 로딩 속도를 개선하는 방법을 알아봅니다.
📋 목차
- 📌 이 문서를 읽기 전에
- 🤔 왜 알아야 하는가
- 🏗️ 비유로 먼저 이해하기
- 🧩 Streaming (점진적 렌더링) 🟢
- 🛡️
Suspense의 진정한 의미 (선 렌더링, 후 데이터) 🟡 - 🚦 우아한 로딩 패턴:
loading.tsxvs<Suspense>🟡 - 💥 에러 해결 카탈로그
- 🏁 이번에 배운 내용 총정리
- 📝 마무리 퀴즈
- 🔗 더 알아보기
📌 이 문서를 읽기 전에
⏱️ 예상 읽기 시간: 15분(전체) / 핵심 파트만: 8분
🗺️ 이 문서의 흐름
RSC의 치명적 단점(병목) → Streaming 해결책 → Suspense 기반 선 렌더링 마법 → 실전 배치
🎯 이 문서를 다 읽으면 할 수 있는 것
- API 응답이 고의로 5초나 지연되는 악조건 속에서도, 사이트의 뼈대(Header, Layout) 를 0.1초 만에 띄워 체감 로딩을 줄이는 최적화를 할 수 있다.
- 예약어 파일인
loading.tsx와 수동<Suspense>경계선의 차이를 알고 상황에 맞게 버무릴 수 있다.
🗺️ 이 문서의 배경 세계관: '영수네 커뮤니티'
- 영철(새로 온 주니어): "요즘 서버 컴포넌트 정말 좋네요! 데이터도 직접 부르고... 아, 그런데 사용자가 접속하면 한 5초 동안 하얀 화면만 나오다가 갑자기 한꺼번에 퉁! 하고 다 떠요. 로딩 스피너라도 보여주고 싶은데 방법이 없을까요? 😭"
- 영호(FE 리드): "영철 님... 모든
await가 다 끝날 때까지 HTML 전송을 멈추고 기다렸잖아요! 사용자는 그동안 높은 TTFB 지연과 하얀 화면 만 보게 됩니다. 덩치 큰 대상들은<Suspense>로 감싸서, 준비된 부분부터 먼저 브라우저로 흘려보내야지(Streaming)!"
🤔 왜 알아야 하는가
과거 React(CSR)에서는 하얀 화면이 뜨는 동안 자바스크립트가 로딩 빙글빙글 스피너를 직접 그려줬어.
하지만 Next.js App Router 생태계에서는 모든 게 서버에서 그려져.
서버 컴포넌트(Page)가 DB에서 수만 건의 데이터를 끌어오는 await DB.find() 코드를 만났다고 치자.
이 쿼리가 3초가 걸린다면? Next.js 서버는 이 3초간 그 어떤 HTML 조각도 유저 브라우저로 내려보내지 않아. (All or Nothing 원칙).
그럼 유저는 브라우저 탭에 동그라미만 도는 허연 화면과 마주하고 사이트를 이탈하겠지.
이를 극복하기 위해 등장한 혁명적인 아키텍처가 바로 Streaming(스트리밍) 과 그 문을 열어주는 열쇠인 Suspense 야. 이 둘을 모르면 SSR의 늪에 빠져 오히려 CSR보다 사용자 경험(UX)을 훨씬 정리하게 돼.
🏗️ 비유로 먼저 이해하기
🧒 5살에게 설명한다면? (영수네 국밥집의 코스 서빙)
❌ 기존 SSR (주문하고 15분간 물도 안 줌)
주방장이 국밥 10분, 수육 5분 총 15분을 요리해. 그리고 15분이 완벽하게 지날 때까지 손님은 빈 테이블에서 굶어야 해. 다 되면 거대한 쟁반 하나에 음식을 몽땅 들고 와서 상에 퉁! 놓지. (All or Nothing)✅ Streaming + Suspense (나오는 대로 즉시 서빙)
주방장이 조리가 필요 없는 물과 김치(기초 레이아웃) 부터 10초 만에 갖다 줘. (Streaming!)
그리고 국밥이 놓일 빈 자리에는 "맛있게 끓이는 중...🥘" 이라는 팻말(스켈레톤 UI, Suspense Fallback) 을 올려둬.
국밥이 다 끓으면 그 빈칸에만 쏙! 끼워 넣어주고. 손님은 계속 뭔가를 먹고 있어서 지루할 틈이 없어. 이게 진정한 체감 속도 최적화야.
🧩 Streaming (점진적 렌더링) 🟢
스트리밍은 특별한 코드가 아니야. HTML의 전송 방식을 쪼개는 기술이지.
"빨리 만들어지는 HTML 덩어리부터 청크(Chunk) 단위로 쪼개서 브라우저로 흘려보내겠다!"
이 방식의 최대 장점은 TTFB(Time To First Byte, 맨 첫 번째 데이터가 브라우저에 도달하는 시간)가 극단적으로 단축된다는 거야. 사용자 눈에는 0.1초 만에 최상단 헤더(GNB)와 사이드바가 뜨기 때문에, 사이트가 "진짜 매우 빠르다!"고 착각하게 돼. (실제 데이터 로딩은 오래 걸리더라도)
이 마법의 발동 조건
Next.js 서버가 아무 곳에서나 이 스트림을 뿜어내게 할 순 없어.
Next.js 엔진에게 "지금 이 구역은 오래 걸릴 테니까, 일단 이 스켈레톤 뼈대나 로딩 글씨를 먼저 갖다 주고 나중에 데이터 오면 끼워 넣어줘!" 라고 명시적으로 명령을 내려야 해.
그 명령어가 바로 리액트의 <Suspense /> 모듈이야!
🛡️ Suspense의 진정한 의미 (선 렌더링, 후 데이터) 🟡
영철이의 문제가 큰 병목 코드부터 원인 분석을 해보자.
❌ 병목의 시작 (All or Nothing)
// app/dashboard/page.tsx
import Header from './Header' // 0.1초
import HeavyChart from './HeavyChart' // 💥 무려 5초짜리 무거운 DB 쿼리
import Footer from './Footer' // 0.1초
// 이 페이지 전체는 HeavyChart가 5초 동안 끙끙대는 동안 HTML 전송을 '멈추고 대기'한다!
export default async function DashboardPage() {
return (
<div>
<Header /> {/* 불쌍하게도 나까지 화면에 나타나지 못하고 5초를 같이 굶어야 해... */}
<HeavyChart />
<Footer />
</div>
)
}✅ 우아한 선 렌더링 (Streaming 쪼개기)
영호 리드가 HeavyChart 주변에 부적을 감았어.
// app/dashboard/page.tsx
import { Suspense } from 'react'
import Header from './Header'
import HeavyChart from './HeavyChart'
import Footer from './Footer'
export default function DashboardPage() {
// 🌟 이 페이지 컴포넌트 자체에서는 async/await (블로킹)을 완전히 제거해버렸어!
return (
<div>
{/* 1차 배달 (Streaming): Header와 Footer는 서버에서 즉시 0.1초 만에 조립되어 브라우저로 쏴진다! */}
<Header />
{/* 2차 배달 구역: Suspense 안쪽에 가둬둔 HeavyChart 대상만 서버에 남겨두고,
일단 fallback에 적힌 "차트 로딩 중..." 글씨를 1차 배달 때 끼워 보낸다. */}
<Suspense fallback={<p className="text-gray-500 animate-pulse">차트 로딩 중...📊</p>}>
<HeavyChart />
</Suspense>
<Footer />
</div>
)
}// app/dashboard/HeavyChart.tsx
// ⚠️ 반드시 데이터를 가져오는 대상(async)을 별도의 "서버 컴포넌트"로 오려내야(분리) 한다!!
export default async function HeavyChart() {
const data = await fetch('https://api.my.com/heavy', { cache: 'no-store' }); // 5초 걸림
return <div>눈부신 5초짜리 차트 결과물: {data.result}</div>
}💡 핵심 설계 기법
Suspense를 발동시키려면 렌더링을 가로막는 무거운 로직(await fetch) 을 페이지 최상단 트리에 두지 말고, 쪼개어 만든 하위 자식 컴포넌트 내부에 밀어 넣어야 해. 그래야 부모 트리는 멈추지 않고 렌더링을 이어갈 수 있으니까!
🚦 우아한 로딩 패턴: loading.tsx vs <Suspense> 🟡
Next.js는 이 Suspense 작성을 자동화해주는 꿀 단축기 파일도 지원해. 바로 예약어 loading.tsx 지.
1) 폴더 단위 자동 래핑: loading.tsx
app/dashboard/ 폴더 트리에 loading.tsx 라는 파일을 툭 던져두면,
Next.js가 몰래 뒷단에서 page.tsx 껍데기 전체를 거대한 <Suspense>로 통째로 감싸버려.
app/
└─ dashboard/
├─ layout.tsx (1차로 즉시 Streaming 되어 렌더링 완료됨)
├─ loading.tsx (2차로 page.tsx가 준비될 때까지 화면 가운데 표시됨)
└─ page.tsx (3차로 5초 뒤에 렌더링 결괏값으로 쾅! 교체됨)- 장점: 파일 하나 만들면 라우팅 이동 시 체감 속도가 우주 방어급.
- 단점: "페이지 전체"를 통째로 묶어버림. 만약 페이지 안에 1초 걸리는 A와 10초 걸리는 B가 있다면, 10초 뒤에 A와 B가 통째로 같이 툭 튀어나옴.
2) 파편 단위 수동 래핑: <Suspense>
그래서 시니어급 5년차로 갈수록 단순한 loading.tsx 에 의존하기보다는, 화면(Page) 트리를 쪼개고 여러 개의 <Suspense> 로 개별 포장하는 고급 아키텍처를 추구해.
export default function Page() {
return (
<section>
{/* 1. 리뷰 영역은 1초 만에 뜨고 스피너 종류도 다름! */}
<Suspense fallback={<SmallSpinner />}>
<FastReviewList />
</Suspense>
{/* 2. 추천 상품 영역은 5초 걸리니까 뼈대(Skeleton) UI로 따로 돌아감! */}
<Suspense fallback={<SkeletonCard />}>
<SlowRecommendList />
</Suspense>
</section>
)
}이게 바로 진정한 애플리케이션의 점진적 경험(Progressive Enhancement) 최적화야.
💥 에러 해결 카탈로그
에러 메시지가 뜨면 Ctrl+F로 검색해봐.
❌ 화면 전체 로딩이 끝날 때까지 헤더(Header)마저 안 떠요!
원인: layout.tsx나 루트 부모 깊이의 파일 최상단 영역에서 export default async function Layout() { const db = await ... } 처럼 전역 블로킹(Blocking)을 시전해버렸기 때문.
해결책: 이 거대한 await 로직이 꼭 Layout 전체 기둥을 멈추게 해야만 하는 통신인지 고민해라. 만약 그 데이터가 사이드바 프로필 그림 하나용이라면, 사이드바 컴포넌트만을 별도로 쪼개서 뽑아낸 뒤 걔한테 await 짐을 몰아주고, 부모 레이아웃에는 껍데기에 <Suspense> 하나 씌운 채 즉시 리턴하게 풀어줘라.
🏁 이번에 배운 내용 총정리
| 도구 | 스코프 크기 | 역할 및 특징 |
|---|---|---|
loading.tsx | 페이지(Page) 통째로 | URL 진입 시 페이지 전체를 하나의 Suspense 덩어리로 간주해 즉시 로딩 화면을 띄움. (가장 뼈대 레벨) |
<Suspense> | 쪼개진 개별 컴포넌트 | 화면 내부의 독립적인 느린 파편들을 각각 지연시켜 Streaming(동시 전송)을 발생시키는 세밀한 컨트롤 타워 |
layout.tsx | URL 변경 무관 보존 뼈대 | 이 뼈대는 라우팅과 무관하게 최초 로딩되어 Streaming 껍데기 역할을 하며, 웬만하면 이 안에서 전역 블로킹 await를 남발하지 말아야 함 |
📝 마무리 퀴즈
Q1. Suspense가 스트리밍에서 하는 핵심 역할은 무엇인가?
✅ 정답: 느린 컴포넌트가 준비될 때까지 해당 경계만 fallback으로 두고 나머지 UI를 먼저 보낼 수 있게 한다.
💡 상세 해설: 사용자는 전체 페이지가 끝날 때까지 빈 화면을 보는 대신, 헤더와 주요 레이아웃을 먼저 받는다. Suspense는 UX와 서버 응답 구조를 함께 바꾸는 경계다.
Q2. loading.tsx를 라우트 세그먼트에 두면 어떤 효과가 있나?
✅ 정답: 해당 세그먼트의 page와 하위 경로에 자동 Suspense fallback처럼 작동한다.
💡 상세 해설: 폴더 단위 로딩 UI를 빠르게 만들 수 있지만, 세밀한 영역별 로딩은 직접 Suspense 경계를 두는 편이 더 적절하다.
Q3. 영철이의 테스트 타임: 댓글 목록 API가 느리지만 게시글 본문은 빠르다. 어떻게 보여줄까?
✅ 정답: 게시글 본문은 먼저 렌더링하고 댓글 목록만 Suspense 스켈레톤으로 감싼다.
💡 상세 해설: 느린 데이터 하나 때문에 전체 화면을 막으면 체감 속도가 나빠진다. 스켈레톤은 실제 댓글 영역과 비슷한 크기로 만들어 레이아웃 흔들림도 줄여야 한다.
🐣 영철이의 퇴근 일기
오늘은 로딩 화면을 땜빵이 아니라 응답 시간을 설계하는 도구로 보게 됐다.
💡 "느린 데이터는 숨기는 게 아니라 경계를 세워 먼저 보여줄 것과 나중에 채울 것을 나눈다."
다음 화면 설계에서는 어떤 데이터가 사용자의 첫 이해에 필요한지부터 나누겠다.