빈 상태
보여 줄 콘텐츠가 없을 때 사용자를 안내하는 방법이다. 빈 상태는 막다른 화면을 다음 행동으로 이어지는 출발점으로 바꾼다. 무슨 일이 있었는지 설명하고, 앞으로 어떻게 되는지 알려 주고, 다음에 할 일을 제안한다.
돈을 다루는 앱에서 빈 화면을 보면 사용자는 내 데이터가 사라졌나? 하고 걱정부터 한다. "you have no transfers yet"와 "we couldn't load your transfers"는 하나는 온보딩이고 하나는 사용자를 당황하게 만드는 순간이다. 두 화면이 비슷해 보이면 절대 안 된다.
유형
Senco의 빈 상태는 네 가지이고, 유형마다 어조와 목적이 다르다.
| Type | When it appears | Tone |
|---|---|---|
| First-use | 아직 데이터가 없다 (새 계정, 거래 없음) | 격려, 온보딩 |
| No results | 검색이나 필터 결과가 없다 | 도움, 제안 |
| Error | 데이터를 불러오지 못했다 | 안심, 해결 방법 제시 |
| Cleared | 모든 항목을 처리했다 (대기 작업 없음) | 긍정, 축하 |
다른 유형을 잘못 보여 주지 않는다
네 상태는 겉모습이 비슷하지만 뜻은 정반대다. 다음 두 규칙으로 가장 심각한 혼동을 막는다.
- 로딩 중에 first-use를 보여 주지 않는다. 요청이 느린 사이에 "No transactions yet"가 뜨면, 송금 내역이 200건 있는 사용자에게 내역이 모두 사라졌다고 말하는 셈이다. 로딩 중에는 스켈레톤을 보여 주고, 빈 상태는 응답이 실제로 비어 있다고 확인한 뒤에만 보여 준다.
- 에러가 났을 때 first-use를 보여 주지 않는다. 요청이 실패했다면 빈 계정이 아니라 다시 시도할 수 있는 에러 상태를 보여 줘야 한다.
이 페이지에서 가장 중요한 규칙이다. 디자인 문제라기보다 타이밍 버그에 가깝지만, 디자인을 정의하는 곳이 여기라서 이 문서에 적어 둔다.
구조
모든 빈 상태는 같은 구조를 따른다.
- 일러스트나 아이콘 — 맥락에 맞는 시각 요소 (아무 데나 쓰는 범용 이미지는 피한다)
- 제목 — 지금 상황을 짧고 분명하게 말하는 문장
- 설명 — 왜 이런 상태인지, 무엇을 하면 되는지 알려 주는 한두 문장
- CTA 버튼 — 빈 상태를 벗어나게 해 주는 주요 액션 ("cleared" 유형은 생략 가능)
아이콘은 이미지 파일이 아니다
빈 상태의 시각 요소는 따로 불러오는 에셋이 아니라 인라인으로 그리는 아이콘이다. 네트워크가 실패해서 나타난 빈 상태가 일러스트를 그리려고 다시 네트워크에 의존할 수는 없다. Design principles를 참고한다.
첫 사용 빈 상태
가장 중요한 빈 상태다. 새 Senco 사용자는 첫 송금을 하기 전 거래 내역에서 이 화면을 본다. 반갑게 맞이하는 느낌을 주고 첫 액션으로 자연스럽게 이끌어야 한다.
- 아이콘: 송금과 관련된 친근한 일러스트
- 제목: "No transactions yet"
- 본문: "Once you send or receive a transfer, it will appear here."
- CTA: "Send your first transfer"
새 사용자에게는 제품 전체에서 가장 영향력이 큰 화면이다. 빈 상태가 곧 온보딩이 되는 유일한 순간이고, 여기의 CTA가 앱 전체의 주요 액션이다.
No transactions yet
Once you send or receive a transfer, it will appear here.
결과 없음
검색이나 필터 결과가 하나도 없을 때 나타난다. 사용자가 막혀 있지 않도록 조건을 바꿔 볼 수 있게 돕는다.
- 아이콘: 검색을 떠올리게 하는 시각 요소
- 제목: "No transfers found"
- 본문: "Try adjusting your search terms or clearing filters."
- CTA 버튼은 필요 없다. 검색 입력창이 곧 액션 영역이다
검색어를 보여 주고 되돌릴 방법을 준다
"No transfers found"보다 "No transfers found for maria"가 더 쓸모 있다. 시스템이 실제로 무엇을 찾았는지 보여 주기 때문에 사용자가 오타를 발견할 수 있다. 필터가 걸려 있으면 어떤 필터인지 알려 주고 해제할 수 있게 한다: "No transfers found · 2 filters active · Clear filters".
필터 해제 액션은 이름만 다를 뿐 사실상 CTA다. 이게 없으면 필터를 세 개 건 사용자가 무엇을 설정했는지 직접 기억해 내야 한다.
No transfers found
Try adjusting your search terms or clearing your filters.
에러 상태
데이터를 불러오지 못했다면 그대로 솔직하게 말하고 해결 방법을 제시한다. 사용자 탓으로 돌리지 않는다.
- 아이콘: 경고 시각 요소
- 제목: "Something went wrong"
- 본문: "We couldn't load your transfers. Check your connection and try again."
- CTA: "Try again"
돈이 어떤 상태인지 알려 준다
송금 앱의 에러 상태는 실패를 설명하는 것으로 끝나지 않는다. 사용자의 돈이 지금 어떤 상태인지도 알려 줘야 한다. "We couldn't load your transfers — nothing has changed and your balance is unaffected"라고 하면 사용자가 진짜 궁금해하는 질문에 답이 된다.
오프라인은 따로 다룬다
오프라인은 일반적인 에러와 다르다. 원인도 해결 방법도 분명하고, 대개 보여 줄 캐시 데이터도 있다.
You're offline — showing your transfers as of 3 minutes ago. New transfers will appear when you reconnect.
시각이 표시된 캐시 콘텐츠는 빈 에러 화면보다 언제나 낫다. 이 차이로 앱이 기능을 줄여서라도 계속 동작하느냐, 그냥 멈춰 버리느냐가 갈린다.
Something went wrong
We couldn't load your transfers. Check your connection and try again.
비워진 상태
긍정적인 빈 상태로, 사용자가 처리할 일이 남아 있지 않은 경우다. 작은 보상처럼 느껴지게 한다.
- 아이콘: 체크 표시나 축하 시각 요소
- 제목: "All caught up!"
- 본문: "You have no pending transfers. Nice work."
- CTA는 필요 없다
빈 상태 중 가벼운 어조를 써도 되는 건 cleared뿐이다. 그래도 들뜨지 않게 한다. 바로 옆 화면에 실패한 결제가 있을 수 있는 제품이라 과한 축하는 눈치 없어 보인다. Brand를 참고한다.
All caught up!
You have no pending transfers. Nice work.
컴포넌트 API
EmptyState는 하위 컴포넌트 다섯 개로 이루어진 복합 컴포넌트다. @/components/ui/empty-state에서 가져온다.
하위 컴포넌트
| Component | Element | data-slot | Notes |
|---|---|---|---|
EmptyState | div | empty-state | 루트 래퍼 |
EmptyStateIcon | div | empty-state-icon | aria-hidden="true", 둥근 배경 |
EmptyStateTitle | h3 | empty-state-title | 제목 텍스트 |
EmptyStateDescription | p | empty-state-description | 보조 텍스트 |
EmptyStateAction | div | empty-state-action | CTA 컨테이너 |
크기
EmptyState 루트는 size prop으로 "default"나 "sm"을 받는다. 크기는 group-data-[size=sm]/empty-state를 통해 모든 자식에 적용된다.
| Default | Compact (sm) | |
|---|---|---|
| 패딩 | 64px / 32px | 24px / 16px |
| 아이콘 컨테이너 | 48px | 40px |
| 아이콘 크기 | 24px | 20px |
| 제목 | 16px medium | 14px medium |
| 설명 | 14px regular | 12px regular |
사용
<EmptyState>
<EmptyStateIcon><Send /></EmptyStateIcon>
<EmptyStateTitle>No transactions yet</EmptyStateTitle>
<EmptyStateDescription>
Once you send or receive a transfer, it will appear here.
</EmptyStateDescription>
<EmptyStateAction>
<Button size="sm">Send your first transfer</Button>
</EmptyStateAction>
</EmptyState>
크기 고르기
Default는 거래 내역 전체나 탭 패널 전체처럼 페이지의 한 영역을 채울 때 쓴다. Compact는 결과가 없는 수취인 검색처럼 카드나 드롭다운 안에 넣을 때 쓴다. 200px짜리 팝오버 안에 default 크기 빈 상태를 넣으면 나머지 요소가 전부 화면 밖으로 밀려난다.
디자인과 접근성
- 의미는 제목과 설명이 전달하므로 아이콘에는
aria-hidden="true"를 쓴다 - CTA 버튼은 포커스를 받을 수 있고 표준 Button 컴포넌트 API를 따른다
- 텍스트는 짧게 쓴다. 설명은 두 줄을 넘기지 않는다
- 빈 상태 컨테이너는 부모 요소 안에서 가운데 정렬하고 세로 간격을 일정하게 유지한다
- 간격은 모두 DS 토큰으로 지정하고 값을 하드코딩하지 않는다
빈 결과 알리기
검색 결과가 없거나 필터로 모든 항목이 걸러지는 등, 사용자 액션 때문에 콘텐츠가 빈 상태로 바뀌면 결과를 polite로 알린다: "No transfers found". 이 안내가 없으면 스크린 리더 사용자는 검색 후 아무 소리도 듣지 못해 검색이 실행됐는지조차 알 수 없다.
제목 수준은 글자 크기가 아니라 페이지 위계에 맞춰 정한다. EmptyStateTitle은 <h3>으로 렌더링되므로, <h2>가 있는 섹션 안에서는 맞지만 <h1> 바로 아래에서는 맞지 않다.
권장과 금지
| Do | Don't |
|---|---|
| Show a skeleton while loading | Show "No transactions yet" during a slow request |
| Distinguish error from empty | Render an empty account when the request failed |
| Say what is true of the balance in an error | Say "Something went wrong" and stop |
| Show cached data with a timestamp when offline | Show an empty screen because the network is down |
| Name the search term that found nothing | Show a bare "No results" |
| Offer "Clear filters" when filters are active | Leave the user to remember what they filtered by |
알려진 공백
- 로딩, 빈 상태, 에러를 각각 다른 상태로 정의했지만, 로딩 중에 빈 상태가 뜨지 않게 막는 장치는 컴포넌트 API로 강제되지 않고 관례에 맡겨져 있다.
- 컴포넌트에 오프라인 변형이 없다. 카피는 이 문서에만 정의돼 있다.
- 일러스트는 맥락에 맞춰야 한다고만 적혀 있고, 상태별 아이콘 세트는 정해지지 않았다.