개발 slecs

결제 대시보드에 PG 차감 순수익 KPI 실시간 폴링 추가

목차

dashboard-sr 영역에 PG 차감 순수익 KPI 카드를 추가했다. 변경 파일은 뷰/스타일 1개인데, 그 파일 하나에 들어간 판단이 생각보다 많아서 기록을 남겨둔다.

왜 이 지표가 필요했나

기존 대시보드에 표시되던 매출 숫자는 결제 총액 기준이었다. PG사에 내는 수수료를 빼지 않은 gross 수치라 실제 운영에서 쓸 수 있는 숫자와 괴리가 있었다. 정산 파악이 필요할 때마다 별도 계산을 해야 했고, 그게 반복되다 보니 아예 대시보드에 박아두는 게 맞겠다 싶었다.

단순히 기존 값에서 고정 비율을 빼는 게 아니라 쿼리 레벨에서 집계를 설계해야 했다. 일별/월별/누적 단위로 다 나와야 하고, 다른 화면에서 보이는 동일 지표와 숫자가 맞아야 한다는 제약도 있었다. UI 작업 전에 SQL부터 잡은 이유다.

구현 흐름

SQL은 대략 이런 구조로 작성했다.

SELECT
  DATE(created_at)                          AS day,
  SUM(amount)                               AS gross_revenue,
  SUM(amount * (1 - pg_fee_rate))           AS net_revenue,
  SUM(amount * pg_fee_rate)                 AS pg_fee_total
FROM payments
WHERE status = 'COMPLETED'
  AND created_at BETWEEN :start AND :end
GROUP BY DATE(created_at)
ORDER BY day DESC;

pg_fee_rate는 결제 수단별로 다를 수 있어서 payments 테이블에 컬럼으로 두는 게 맞았다. 단일 고정값으로 처리하면 나중에 수수료 구조가 바뀔 때 쿼리를 고쳐야 한다. 데이터를 처음 쌓을 때 맞는 값을 같이 저장해두면 재집계가 훨씬 편하다.

폴링은 5초로 잡았다. polling vs WebSocket을 잠깐 고민했는데, 이 화면의 갱신 성격을 생각하면 결제는 실시간 스트림이 아니라 주기적 스냅샷에 가깝다. 접속 인원도 많지 않고, 연결 유지 오버헤드를 감수할 이유가 없어서 polling이 더 단순하고 맞는 선택이었다.

방식 장점 단점 선택 기준
5초 폴링 구현 단순, 서버 상태 없음 주기 사이 데이터 지연 이 화면처럼 스냅샷 성격일 때
WebSocket 이벤트 즉시 반영 연결 관리, 인프라 복잡도 증가 채팅/주문 알림처럼 push가 필요할 때

숫자 포맷은 천단위 콤마와 원 단위로 통일했다. 같은 금액이 어느 화면에선 1234567로, 다른 화면에선 1,234,567원으로 뜨면 같은 숫자인지 한 번 더 확인하게 된다. 그 인지 비용이 쌓이면 신뢰 문제로 번진다. 포맷 유틸을 공용으로 쓰고 있어서 통일은 어렵지 않았다.

예외 처리로 챙긴 것들:

  • 집계 결과가 없는 날(신규 오픈 직후, 장애 일자 등): NULL 대신 0으로 내려오도록 쿼리에서 COALESCE 처리
  • 권한 없는 접근: 뷰에서 조건 분기가 아니라 API 레벨에서 403 처리
  • 화면 로딩 중 깜빡임: 폴링 응답이 올 때까지 이전 값 유지, 로딩 스피너는 최초 진입 때만

정합성 검증이 핵심이었다

구현 완료 후 가장 공 들인 게 숫자 맞추기였다. 기존에 다른 경로로 집계된 값과 비교했을 때 1원이라도 차이 나면 원인을 찾아야 한다.

결제 도메인에서 숫자가 틀리면 "약간 다른 것 같아요" 수준으로 넘어가지 않는다. 운영자 입장에서는 어느 화면을 믿어야 하는지 모르게 되고, 그게 반복되면 대시보드 자체를 안 보게 된다. 기능을 만든 의미가 없어지는 거다.

이번에 cross-check하면서 타임존 처리 차이가 있는 걸 발견했다. 쿼리 하나는 UTC 기준, 다른 쪽은 KST 기준으로 날짜 구분을 하고 있어서 자정 근방 데이터에서 하루치가 다르게 잡혔다. 신규 기능 작업이 아니었으면 묻혀 있었을 버그다. 이런 게 기능 추가 과정에서 발견되는 경우가 종종 있다.

작업 습관 메모

변경 전에 현재 화면 동작이나 주요 수치를 스크린샷 또는 메모로 남겨두는 편이다. 수정 후 같은 케이스로 다시 확인할 기준점이 된다. 기억에 의존하면 "예전에 이렇게 나왔던 것 같은데"가 되고, 그건 검증이 아니다.

커밋은 논리적으로 독립된 단위로 쪼개려 한다. 이번 작업도 SQL 집계 쿼리 추가, API 응답 구조 변경, 뷰 컴포넌트 추가, 포맷 유틸 수정을 별도 커밋으로 분리했다. 나중에 어느 변경에서 문제가 생겼는지 bisect할 때 단위가 작을수록 범위가 좁아진다.

커밋 메시지는 "무엇을"보다 "왜"를 담으려고 한다. KPI 카드 추가보다 PG 차감 순수익 분리 집계 - gross 기준 수치와 운영 실측치 혼용 방지가 3개월 뒤에 봤을 때 훨씬 낫다. diff는 코드에 이미 있으니까.

사내 서비스일수록 이런 습관이 중요하다. 외부 서비스는 문서화된 스펙이나 이슈 트래커가 촘촘한 경우가 많은데, 사내 도구는 맥락이 커밋 히스토리에만 남는 경우가 많아서 메시지가 부실하면 나중에 아무도 이유를 모르게 된다.

댓글 0

첫 댓글 달아줘.