마케팅 데이터 분석

취소와 부분 환불을 반영한 순매출 계산

결제 전 취소, 전액·부분 환불, 처리 중·실패 상태를 구분해 순매출을 계산합니다. 같은 환불의 상태 이력을 중복 차감하지 않는 방법과 기준일별 순매출을 합성 데이터·Python 예제로 재현합니다.

합성 결제 원장에는 200,000원이 있고, 환불 상태 기록의 금액을 모두 더하면 163,000원입니다. 그대로 빼면 순매출이 37,000원이 됩니다. 하지만 같은 환불의 진행 상태를 여러 번 기록한 행과 아직 처리 중인 요청까지 포함한 계산입니다. 이 예제의 차감 기준을 적용하면 환불액은 125,000원, 순매출은 75,000원입니다.

순매출은 총매출에서 환불을 빼는 계산보다 앞서 원천의 금액과 상태를 해석해야 합니다. 아래 예제는 단일 상점의 원화 결제와 환불을 단순화한 합성 데이터입니다. 결제기관의 실제 데이터나 고객 정보를 사용하지 않았습니다.

결제 전 취소와 결제 후 환불을 구분해야 하는 이유

결제를 완료하지 않은 50,000원 주문이 취소됐다면 성공 결제 원장에는 50,000원이 들어오지 않습니다. 여기서 취소액 50,000원을 환불처럼 차감하면 존재하지 않았던 매출에서 돈을 빼게 됩니다.

반대로 100,000원을 결제한 뒤 전액 환불한 주문은 총매출 100,000원과 환불 100,000원을 모두 설명할 수 있어야 합니다. 현재 주문 상태가 취소라는 이유로 원래 결제를 지운 뒤 환불까지 차감하면 결과가 잘못됩니다.

같은 취소 표시라도 결제 사실은 다를 수 있습니다o404 · 50,000원 주문, 결제 전 취소성공 결제 0원 − 환불 0원순매출 0원성공 결제 원장에 들어가지 않습니다.o401 · 결제 후 전액 환불100,000원 − 100,000원순매출 0원원래 결제와 환불을 모두 보존합니다.합성 데이터 · 현재 주문 상태만으로 과거 성공 결제를 지우지 않습니다.
순매출이 같아도 결제와 환불의 규모는 다릅니다. 두 값을 따로 남겨야 대사가 가능합니다.

결제 API도 결제 전 취소와 성공 결제 후 환불을 구분합니다. Stripe는 완료 전 결제를 취소하는 경로와 성공한 결제의 전액·부분 환불을 구분해 안내합니다. 실제 연동에서는 사용 중인 결제 수단과 상태에 맞는 원장을 확인해야 합니다. Stripe 결제 취소와 환불 안내

부분 환불은 주문 번호와 환불 번호를 함께 봐야 한다

9월 2일에 결제한 합성 주문은 다음과 같습니다. 표의 상태 설명은 9월 5일 자정까지 확보한 정보를 기준으로 합니다. 금액은 단순화를 위해 같은 상품 금액 기준으로 맞췄으며 세금·배송비·수수료·환율을 별도로 계산하지 않습니다.

주문결제 ID성공 결제이후 확인한 내용
o401p401100,000원전액 환불 1건
o402p40260,000원10,000원과 15,000원의 부분 환불 2건
o403p40340,000원5,000원 환불 실패, 8,000원 환불 처리 중
o404없음0원주문 금액 50,000원, 결제 전 취소

o402에는 서로 다른 환불 r402와 r403이 있습니다. 두 건이 각각 완료됐으면 10,000원과 15,000원을 더한 25,000원이 환불액입니다. 주문 번호만 기준으로 한 행을 남기면 정상적인 두 번째 부분 환불을 잃습니다.

반대로 r402 하나의 상태가 여러 번 수집된 경우에는 각 행의 금액을 더하면 안 됩니다. 환불이 10,000원에서 20,000원으로 늘어난 것이 아니라, 같은 10,000원 요청의 상태를 다시 확인한 기록일 수 있습니다.

환불 상태 스냅샷을 더하면 왜 금액이 부풀어 오르나

