대시보드 월 P&L에 판매대금·PENDING 정산 현황 추가
목차
dashboard 영역에 신규 기능을 붙였다. 월 P&L 카드에 판매대금과 PENDING 정산 현황 섹션을 추가하는 작업이었고, 변경 파일은 SQL 매퍼 하나, 뷰/스타일 파일 하나. 숫자만 보면 작은 작업 같지만 실제로는 쿼리 레벨 설계부터 화면까지 전 레이어를 건드린 작업이다.
이 기능이 필요해진 이유는 단순하다. 기존 P&L 카드에는 월별 합산 수치만 있었고, 실제로 입금 대기 중인 금액이 얼마인지-즉 PENDING 상태의 정산 현황-가 한눈에 보이지 않았다. 운영 입장에서는 "지금 현금화 가능한 금액이 얼마고, 아직 정산 전인 금액은 얼마냐"를 대시보드 한 화면에서 파악하고 싶은데 그게 안 됐던 것. UI 개선 요청처럼 보여도 근본적으로는 데이터 정의 문제다. 판매대금과 PENDING 금액을 어떤 기준으로 집계할 것인지를 먼저 확정하지 않으면 숫자를 어디다 붙여도 의미가 없다. 그래서 SQL 매퍼부터 시작했다.
쿼리 설계: 집계 기준을 코드보다 먼저 확정한다
SQL 매퍼 파일을 수정하면서 제일 오래 들여다본 건 집계 기준 자체였다. 일별, 월별, 누적 세 가지 슬라이스를 모두 커버해야 했는데, 각각 WHERE 조건과 GROUP BY가 달라진다. 단순히 SUM(amount) 를 날짜로 잘라 붙이는 게 아니라, PENDING 상태인 행과 확정된 행을 한 쿼리 안에서 분기해야 했다.
-- 월별 판매대금 + PENDING 금액 동시 집계 (상태 분기)
SELECT
DATE_FORMAT(created_at, '%Y-%m') AS ym,
SUM(amount) AS total_amount,
SUM(CASE WHEN status = 'PENDING'
THEN amount ELSE 0 END) AS pending_amount,
SUM(CASE WHEN status = 'SETTLED'
THEN amount ELSE 0 END) AS settled_amount
FROM payment_settlements
WHERE created_at >= DATE_FORMAT(NOW(), '%Y-%m-01')
AND created_at < DATE_FORMAT(NOW() + INTERVAL 1 MONTH, '%Y-%m-01')
GROUP BY ym;
이런 패턴에서 흔히 빠뜨리는 게 NULL 방어다. SUM(CASE WHEN ... THEN amount ELSE 0 END) 를 써야지, 간결해 보인다고 NULL이 섞인 컬럼을 그냥 SUM 하다가는 예상 밖 결과가 나온다. 결제 도메인에서 NULL은 "값 없음"이 아니라 "아직 확정 안 됨" 상태인 경우가 많아서 특히 주의해야 한다. NULL 하나가 SUM 전체를 NULL로 만들어버리고, 화면에 그냥 공백이 뜬다.
실행 계획도 붙여봤다. EXPLAIN 돌려서 인덱스를 타는지, 풀 스캔이 없는지 확인. 대시보드는 자주 들어오는 화면이라 쿼리 하나가 슬로우 쿼리가 되면 체감이 바로 온다. created_at 컬럼에 인덱스가 없으면 월 단위 범위 스캔이 테이블 전체를 긁게 되는데, 데이터가 쌓일수록 체감 속도 저하는 선형이 아니라 더 가파르게 온다.
폴링 주기와 숫자 포맷 - 사소해 보이는 결정들
실시간 갱신 방식으로 5초 폴링을 선택했다. SSE나 WebSocket을 쓰는 게 더 "세련된" 방법처럼 보이지만, 대시보드 용도에서는 폴링이 구현 비용 대비 효과가 더 좋다. 연결 관리, 재연결 처리, 서버 부하 분산까지 고려하면 SSE도 단순하지 않다. 5초면 사람 눈에는 충분히 실시간처럼 느껴지고, 서버 부하도 예측하기 쉽다. "초당 요청 수 = 동시 접속자 수 ÷ 5" 라는 식이 성립하니까.
방식별 트레이드오프를 정리하면:
| 방식 | 구현 복잡도 | 서버 부하 예측 | 실시간성 | 적합한 상황 |
|---|---|---|---|---|
| 폴링 | 낮음 | 쉬움 (주기 × 접속자) | 주기 단위 | 대시보드, 상태 모니터링 |
| SSE | 중간 | 중간 | 이벤트 즉시 | 알림, 단방향 스트림 |
| WebSocket | 높음 | 어려움 | 이벤트 즉시 | 채팅, 양방향 상호작용 |
숫자 포맷도 생각보다 신경 쓸 게 많다. 천단위 콤마와 원 단위 표기를 통일하는 건 단순해 보이지만, 화면마다 제각각이면 사용자 입장에서 "이 두 숫자가 같은 건지 다른 건지" 헷갈린다. 1,234,500원이 어떤 화면에서는 1234500, 다른 화면에서는 123만 4500원으로 보이면 소프트웨어 자체를 의심하기 시작한다. 결제 도메인에서 숫자 포맷 불일치는 신뢰 문제다. 이번 작업에서는 기존 뷰에서 쓰던 공통 포맷 함수를 그대로 가져다 썼다. 새 포맷 로직을 따로 짜는 건 그 자체가 불일치를 만드는 행위다.
검증: cross-check 없이 배포하지 않는다
구현 완료 후 직접 화면에서 동작을 확인했다. 이때 체크하는 항목들이 있다.
- 신규 섹션 숫자가 기존 다른 화면의 동일 지표와 일치하는가
- 데이터가 없는 케이스(월 시작 직후, 정산 내역 0건)에서 화면이 깨지지 않는가
- 권한 없는 사용자 접근 시 쿼리가 실행되지 않고 차단되는가
- PENDING 금액이 0원일 때 표기가 어색하지 않은가
- 5초 폴링이 탭 비활성 상태에서도 계속 도는지, 아니면 멈추는지 의도대로인가
특히 권한 체크는 화면 레벨에서만 막아두면 안 된다. API 레벨, 매퍼 레벨까지 방어가 있어야 한다. 대시보드처럼 민감한 지표를 보여주는 화면에서 권한 로직을 UI 조건부 렌더링 하나에만 의존하는 건 위험하다. 누군가 개발자 도구 열고 API 직접 호출하면 뚫린다.
커밋도 논리 단위로 쪼갰다. SQL 매퍼 변경, 뷰 레이아웃 추가, 스타일 적용을 각각 분리했다. 나중에 어떤 커밋에서 숫자가 틀려졌는지 추적해야 할 때 커밋이 덩어리로 묶여 있으면 git bisect도 써먹기 어렵다. 작은 커밋 습관은 디버깅 비용을 줄이는 가장 저렴한 방법이다.
사내 서비스를 오래 만들다 보면 기능 하나가 단순히 화면에 섹션 하나 붙이는 것으로 끝나지 않는다는 걸 계속 체감하게 된다. SQL 집계 기준, 상태 정의, NULL 처리, 폴링 주기, 포맷 통일, 권한 방어, 검증까지 모두 엮여 있어서 어느 하나를 빠뜨리면 숫자가 맞지 않거나 특정 케이스에서 이상한 화면이 나타난다. 그 연결고리를 머릿속에 다 들고 있어야 하는 게 이 포지션의 일이기도 하고.
댓글 0
첫 댓글 달아줘.