0원과 집계 전을 같은 빈칸으로 두면, 대시보드는 조용히 거짓말을 합니다
프랜차이즈 ERP 의 점주 웹에 매출 탭이 있었습니다. 탭은 있는데 안이 비어 있는 화면이었습니다. 여기에 총매출·주문수·객단가 3개 카드와 채널 비중, 배달 플랫폼별 비중, 어제 매출을 채우는 1단계 작업을 했습니다.
데이터 출처는 두 갈래였습니다. 하나는 상용 POS 에서 넘어와 전날치로 적재되는 집계이고, 다른 하나는 배달 플랫폼에서 직접 수집해 오는 원천 데이터입니다. 두 갈래는 갱신 시점도 다르고, 매장별 커버리지도 다르고, 값이 없다는 말의 뜻도 서로 달랐습니다.
어려운 쪽은 값이 있을 때가 아니었습니다
작업을 시작할 때는 숫자를 어떻게 배치할지가 일의 대부분이라고 생각했습니다. 실제로 시간을 많이 쓴 곳은 반대쪽이었습니다. 숫자가 없을 때 그 자리에 무엇을 그릴지가 이 화면 설계의 중심이 됐습니다.
POS 가 깔린 매장은 216곳입니다. 그중 배달 플랫폼 연동까지 되어 있는 곳은 33곳, 15% 입니다. 나머지 85% 의 점주에게 배달 플랫폼 지표는 0 이 아니라 해당 없음입니다.
이 구분을 놓치면 화면이 조용히 거짓말을 합니다. 연동을 한 적이 없는 매장의 점주가 배달 매출 0원을 보고 이번 달 배달 주문이 아예 없었다고 읽습니다. 화면은 아무 오류도 내지 않고, 아무도 이것을 버그로 접수하지 않습니다.

빈칸을 그리는 세 가지 방법
없음을 어떻게 그릴지에는 세 갈래가 있었습니다. 구현 비용과 정직함이 서로 반대 방향으로 움직이는 선택이었습니다.
방법 | 무엇을 하는가 | 무엇이 걸리는가 |
|---|---|---|
A. 전부 0 으로 채운다 | 연동 여부와 무관하게 숫자 0 을 표시한다 | 연동이 안 된 85% 매장에 사실이 아닌 숫자를 보여준다 |
B. 전부 빈칸으로 둔다 | 값이 없으면 대시 기호 하나만 표시한다 | 거짓말은 안 하지만 점주가 다음에 무엇을 할지 모른다 |
C. 없음의 종류를 나눈다 | 상태값을 응답에 싣고 화면이 분기한다 | 문구도 테스트도 상태 종류만큼 늘어난다 |
C 를 골랐습니다. 판단 근거는 하나였습니다. 상태마다 점주가 할 행동이 달랐습니다. 연동을 신청하는 것과 배치가 돌기를 기다리는 것은 같은 일이 아닙니다.
배달 카드는 세 종류, 어제 카드는 두 종류
배달 플랫폼 카드의 상태를 세 가지로 나눴습니다.
정상 — 수집된 실적이 있고 그대로 보여준다
매핑이 없어 수집 자체가 안 되는 상태 — 계정 관리 화면으로 가는 링크를 함께 노출한다
매핑은 있으나 그 기간 실적이 0인 상태 — 숫자 0 을 그대로 보여준다
어제 매출 카드는 두 가지입니다. 정상과 아직 집계 전입니다. 상태 판정은 전부 서버에서 하고, 화면은 서버가 준 상태값에 맞는 문구를 고르기만 합니다.
한 매장만 봐서는 0원인지 미집계인지 알 수 없습니다
이 작업에서 가장 작고 가장 눈에 안 띄는 결정이 여기 있었습니다. 어제치 집계에 그 매장의 행이 없다고 하겠습니다. 이 사실은 두 가지를 뜻할 수 있습니다. 그날 휴무여서 진짜 0원이거나, 배치가 아직 돌지 않았거나입니다.
둘을 가르는 정보가 그 매장의 데이터 안에 없습니다. 그래서 판정을 전 매장을 한 번 훑는 검사로 바꿨습니다. 그 매장만 행이 없으면 휴무입니다. 전 매장에 행이 없으면 배치가 아직 안 돈 것입니다.
판정의 스코프가 데이터의 스코프보다 넓어지는 순간입니다. 한 행의 의미를 그 행만 보고 복원할 수 없을 때, 답은 그 행 바깥에 있습니다.

분모를 억지로 통일하면 음수가 나옵니다
화면에는 비중 위젯이 두 개 있습니다. 위쪽은 홀·포장·배달 비중이고 분모가 총매출입니다. 아래쪽은 배달 플랫폼별 비중이고 분모가 배달 플랫폼 합계입니다. 두 위젯은 분모가 서로 다릅니다.
처음에는 분모를 총매출 하나로 통일하려 했습니다. 그러려면 POS 의 배달 금액에서 플랫폼 합계를 빼서 기타 항목을 만들어야 했습니다. 이 방식은 검토 끝에 폐기했습니다.
배달 플랫폼에는 주문이 있는데 POS 에는 입력되지 않은 비율이 최근 60일 기준 12% 대였습니다. 그런 매장에서는 기타가 음수로 나옵니다. 비중 위젯에 마이너스 조각이 생기는 셈입니다.
대신 두 위젯의 분모를 다르게 두고, 각각의 합계 금액을 위젯에 함께 적었습니다. 하나의 분모로 억지로 묶는 것보다 두 축이 다르다는 사실을 드러내는 쪽이 정직하다고 봤습니다.