환불 r402의 상태 기록은 세 행입니다. recorded_at은 원천과 대조해 상태를 확보한 시각이고, 금액은 각 스냅샷이 설명하는 환불 요청의 금액입니다.

환불 ID상태 확인 시각상태금액기록 의미
r4029월 2일 13:00pending10,000원처리 중
r4029월 3일 13:00succeeded10,000원성공 상태 확인
r4029월 3일 13:00succeeded10,000원같은 스냅샷 재수집
같은 환불의 상태 기록을 새 환불로 더하지 않습니다r402 · 처리 중10,000원r402 · 성공10,000원r402 · 같은 기록 재수집10,000원반영할 환불은 r402 한 건의 10,000원입니다.합성 스냅샷 · 별도 환불 r403의 15,000원은 추가로 반영합니다.
환불 ID가 다르면 별도 요청이고, 같은 ID의 여러 행은 상태 이력인지 확인해야 합니다.

이 예제의 환불 스냅샷은 총 8행입니다. r401 한 행, r402 세 행, r403 한 행, r404의 처리 중·실패 두 행, r405의 처리 중 한 행입니다. 원시 행의 금액 합계 163,000원은 이력과 재수집을 포함하므로 차감할 환불액으로 사용할 수 없습니다.

원천이 누적 환불액을 제공하는 경우도 구분해야 합니다. o402의 누적 환불액이 10,000원에서 25,000원으로 바뀐 두 스냅샷이라면, 둘을 더한 35,000원을 환불로 해석하면 안 됩니다. 건별 증분 원장에서는 10,000원과 15,000원을 더하고, 누적 스냅샷에서는 기준 시점의 25,000원을 읽는 식으로 데이터의 의미에 맞춰야 합니다.

처리 중이거나 실패한 환불은 어떻게 반영하나

환불 요청과 성공 상태를 같은 금액으로 취급하지 않습니다. Stripe의 Refund 객체에는 pending, requires_action, succeeded, failed, canceled 상태가 있으며, 실패한 환불의 잔액 조정 정보도 따로 제공합니다. 상태가 한 번 기록됐다는 사실만으로 환불이 항상 같은 상태로 유지된다고 가정해서는 안 됩니다. Stripe Refund 객체의 상태

예제는 기준일까지 확보한 환불 ID별 최신 상태가 succeeded인 금액만 순매출에서 차감하도록 정했습니다. 처리 중과 추가 조치 필요 상태는 대기 금액으로, 실패와 취소 상태는 별도로 모읍니다. 이는 분석에 사용할 상태 정책이며, 고객 계좌의 실제 입금이나 회계상 매출 인식까지 확인했다는 의미는 아닙니다.

환불 ID연결된 주문기준일의 최신 상태요청 금액순매출 차감
r401o401succeeded100,000원100,000원
r402o402succeeded10,000원10,000원
r403o402succeeded15,000원15,000원
r404o403failed5,000원0원
r405o403pending8,000원0원

따라서 성공 환불은 125,000원, 처리 중은 8,000원, 실패는 5,000원입니다. 처리 중 8,000원이 나중에 성공하면 순매출을 다시 계산해야 합니다. 실패한 요청도 재시도나 다른 처리 경로가 생길 수 있으므로 원본 이력을 지우지 않습니다.

같은 구매일의 순매출이 기준일마다 달라지는 이유

구매일을 고정해도 환불 정보를 얼마나 확보했는지에 따라 분석 값이 달라집니다. 9월 2일에 결제된 같은 주문 세 건을 기준일만 바꾸어 계산한 결과입니다. 모든 기준일은 한국 시간 자정입니다.

같은 9월 2일 주문을 기준일만 바꿔 계산한 순매출9월 3일 자정200,000원9월 4일 자정90,000원9월 5일 자정75,000원새 구매일의 매출 추이가 아니라 같은 주문의 환불 반영 범위가 달라진 결과입니다.합성 데이터 · 막대 길이는 순매출에 비례 · 당시 확보한 스냅샷만 사용
구매일과 데이터 반영 기준일을 함께 기록해야 과거 보고서를 다시 계산할 수 있습니다.

