시리즈에서 Suspense가 어떻게 동작하는지(3편), 그리고 서버에서 스트리밍의 절단선이 되는지(6편) 다뤘다. 그런데 동작 원리를 이해하고 나면 자연스럽게 다음 질문이 따라온다.
"그래서 경계를 어디에, 얼마나 잘게 그어야 하는가?"
공식 문서에 "이 단위로 쪼개라"는 정량적 규칙은 없다. 하지만 실무에서 통용되는 판단 기준은 꽤 명확하다. 이 글에서 그 기준들을 정리한다.
양극단의 실패를 보면 기준이 보인다
너무 잘게 쪼갠 경우: 팝콘 UI
카드 하나하나, 텍스트 한 줄마다 경계를 치면 화면 곳곳에서 스켈레톤이 제각각 팡팡 터지듯 채워진다. 3편에서 말한 "스피너 지옥"이 스켈레톤 지옥으로 이름만 바뀐 것이다. 시각적으로 어수선하고, 레이아웃이 계속 들썩이며(CLS 악화) 사용자가 어디를 봐야 할지 모르게 된다.
너무 크게 잡은 경우: 인질극
6편에서 본 그대로다. 페이지 전체가 하나의 경계면 가장 느린 데이터가 전체를 인질로 잡는다.
기준은 이 사이 어딘가에 있고, 그 "어딘가"를 정하는 원칙이 다음 다섯 가지다.
기준 1: 사용자에게 의미 있는 콘텐츠 단위로 긋는다
가장 중요한 원칙이다. 경계는 기술적 단위(컴포넌트, fetch 호출)가 아니라 사용자가 인지하는 단위로 그어야 한다.
디자이너에게 "이 페이지에서 뭐가 한 덩어리인가?"라고 물었을 때 나올 답 — 상품 정보 영역, 리뷰 영역, 추천 영역 — 그것이 경계 후보다. 리뷰 영역 안에서 별점 따로, 리뷰 텍스트 따로 뜨는 것은 사용자에게 아무 의미가 없다. 오히려 "반쯤 로딩된 리뷰"라는 어정쩡한 상태만 보여준다.
실용적인 리트머스 시험지:
"이 부분에 스켈레톤을 디자인해 달라고 요청할 수 있는가?"
스켈레톤으로 그릴 수 있는 덩어리면 경계 후보이고, 그리기 애매하면 더 큰 단위에 묶여야 한다는 신호다.
기준 2: 속도 차이가 나는 지점에 긋는다
경계의 존재 이유는 "빠른 것이 느린 것을 기다리지 않게" 하는 것이다. 뒤집으면, 속도가 비슷한 것들 사이에는 경계가 필요 없다.
- Header(0ms)와 PostList(200ms) 사이 → 긋는다
- 같은 DB에서 각각 50ms에 오는 두 목록 사이 → 긋지 않는다. 어차피 거의 동시에 도착하는데 경계를 나누면 스켈레톤만 두 벌이다
- 200ms짜리와 2초짜리 사이 → 반드시 긋는다. 가장 가치 있는 절단선이다
경계는 균등 분할이 아니라 지연 시간의 단층선을 따라 긋는 것이다. 어디가 느린지는 감이 아니라 계측(네트워크 탭, 서버 로그)으로 확인한다.
기준 3: 핵심 콘텐츠는 기다리고, 부가 콘텐츠는 넘긴다
모든 것을 스켈레톤으로 대체하는 것이 항상 좋은 것은 아니다. 사용자가 그 페이지에 온 이유인 콘텐츠는 조금 기다리더라도 완성된 상태로 보여주는 것이 나을 때가 많다.
블로그 글 페이지를 생각해 보자. 사용자는 글을 읽으러 왔다. 글 본문을 스켈레톤으로 띄우고 0.3초 뒤에 채우는 것보다, 본문은 페이지와 함께 오게 하고(경계 밖) 댓글·추천글만 경계 안에 넣는 것이 낫다. 본문이 스켈레톤이면 사용자는 어차피 아무것도 할 수 없으므로, 그 스켈레톤은 "빨리 보여준 척"에 불과하다.
경계 밖 = "이것은 페이지의 본체다, 이것 없이는 의미 없다" 경계 안 = "이것은 나중에 와도 페이지가 성립한다"
이렇게 보면 Suspense 경계는 사실 콘텐츠 우선순위 선언이다. 6편에서 "경계를 긋는 것이 곧 UX 설계"라고 한 말의 구체적인 의미다.
기준 4: 함께 의미가 완성되는 데이터는 한 경계에 묶는다
주문 요약 영역에 상품 목록, 배송비, 총액이 있다고 하자. 이 셋을 각각 경계로 나누면 "상품은 보이는데 총액은 스켈레톤"인 순간이 생긴다. 사용자가 잘못된 판단을 할 수 있는 불완전한 진실의 상태다.
서로를 참조해야 의미가 완성되는 데이터라면, fetch가 몇 개든 하나의 경계 안에서 함께 나타나야 한다. 반대로 리뷰와 추천 상품처럼 서로 독립적인 정보는 따로 도착해도 문제없다.
데이터의 의미적 결합도가 경계의 결합도를 결정한다.
기준 5: fallback은 실물과 같은 크기여야 한다
경계를 그었다면, fallback이 실제 콘텐츠와 같은 자리·같은 크기를 차지해야 한다. 콘텐츠가 도착할 때 페이지가 덜컥 밀리면(layout shift) 사용자가 클릭하려던 버튼이 움직여 오클릭이 난다. Core Web Vitals의 CLS 점수도 깎인다.
이것은 기준 1과 연결된다. 크기를 예측할 수 있는 의미 단위여야 제대로 된 스켈레톤을 만들 수 있다. fallback의 크기를 정할 수 없다면 경계 위치가 잘못됐다는 신호일 수 있다.
보너스: 갱신 상황에서는 경계가 답이 아니다
이미 보이는 콘텐츠를 갱신하는 상황(탭 전환, 검색, 필터링)에서 스켈레톤이 번쩍인다면, 반사적으로 경계를 조정하려 하기 쉽다. 하지만 그것은 경계 문제가 아니라 발동하지 말아야 할 상황에 fallback이 발동한 것이다.
답은 3편에서 다룬 startTransition / useDeferredValue로 이전 화면을 유지하는 것이다. 경계는 "처음 나타나는 콘텐츠"를 위한 도구다.
한 장 요약
| 질문 | 판단 |
|---|---|
| 스켈레톤을 디자인할 수 있는 덩어리인가? | No면 더 큰 단위로 |
| 주변과 로딩 속도가 유의미하게 다른가? | 비슷하면 경계 불필요 |
| 사용자가 이 페이지에 온 목적인가? | 그렇다면 경계 밖 (기다려서 완성체로) |
| 서로 있어야 의미가 완성되는 데이터인가? | 그렇다면 한 경계에 묶기 |
| fallback 크기를 예측할 수 있는가? | 없다면 경계 위치 재고 |
| 첫 로딩인가, 갱신인가? | 갱신이면 경계가 아니라 transition |
관통하는 원칙은 하나다.
Suspense 경계는 코드의 구조가 아니라, 사용자가 화면을 읽는 구조를 따라간다.
컴포넌트가 10개든 fetch가 5개든, 사용자 눈에 "한 덩어리"면 경계도 하나다.