매월 1일에만 보이는 빈 화면
기본 조회 기간은 최근 한 달입니다. 이 계산에 월초 예외를 하나 넣었습니다. 매월 1일에는 기본 기간이 지난 달로 떨어지도록 했습니다.
예외가 없으면 1일에 시작일이 종료일보다 뒤인 구간이 만들어지고, 그 달 처음 들어온 사람이 빈 화면을 봅니다. 1년에 12번, 그것도 매번 그 달의 첫 접속자에게만 보입니다. 재현이 어려워서 버그로 접수되지도 않습니다.
매장 스코프도 같은 자리에서 정리했습니다. 클라이언트가 보낸 매장 값을 믿지 않고, 서버가 로그인 주체 기준으로 다시 판정합니다. 남의 매장을 조회하면 403 입니다.
검증은 단위테스트 11개와 운영 API 실측으로 했습니다. 한 매장의 한 달치 조회에서 총매출·주문수·객단가가 서로 모순 없이 맞는지, 기간 자르기가 의도대로 되는지, 매장 식별자를 생략했을 때와 남의 매장을 조회했을 때 각각 어떻게 나오는지를 확인했습니다.
값이 없다는 최소 네 가지 상태입니다
종류 | 의미 | 사용자가 할 일 |
|---|---|---|
진짜 0 | 측정했고 값이 0이다 | 없음(휴무였다는 확인) |
해당 없음 | 애초에 측정 대상이 아니다 | 없음(연결하면 대상이 된다) |
미연결 | 대상인데 수집 경로가 없다 | 연결한다 |
미집계 | 대상이고 경로도 있는데 아직 안 돌았다 | 기다린다 |
DB 는 이 넷을 전부 NULL 이나 행 없음 하나로 표현합니다. 그래서 의미를 되살리는 일은 애플리케이션 몫이 되고, 되살리는 데 필요한 정보가 그 행 안에 없는 경우가 자주 생깁니다.
여기서 남은 실용 규칙 몇 가지를 정리합니다.
상태값을 응답에 실어 보냅니다. 화면이 숫자만 보고 상태를 추론하게 두면 같은 추론을 화면마다 다시 짜게 되고, 매번 조금씩 다르게 짜입니다
분모가 다르면 다르다고 적습니다. 억지 통일은 음수처럼 불가능한 값으로 튀어나옵니다
경계 조건의 기본값이 첫인상을 만듭니다. 월초나 첫 접속처럼 드물게만 걸리는 조건일수록 사람 눈에 안 잡힙니다
모르겠음을 0 으로 반올림하지 않습니다. 반올림한 순간 사용자는 그 0 을 근거로 판단합니다
나누는 데에도 비용이 듭니다
이 설계가 늘 옳지는 않습니다. 상태가 네 개면 문구도 네 개, 테스트도 네 배입니다. 사용자가 그 구분에 따라 다른 행동을 하지 않는다면 나누지 않는 편이 낫습니다.
전사 검사도 공짜가 아닙니다. 매장 수가 수만 단위로 커지면 매 요청마다 전 범위를 훑는 판정은 유지되지 않습니다. 그때는 배치가 자기 실행 사실을 따로 기록하고 화면이 그 기록을 읽는 쪽으로 옮기게 됩니다. 지금 규모라서 성립하는 선택입니다.
분모를 나눈 것도 완전한 해결은 아닙니다. 합계 금액을 병기해 완화했을 뿐이고, 두 위젯을 합산해서 읽는 점주가 있다면 다시 설계할 부분입니다.
그리고 이 이야기 전체는 화면을 보는 사람이 조치 주체일 때 성립합니다. 사내 모니터링 화면처럼 보는 사람과 고치는 사람이 같다면 상태를 나눠 안내할 이유가 줄어듭니다.
빈칸을 설계하기 전에 확인한 것들
이 화면의 빈칸에 원인이 몇 가지인지 먼저 세어봅니다
원인마다 사용자가 할 행동이 다른지 확인합니다. 같으면 나누지 않습니다
상태 판정에 필요한 정보가 그 데이터 안에 있는지 봅니다. 없으면 판정 범위를 넓힙니다
비율 위젯이 둘 이상이면 분모가 같은지 확인하고, 다르면 다르다고 화면에 적습니다
기본 조회 기간이 월초 같은 경계에서 뒤집히지 않는지 확인합니다
화면은 문구를 고르고, 판정은 서버가 합니다
1단계 작업을 끝내고 남은 것은 카드 몇 개가 아니라 상태 분류였습니다. 배달 카드 세 종류, 어제 카드 두 종류, 판정 로직은 서버에 있고 화면은 상태값에 맞는 문구를 고릅니다.
값이 있을 때의 화면은 대체로 한 가지 모양입니다. 값이 없을 때의 화면이 몇 가지 모양인지는 세어보기 전에는 모릅니다. 이 화면에서는 네 가지였습니다.