9월 3일 자정에는 r402의 처리 중 상태만 확인돼 성공 환불을 차감하지 않습니다. 9월 4일 자정에는 r401과 r402가 성공 상태여서 110,000원을 차감합니다. 9월 5일 자정에는 별도 부분 환불 r403의 15,000원이 추가돼 순매출이 75,000원이 됩니다.

과거 보고서를 만들면서 미래에 확보한 최신 상태까지 사용하면 당시 알고 있던 정보와 다른 결과가 됩니다. 먼저 기준일까지 확보한 스냅샷으로 범위를 제한하고, 그 안에서 환불 ID별 최신 상태를 골라야 합니다.

같은 날짜에 뽑은 리포트라도 오래된 구매 코호트와 최근 구매 코호트는 환불을 관측한 기간이 다릅니다. 최근 유입의 환불률이 낮다는 이유만으로 고객 품질이 더 좋다고 결론 내리기 전에 관측 기간을 맞춰야 합니다.

Python에서 환불 ID별 최신 상태를 선택하는 방법

예제는 성공 결제 사실과 환불 상태 스냅샷을 별도 입력으로 받습니다. 현재 주문 상태는 결제를 제거하는 필터로 사용하지 않습니다. 환불은 원래 결제 ID에 연결하고, 환불 ID·금액·통화가 같은 요청이라는 조건을 확인합니다.

핵심 선택 순서는 다음과 같습니다. recorded_at은 원천과 대조해 확보한 스냅샷 시각이며, 단순히 웹훅이 늦게 도착한 시각으로 대체하면 안 됩니다.

recorded_at = instant(row["recorded_at"])
if recorded_at > cutoff:
    continue

# 식별자·금액·상태와 같은 시각의 충돌을 검증한 뒤
if key not in latest or recorded_at > latest[key]["recorded_at"]:
    latest[key] = snapshot

웹훅이 순서 없이 도착할 수 있는 수집 구조에서는 원천의 버전이나 권위 있는 조회 결과를 통해 상태를 대사해야 합니다. 이 예제는 그런 대사를 마친 상태 스냅샷이라는 입력 계약을 둡니다. 실제 결제기관의 웹훅 처리기를 구현한 코드로 해석하지 않습니다.

환불 ID별 최신 상태를 고른 뒤에는 성공 상태의 서로 다른 환불들을 원래 결제별로 더합니다. 환불액이 원래 결제액을 넘으면 음수 순매출로 덮어 두지 않고 오류를 냅니다. 연결되지 않은 결제, 같은 ID·스냅샷 시각의 충돌, 환불 금액이나 결제 연결 변경, 정의하지 않은 상태도 확인 대상으로 남겨야 합니다.

주문별 순매출
  = 원래 성공 결제액
    − 기준일까지 확보한 최신 상태가 성공인 별도 환불 건들의 합계

기준일인 9월 5일 자정의 주문별 결과는 다음과 같습니다.

주문성공 결제반영한 환불순매출
o401100,000원100,000원0원
o40260,000원25,000원35,000원
o40340,000원0원40,000원
합계200,000원125,000원75,000원

결제 주문 수 3건과 전액 환불 주문 수 1건도 분리해서 표시합니다. 환불 후 매출이 남은 주문 수를 보고 싶다면 별도 지표로 정의해야 합니다. 취소·환불 처리 이후에도 ‘주문 수’가 무엇을 뜻하는지 고정돼 있어야 다른 리포트와 비교할 수 있습니다.

GA4 환불 이벤트와 결제 원장을 연결할 때 확인할 것

Google의 GA4 전자상거래 문서는 refund 이벤트에 원래 거래를 연결하는 transaction_id를 지정하고, 상품별 환불 지표가 필요하면 item_id와 quantity 등의 상품 정보를 포함하도록 안내합니다. GA4 구매·환불 측정 안내

내부 환불 ID는 환불 요청을 고유화하고, GA4의 거래 ID는 원래 거래에 연결하는 역할로 구분해서 관리할 수 있습니다. 내부 원장에서 한 번만 차감됐다고 해서 모든 수집 경로의 이벤트가 자동으로 같은 방식으로 처리됐다고 가정하지는 않습니다.

금액 범위도 맞춰야 합니다. GA4 권장 이벤트 문서는 refund의 value를 상품별 가격과 수량의 합으로 정의하며 배송비와 세금을 포함하지 않도록 안내합니다. 결제 원장의 환불액에 배송비·세금이 포함됐다면 같은 금액처럼 바로 비교하기 전에 항목을 나눠 대조해야 합니다. GA4 refund 매개변수

합성 데이터로 순매출을 재현하는 방법

취소·부분 환불 예제 ZIP을 풀고 폴더 안에서 실행합니다. Python 3.9 이상과 IANA 시간대 데이터가 필요하며 추가 Python 패키지는 사용하지 않습니다.

python3 analysis.py
python3 -m unittest

실행 결과는 세 기준일의 보고서를 출력합니다. 마지막 보고서의 성공 결제는 200,000원, 성공 환불은 125,000원, 순매출은 75,000원입니다. 원시 환불 상태 행의 합계 163,000원도 비교용으로 남겨 두었습니다.

테스트는 기준일 이후 상태를 사용하지 않는지, 별도 부분 환불을 모두 반영하는지, 재수집이나 입력 순서가 금액을 바꾸지 않는지 확인합니다. 같은 시각의 상태 충돌, 원래 결제를 넘는 환불, 알 수 없는 결제와 상태도 검사합니다. 나중에 실패 상태로 수정된 환불을 계속 성공으로 차감하지 않는 경우도 포함했습니다.

이 예제는 한 상점에서 주문당 성공 결제가 한 건인 원장에 적용합니다. 분할 결제, 여러 통화, 정산 수수료, 세무·회계상 매출 인식과 은행 입금 확인은 별도 모델링이 필요합니다. 여기서는 순매출을 재현하기 위한 분석 기준을 고정했습니다.

결제와 환불 그래프를 확인하는 매도비

환불 반영 뒤 매출이 줄었다면 어떤 요청을 어느 상태에서 차감했는지 함께 확인해 보세요. 결제 사실, 별도 환불 건, 상태 이력과 기준일을 나눠 보관하면 같은 순매출을 다시 계산할 수 있습니다.

공식 문서는 2026년 9월 6일 확인했습니다.

마케팅 데이터 모델·품질·지표 정의 연재 4편입니다. 이전 글: 매출 데이터의 날짜와 시간대를 맞추는 방법

다음 글: 광고비·부가세·수수료·환율을 같은 기준으로 맞추기

🐹

자주 묻는 질문

취소된 주문은 원래 매출에서도 삭제해야 하나요?

결제 전 취소와 결제 후 환불을 구분해야 합니다. 이미 성공한 결제가 있었다면 원래 결제 사실과 환불을 각각 남겨야 총매출과 환불액을 대조할 수 있습니다. 현재 주문 상태만 보고 성공 결제를 삭제하면 환불을 이중으로 반영할 수 있습니다.

부분 환불이 여러 번 발생하면 어떻게 합산하나요?

서로 다른 환불 건은 각각 반영하되, 같은 환불 ID의 상태 스냅샷을 여러 번 더하지 않습니다. 원천 데이터가 건별 증분인지 누적 환불액인지도 구분해야 합니다.

환불 요청 금액을 모두 순매출에서 빼도 되나요?

어떤 상태를 차감 기준으로 사용할지 먼저 정해야 합니다. 이 예제는 기준일까지 확보한 환불 ID별 최신 상태가 succeeded인 금액만 차감하고, 처리 중·추가 조치 필요·실패·취소 상태는 별도로 집계합니다. 실제 입금과 회계상 인식은 별도 대사가 필요합니다.

같은 구매일의 순매출이 나중에 달라질 수 있나요?

그럴 수 있습니다. 구매일 이후 부분 환불이 추가되거나 환불 상태가 변경되면 해당 주문 코호트의 순매출이 바뀝니다. 구매일과 환불 정보 반영 기준일을 함께 기록하고, 과거 보고서는 당시 확보한 스냅샷으로 재현해야 합니다.