스마트스토어 운영의 ‘진짜 일’은 상품을 고르고, 상세페이지를 개선하고, 광고를 조정하고, 고객 경험을 매만지는 데 있습니다. 그런데 막상 하루를 돌이켜 보면 정작 시간을 가장 많이 잡아먹는 건 데이터 정리입니다. 셀러센터에서 어제 주문/취소/환불 CSV를 내리고, 광고 리포트를 열어 비용을 확인한 뒤, 시트에 붙여 넣고, 수식을 복사하고, 피벗을 새로 돌리고, 급히 만든 스크린숏을 팀에 공유하는 사이—아침이 사라지죠. 더 큰 문제는 이 과정이 사람 손을 많이 탈수록 누락과 왜곡이 생긴다는 겁니다. 주문번호 중복으로 매출이 뻥튀기되거나, 쿠폰/포인트/배송비가 케이스마다 다르게 처리되어 ‘총매출=실매 출’ 착시가 나타납니다. 환불 반영이 늦어 어제는 흑자처럼 보였는데, 오늘 갑자기 적자처럼 튀어나오는 일도 흔하죠.
그래서 필요한 건 새 툴을 하나 더 추가하는 게 아니라, 흐름 자체를 자동화하는 일입니다. 즉, 수집(셀러센터/광고/클레임) → 정제(필드 표준화) → 정산 (실 매출/수수료/마진) → 리포트(대시보드) → 알림(이상 탐지)까지 이어지는 파이프라인을 만들어 두고, 사람은 그 결과만 보고 의사결정에 집중하는 구조로 바꾸는 거죠. 현실적으로는 Google Sheets와 Apps Script만 있어도 시작할 수 있습니다. 외부 파일이나 메일을 자동으로 끌어오고 싶다면 Zapier/Make 같은 노코드 도구를 곁들이고, 보이는 화면은 Looker Studio(구 Data Studio)로 구성하면 됩니다. 이 조합의 장점은 두 가지입니다. 첫째, 빠르게 세팅할 수 있고 둘째, 유지보수가 쉽다는 점입니다. 담당자가 바뀌어도 “매일 같은 규칙으로 같은 결과”가 나오는 체계를 만드는 게 목적이니까요.
핵심은 표준 스키마입니다. 파일 포맷이 조금씩 달라도, 들어오는 순간 order_date
(YYYY-MM-DD), order_id
(문자열), qty
(정수), sales_amount
(정수), coupon
, point
, shipping
, refund_flag
, refund_amount
, commission
같은 공통 필드로 맞춰 놓으면 이후 계산이 놀랄 만큼 단순해집니다. 그러면 실매 출 = 결제 − 쿠폰 − 포인트 − 환불 − 수수료 − 배송비, 순이익 = 실매 출 − 매입원가 − 물류비 − 광고비가 매일 자동으로 확정되고, 우리는 ‘어제 얼마나 벌었는지’를 아침 1분 만에 확인할 수 있죠. 이 한 줄이 안정되면, 가격 실험/광고 집행/재고 발주 타이밍을 훨씬 공격적으로 가져갈 수 있습니다.
왜 ‘매일 확정’이 중요하냐고요? 매출/마진 의사결정은 타이밍 게임이기 때문입니다. 어제 마진이 급락했는데 그 사실을 이틀 뒤에 알면 이미 손실을 되돌리기 어렵습니다. 반면 전일 대비 −30% 매출 하락, 환불률 급증, 광고비 대비 ROAS 급락 같은 경보를 오전에 받으면, 캠페인/가격/쿠폰 세팅을 그 자리에서 튜닝해 손실을 ‘그날’ 줄일 수 있습니다. 자동화의 가치는 바로 여기에 있어요. 알아야 할 정보를 먼저 알려주는 시스템—이게 만들어지면 운영은 “사후 대응”에서 “선제 대응”으로 바뀝니다.
현장에서 자주 마주치는 실패 패턴도 미리 끊어봅시다. 첫째, 다운로드-붙여 넣기 의존: 담당자 휴가/이탈 시 공백이 생깁니다. 둘째, 수식 분산: 여기저기 흩어진 계산식은 어느 날 갑자기 깨집니다. 셋째, 정의 불일치: 팀마다 ‘실매 출’ 정의가 다르면 회의에서 합의가 안 됩니다. 이를 해결하려면 역할을 분리하세요. raw 시트(원본 누적·수정 금지), clean 시트(스크립트 정제), fact 시트(지표·명시적 계산), dashboard(읽기 전용). 여기에 정의서(실매 출·마진·수수료 계산식)를 짧게 문서화하면 신입/외주와도 바로 합이 맞습니다.
비용도 고민될 수 있지만, 시작 장벽은 낮습니다. Sheets + Apps Script 조합은 무료로 시작할 수 있고, 노코드 커넥터(Zapier/Make)는 파일/메일 수집에만 최소로 써도 됩니다. 성장이 빠른 팀이라면 BigQuery로 옮겨 대용량·다채널(자사몰·마켓 통합) 분석을 생각해 볼 수 있지만, 매일 의사결정을 내리는 수준이라면 스프레드시트 기반 파이프라인만으로도 충분히 ‘탄탄한 자동화’를 경험할 수 있어요. 중요한 건 도구의 화려함이 아니라 데이터의 일관성과 알림의 정확성입니다.
그럼 어디서부터? 작게, 확실하게 시작하세요. ① 셀러센터 주문/정산 CSV를 매일 같은 폴더에 떨구고, ② Apps Script로 raw_orders/raw_settlements에 자동 누적한 뒤, ③ 표준 스키마로 실매 출/순이익을 계산하고, ④ Looker Studio로 전일 핵심 카드(매출·광고비·순이익·환불률)와 추이 차트를 만들고, ⑤ 이상 알림(전일 대비 급락/급증)을 Slack/이메일로 받으면 됩니다. 이 5단계만 끝나도 “아침 보고”는 3분이면 충분합니다. 이후엔 광고 리포트 결합, 베스트셀러 재고 연동, 주간 PDF 리포트 자동 발송을 덧붙이면 체감이 더 커집니다.
- 표준 필드:
order_date
,order_id
,qty
,sales_amount
,coupon
,point
,shipping
,refund_flag
,refund_amount
,commission
,sku
- 폴더 규칙: Google Drive
/smartstore/exports/YYYYMMDD/
(고정 경로) - 정의서: 실 매출/순이익/마진율 공식 3줄 요약 문서화(팀 공용)
- 권한: 시트 편집 권한 최소화, raw 시트는 수정 금지
- 알림: “전일 대비 -30% 하락”·“환불률 급증” 조건을 미리 정의(경보 채널 준비)
이 글에서는 곧바로 쓸 수 있는 수집 스크립트, 정산 수식, 광고비 결합, 대시보드 구성, 이상 알림 순서로 전 과정을 안내합니다. 목표는 간단합니다. 매일 아침, 한 화면과 한 통의 알림으로 “어제 벌고 쓴 것”과 “지금 바로 손댈 일”을 결정하는 것. 자동화가 시간을 벌어주면, 우리는 고객과 상품에 더 많은 시간을 쓸 수 있습니다.
1) 데이터 수집 자동화: 셀러센터 → 드라이브 → 시트, ‘무손실’ 파이프라인
매출 자동화의 출발점은 데이터 일원화입니다. 네이버 셀러센터(주문/정산/클레임)와 광고 리포트, 배송/물류 자료가 매일 같은 규칙으로 한 저장소(스프레드시트)에 쌓여야 이후 계산과 대시보드가 안정적으로 굴러갑니다. 핵심은 ① 파일 떨어뜨리는 폴더를 고정, ② 표준 스키마로 칼럼 정렬, ③ 중복 방지(주문키 기준)입니다. 아래 과정을 그대로 따라 하면 “사람이 끼지 않아도” 아침마다 원천데이터가 준비됩니다.
1-1) 폴더·시트 표준(복붙 템플릿)
/smartstore/exports/ # 외부에서 떨어지는 원본 CSV
└── YYYYMMDD/ # 날짜별 폴더(매일 신규)
├── orders_YYYYMMDD.csv
├── settlements_YYYYMMDD.csv
└── claims_YYYYMMDD.csv
/smartstore/logs/ # 처리 로그/백업
raw_orders
·raw_settlements
·raw_claims
(원본 누적, 수정 금지)clean_orders
(헤더 표준화/타입 변환)fact_orders
(분석용 칼럼 확정: 실 매출/수수료 등은 다음 섹션)_logs
(수집 실행 시간, 파일명, 건수, 중복수)
1-2) 인코딩/헤더 표준화(현실 체크)
- 인코딩: 가능하면 셀러센터 UTF-8로 내보내세요. EUC-KR/MS949로 떨어질 때는 Apps Script에서
getDataAsString('MS949')
로 읽어 변환합니다. - 헤더: 날짜/금액/주문키 필드명은 고정(예:
order_date
,order_id
,sales_amount
). 셀러센터 원본 칼럼명을 매핑 테이블로 통일합니다. - 타입: 날짜 →
YYYY-MM-DD
, 금액 → 정수(원), 수량 → 정수, Y/N 플래그 분리(refund_flag
).
1-3) Apps Script: 최신 CSV 자동 누적(중복 제거/헤더 매핑)
아래 스크립트는 Drive의 날짜 폴더에서 최신 CSV를 읽어 raw_orders
에 누적하고, 주문 ID 중복을 걸러냅니다.
/** 시트/폴더 설정 */
const FOLDER_ID = 'YOUR_EXPORTS_FOLDER_ID';
const SHEET_RAW = 'raw_orders';
const SHEET_LOG = '_logs';
/** 원본 헤더 → 표준 헤더 매핑(예시) */
const HEADER_MAP = {
'주문일자': 'order_date',
'주문번호': 'order_id',
'상품명': 'product_name',
'옵션명': 'option',
'수량': 'qty',
'결제금액': 'sales_amount',
'쿠폰할인': 'coupon',
'포인트사용': 'point',
'배송비': 'shipping'
};
function importLatestOrders() {
const shRaw = SpreadsheetApp.getActive().getSheetByName(SHEET_RAW);
const shLog = SpreadsheetApp.getActive().getSheetByName(SHEET_LOG);
const latest = findLatestCsv('orders'); // 파일명에 'orders' 포함
if (!latest) return log(shLog, 'NO_FILE', 'orders', 0, 0, '파일 없음');
// 인코딩 변환(UTF-8 권장, 필요 시 MS949로 시도)
const txt = latest.getBlob().getDataAsString('UTF-8') || latest.getBlob().getDataAsString('MS949');
const rows = Utilities.parseCsv(txt);
if (!rows.length) return log(shLog, 'EMPTY', latest.getName(), 0, 0, '빈 파일');
// 헤더 매핑
const srcHeader = rows[0];
const idx = Object.fromEntries(srcHeader.map((h,i)=>[h,i]));
const stdHeader = Object.values(HEADER_MAP);
const existsHeader = shRaw.getLastRow() ? shRaw.getRange(1,1,1,shRaw.getLastColumn()).getValues()[0] : [];
if (!existsHeader.length) shRaw.getRange(1,1,1,stdHeader.length).setValues([stdHeader]);
// 기존 주문ID 인덱스(중복 방지)
const orderIdCol = stdHeader.indexOf('order_id') + 1;
const currentIds = new Set(
shRaw.getLastRow() > 1 ? shRaw.getRange(2,orderIdCol,shRaw.getLastRow()-1,1).getValues().flat().filter(Boolean) : []
);
// 본문 변환
const body = [];
for (let r=1; r<rows.length; r++){
const row = rows[r];
const record = stdHeader.map(key => {
const srcKey = Object.keys(HEADER_MAP).find(k => HEADER_MAP[k] === key);
const val = srcKey in idx ? row[idx[srcKey]] : '';
switch(key){
case 'order_date': return normalizeDate(val); // YYYY-MM-DD
case 'qty': return toInt(val);
case 'sales_amount':
case 'coupon':
case 'point':
case 'shipping': return toInt(val);
default: return (val||'').toString().trim();
}
});
const orderId = record[stdHeader.indexOf('order_id')];
if (!orderId || currentIds.has(orderId)) continue; // 중복/빈키 스킵
currentIds.add(orderId);
body.push(record);
}
if (body.length){
shRaw.getRange(shRaw.getLastRow()+1,1,body.length,stdHeader.length).setValues(body);
}
log(shLog, 'IMPORTED', latest.getName(), rows.length-1, body.length, `중복제거:${(rows.length-1)-body.length}`);
}
function findLatestCsv(keyword){
const folder = DriveApp.getFolderById(FOLDER_ID);
const files = folder.getFiles();
let latest=null, t=0;
while(files.hasNext()){
const f = files.next();
if (f.getName().toLowerCase().includes(keyword) && f.getLastUpdated().getTime() > t){
latest = f; t = f.getLastUpdated().getTime();
}
}
return latest;
}
function normalizeDate(s){
// 2025-10-21, 2025/10/21, 20251021 모두 허용 → YYYY-MM-DD
if(!s) return '';
const v = s.toString().replaceAll('.','-').replaceAll('/','-');
if (/^\d{8}$/.test(v)) return v.slice(0,4)+'-'+v.slice(4,6)+'-'+v.slice(6,8);
if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(v)) {
const [y,m,d] = v.split('-'); return [y, ('0'+m).slice(-2), ('0'+d).slice(-2)].join('-');
}
return v;
}
function toInt(s){ return parseInt((s||'').toString().replace(/[^\d-]/g,''))||0; }
function log(sh, type, file, srcCnt, insCnt, note){
const now = new Date();
sh.appendRow([now, type, file, srcCnt, insCnt, note]);
}
실행: 트리거 > 시간 기반으로 매일 08:30 설정. 실패 시 _logs
시트에서 원인을 바로 확인합니다.
1-4) 클레임/정산/광고 리포트까지 확장(모듈화)
- 정산(
raw_settlements
): 수수료/정산일자/정산금액 필드 포함. 헤더 매핑만 바꾸고 같은 함수 재사용. - 클레임(
raw_claims
):order_id
,refund_amount
,claim_type
,processed_at
. 이후 주문테이블에 증분 업데이트(두 번째 섹션에서 반영). - 광고 리포트(
raw_ads
): 날짜/소재/캠페인/비용/클릭/전환 필드. 날짜 기준 합산부터 붙이고, SKU 연결은 캠페인 네이밍 규칙으로 맵핑.
1-5) 이메일/파일 자동 수집(Zapier/Make 대안)
수동 업로드 없이 메일 → 드라이브 → 스크립트 순으로 자동화하려면:
- Gmail 라벨
smartstore_exports
생성, 셀러센터 내보내기 수신 메일에 자동 라벨 부여 - Zapier(또는 Make): New labeled email → 첨부 저장(Drive
/smartstore/exports/YYYYMMDD/
) - Apps Script 시간 트리거가 새 파일을 감지해
raw_*
누적
Tip. 파일명 규칙(예: orders_YYYYMMDD.csv
)을 고정하면 findLatestCsv()
가 견고해집니다.
1-6) 데이터 품질 체크(5가지 가드레일)
- 필수 필드 유효성:
order_id
빈값 제거,order_date
파싱 실패건 로그 - 음수/이상치: 금액/수량 음수는 별도 태그로 마킹
- 중복: 동일
order_id
2회 유입 시 스킵 + 로그 - 헤더 변경 감지: 원본 헤더가 바뀌면 로그에 HEADER_CHANGED 기록(알림과 연계)
- 감사 로그: 파일명/처리시간/원본건수/인서트건수 저장 → 주간 점검
1-7) 표준 스키마(주문) — 이대로 맞추면 계산이 쉬워집니다
필드 | 타입/예시 | 설명 |
---|---|---|
order_date | YYYY-MM-DD | 일자 집계 기준 |
order_id | 문자열 | 중복 방지 고유키 |
product_name / option | 텍스트 | 상품/옵션 분석 |
sku | 텍스트 | 원가/재고 매핑 키 |
qty | 정수 | 판매 수량 |
sales_amount | 정수(원) | 결제 금액 |
coupon / point / shipping | 정수(원) | 할인/포인트/배송비 |
refund_flag / refund_amount | Y/N, 정수 | 환불 반영 |
channel | 스마트스토어 | 멀티채널 확장 대비 |
1-8) 알림·예외 처리(놓치면 아픈 부분)
- 실패 알림:
_logs
에 NO_FILE/EMPTY/HEADER_CHANGED 발생 시 Slack Webhook/이메일로 경보 - 백업: 원본 CSV를
/logs/YYYYMM/
로 월별 이동(감사/되돌리기 대비) - 권한:
raw_*
는 편집 금지,clean/fact
만 편집 가능 권고
요약하면, 고정 폴더 → 헤더 매핑 → 중복 제거 → 로그의 네 축만 세워도 ‘무손실 수집’이 완성됩니다. 이 상태가 되면 두 번째 섹션의 정산·수수료·광고비 자동 계산으로 바로 넘어가 “매일 아침 실매 출 확정”을 만들 수 있습니다.
2) 정산·수수료·광고비 자동 계산: “실매 출·마진”이 매일 확정되게
매출 관리는 결국 얼마를 벌고(실매 출), 얼마가 남는가(순이익·마진)로 귀결됩니다. 문제는 파일마다 쿠폰/포인트/배송비/수수료/환불의 표기가 제각각이라 사람 손으로 합치면 실수가 잦다는 점이죠. 이 섹션에서는 표준 공식을 시트에 고정하고, 광고비 결합과 환불 반영까지 자동화하여 “어제 기준 실매 출/순이익이 매일 아침 확정되는” 상태를 만드는 방법을 단계별로 정리합니다. 핵심은 계산을 한 시트(또는 한 열)에 모으고, 정의서를 짧게 문서화해 팀 전체가 같은 숫자를 보게 하는 것입니다.
- (Net Sales) = 결제금액(
sales_amount
) − 쿠폰 − 포인트 − 환불금액 − 수수료 − 배송비 - 순이익(Profit) = 실 매출 − 매입원가 − 물류비(포장/택배) − 광고비
- 마진율(%) = 순이익 ÷ 실매 출
2-1) 시트 계산식: clean_orders → fact_orders로 확정
먼저 clean_orders
에 표준 필드(날짜/주문 ID/수량/결제금액/쿠폰/포인트/배송비/환불/수수료/sku 등)가 정리돼 있다고 가정하고, 분석용 계산을 넣은 fact_orders
를 만듭니다. ARRAYFORMULA로 한 번에 계산해 수식 관리 포인트를 최소화하세요.
=ARRAYFORMULA(
LET(
d, clean_orders!A:Z,
header, INDEX(d,1,),
rownum, ROW(d),
order_date, INDEX(d,,MATCH("order_date",header,0)),
order_id, INDEX(d,,MATCH("order_id",header,0)),
qty, N(INDEX(d,,MATCH("qty",header,0))),
amt, N(INDEX(d,,MATCH("sales_amount",header,0))),
coupon, N(INDEX(d,,MATCH("coupon",header,0))),
point, N(INDEX(d,,MATCH("point",header,0))),
shipping, N(INDEX(d,,MATCH("shipping",header,0))),
refund_amt, N(INDEX(d,,MATCH("refund_amount",header,0))),
fee, N(INDEX(d,,MATCH("commission",header,0))),
sku, INDEX(d,,MATCH("sku",header,0)),
net, amt - coupon - point - refund_amt - fee - shipping,
HSTACK(order_date, order_id, sku, qty, amt, coupon, point, shipping, fee, refund_amt, net)
)
)
Tip. LET
과 INDEX/MATCH
를 쓰면 헤더 순서가 바뀌어도 견고합니다. N()
은 숫자 캐스팅.
2-2) 수수료 계산 자동화(상품군·프로모션별 가중)
수수료가 CSV에 없거나 상품군별로 달라지는 경우가 많습니다. fee_rules
테이블을 만들어 SKU/카테고리별 수수료율을 매핑하세요.
키 | 설명 | 예시 |
---|---|---|
sku | 상품 식별자 | ABC-001 |
category | 대카/중카 | 리빙/수납 |
fee_rate | 수수료율 | 0.12 |
/* clean_orders에 commission이 비어있다면 아래로 계산 */
=ARRAYFORMULA(
IF(
clean_orders!1:1="commission",
"commission",
N(VLOOKUP(clean_orders!F2:F, fee_rules!A:C, 3, FALSE)) * N(clean_orders!E2:E) /* F=sku, E=sales_amount */
)
)
프로모션 수수료(기간 한정)가 있다면 fee_rules
에 start_date/end_date
를 추가하고 조건부 VLOOKUP(또는 FILTER
)로 기간을 함께 매칭합니다.
2-3) 원가·물류비 자동 매핑(테이블화)
원가는 SKU×단가, 물류비는 주문/박스당 고정 혹은 무게/구간별 변동으로 설정합니다. 각각 cost_table
, logistics_table
을 만들고 VLOOKUP으로 매핑하세요.
/* 매입원가(원) */
=ARRAYFORMULA( N(VLOOKUP(fact_orders!C2:C, cost_table!A:B, 2, FALSE)) * N(fact_orders!D2:D) )
/* 물류비(간단: 주문당 고정) */
=ARRAYFORMULA( IF(fact_orders!A2:A<>"", 2500, ) )
무게/구간별 물류는 IFs
또는 VLOOKUP(무게→요금표)
로 매핑하고, 택배 업체 인상 시 logistics_table
만 업데이트하면 전체 계산이 갱신됩니다.
2-4) 광고비 결합(날짜→상품군→캠페인 매칭)
광고비는 먼저 일자 단위 합계를 붙인 뒤, 필요시 상품군/캠페인까지 정교화합니다. 처음엔 일자 기준부터 안정화하는 것이 좋습니다.
/* raw_ads: date, campaign, ad_cost, clicks, conv 등 */
=LET(
d, fact_orders!A:L, /* A:order_date, ... L:net */
a, raw_ads!A:Z,
day, INDEX(d,,1),
costMap, QUERY(a, "select A, sum(C) group by A label sum(C)''", 0), /* A=date, C=ad_cost */
adCost, IFERROR(VLOOKUP(day, costMap, 2, FALSE), 0),
HSTACK(d, adCost)
)
캠페인 네이밍에 SKU/상품군 키를 포함하면(예: CAT-리빙_ABC-001_탑
), REGEXEXTRACT
로 키를 빼서 SKU 레벨까지 비용 배분이 가능합니다.
2-5) 최종 지표 칼럼(순이익·마진율) 산출
여기까지 붙이면 fact_orders
에서 순이익/마진율을 한 번에 산출할 수 있습니다.
/* net: 실매출, cogs: 원가, logistics: 물류비, ad: 광고비 */
=ARRAYFORMULA(
LET(
net, fact_orders!L2:L,
cogs, fact_orders!M2:M,
logistics, fact_orders!N2:N,
ad, fact_orders!O2:O,
profit, net - cogs - logistics - ad,
margin, IFERROR(profit / net, 0),
HSTACK(profit, margin)
)
)
2-6) 환불/취소의 ‘증분 반영’(Apps Script로 안정화)
환불 CSV(raw_claims
)를 누적했다면, 주문키 기준으로 refund_flag
/refund_amount
를 갱신해야 합니다. 아래 스니펫은 새로 들어온 환불만 찾아 업데이트합니다.
function applyRefundsIncremental() {
const ss = SpreadsheetApp.getActive();
const orders = ss.getSheetByName('clean_orders');
const claims = ss.getSheetByName('raw_claims').getDataRange().getValues();
const oh = headerIndex(orders.getRange(1,1,1,orders.getLastColumn()).getValues()[0]);
const map = {};
claims.slice(1).forEach(r => { map[r[0]] = r[3]; }); // 0:order_id, 3:refund_amount 예시
const rng = orders.getRange(2,1,orders.getLastRow()-1,orders.getLastColumn());
const vals = rng.getValues();
let touched = 0;
vals.forEach(row => {
const id = row[oh.order_id];
if (map[id] != null && row[oh.refund_amount] !== map[id]) {
row[oh.refund_flag] = 'Y';
row[oh.refund_amount] = map[id];
touched++;
}
});
rng.setValues(vals);
Logger.log(`refund updated: ${touched}`);
}
function headerIndex(h){ const m={}; h.forEach((v,i)=>m[v]=i); return m; }
시간 기반 트리거(매일 08:40)를 걸면 수집→환불반영→계산→대시보드가 자동으로 이어집니다.
2-7) 일/주/월 집계(QUERY/피벗) — 대시보드 입력값 만들기
Looker Studio로 넘기기 전, 시트에서 kpi_daily 같은 요약 테이블을 생성해 두면 시각화가 가벼워집니다.
=QUERY(
{fact_orders!A:A, fact_orders!L:L, fact_orders!P:P}, /* A=order_date, L=net, P=profit */
"select Col1, sum(Col2), sum(Col3) group by Col1 label sum(Col2)'net', sum(Col3)'profit'",
1
)
주간/월간은 EOMONTH
또는 TEXT(A, "YYYY-MM")
로 그룹 키를 만든 뒤 동일한 QUERY
패턴을 적용하세요.
2-8) 자주 생기는 오류 & 빠른 처방(현장 메모)
증상 | 원인 | 해결 |
---|---|---|
실매출이 총매출과 동일 | 쿠폰/포인트/수수료/배송비 컬럼 누락 | 표준 스키마에 필수 컬럼 추가, 기본값 0으로 캐스팅 |
마진율이 비정상 | 원가/광고비 매핑 오류 | VLOOKUP 키 정합(소문자·공백 제거), 룰테이블 최신화 |
광고비 미결합 | 날짜 포맷/타임존 불일치 | 양쪽 모두 YYYY-MM-DD, Asia/Seoul로 통일 |
환불 누락 | 증분 업데이트 미작동 | Apps Script 트리거 확인, _logs 에 touched 카운트 기록 |
2-9) 운영 SOP(복붙)
[매일 08:30] raw 수집(importLatestCSV) → [08:40] 환불 반영(applyRefundsIncremental)
→ [08:42] fact 계산(ARRAYFORMULA/LET) → [08:45] kpi_daily 갱신 → [08:50] 이상 알림
[주간] 수수료/원가 룰테이블 점검, 광고비 맵핑 정상 여부 확인
[월간] 정의서 변경사항 리뷰(실매출/마진 공식), 룰테이블 버전업 기록
요약하면, 표준 공식 + 룰테이블 매핑 + 증분 환불 반영 + 광고비 결합의 네 축만 잡아도 스마트스토어의 실매 출·순이익은 매일 자동으로 확정됩니다. 이렇게 숫자가 안정되면, 다음 섹션의 대시보드·이상 알림에서 “보는 순간 이해 + 문제는 먼저 알림”을 구현하기가 훨씬 쉬워집니다.
3) 대시보드·알림 자동화: ‘보는 순간 이해’ + ‘문제는 먼저 알림’
숫자를 잘 관리하는 팀의 공통점은 두 가지입니다. 하나의 화면으로 모두가 같은 진실을 보고, 문제는 먼저 알림으로 받습니다. 스마트스토어 매출 관리에서도 마찬가지예요. Looker Studio(구 Data Studio)로 핵심 지표를 한눈에 보여주고, Apps Script/웹훅으로 특이사항을 먼저 알려주면, 아침 회의는 “무슨 일이 있었나?”가 아니라 “무엇을 바꿀까”로 시작합니다.
3-1) Looker Studio 설계 — 카드·추이·분해 ‘3단 구성’
kpi_daily
(일자·실매 출·순이익·환불률·광고비 등 요약)fact_orders
(SKU/옵션/채널 차원까지 상세)- 필요 시
raw_ads
(캠페인/비용/전환)
스프레드시트 커넥터 연결 시, 첫 행=헤더 · 시트 범위를 명확히 지정
- 지표: 실매 출(
net
), 순이익(profit
), 마진율, 광고비, 환불률 - 차원: 일자, 상품군, SKU, 옵션, 캠페인, 채널
- ① KPI 카드(상단): 전일 실매 출 · 전일 순이익 · 마진율 · 환불률 · 광고비. 전주/7일 평균 대비 증감(▲/▼) 배지 표시
- ② 추이(중단): 일자별 실매 출/순이익/광고비 라인 차트(득실 골든라인). 이벤트(쿠폰/광고 변경일) 점형 주석 추가
- ③ 분해(하단): 상품 Top10 바차트, SKU 피벗(수량·매출·이익), 환불 사유 도넛, 캠페인별 ROAS 히트맵
- 색상 규칙: 실매 출=파랑(#2563EB), 순이익=초록(#16A34A), 환불/감소=빨강(#DC2626)
- 기간 컨트롤: 상단에 최근 7/30/90일 프리셋 버튼
- 필터: 상품군·캠페인·채널 필터를 보고서 수준으로 고정(페이지 공통)
3-2) 이상 탐지(Anomaly) 로직 — ‘어제-평균-임계치’ 3단계
알림의 핵심은 과도한 소음 없이 정말 중요한 변화만 잡아내는 것입니다. 아래 기준을 추천합니다.
- 전일 대비: 어제 실매 출이 7일 평균 대비 -30% 이하
- 환불 급증: 환불률이 7일 평균 + 2 표준편차 이상
- ROAS 급락: 광고비가 증가했는데 실매 출이 전일/평균 대비 동반 하락
/** kpi_daily: date, net, profit, refund_rate, ad_cost, net_avg7, net_std7 */
function alertAnomaly() {
const sh = SpreadsheetApp.getActive().getSheetByName('kpi_daily');
const last = sh.getLastRow();
const h = sh.getRange(1,1,1,sh.getLastColumn()).getValues()[0];
const ix = Object.fromEntries(h.map((v,i)=>[v,i+1]));
const val = (col) => sh.getRange(last, ix[col]).getValue();
const date = val('date'), net = val('net'), avg7 = val('net_avg7'), std7 = val('net_std7');
const refund = val('refund_rate'), ad = val('ad_cost');
let alerts = [];
if (avg7 > 0 && net < avg7*0.7) alerts.push(`실매출 하락: ${fmt(net)} (7일평균 ${fmt(avg7)} 대비 ▼${pct(net/avg7)})`);
if (std7 > 0 && refund > val('refund_avg7') + 2*val('refund_std7')) alerts.push(`환불률 급증: ${(refund*100).toFixed(1)}%`);
if (ad > val('ad_avg7')*1.2 && net < avg7*0.9) alerts.push(`광고효율 의심: 광고비 ↑, 매출 ↓`);
if (!alerts.length) return;
const payload = { text: `📢 [스마트스토어 경보] ${formatDate(date)}\n- ` + alerts.join('\n- ') };
UrlFetchApp.fetch('YOUR_SLACK_WEBHOOK', {
method:'post', contentType:'application/json', payload: JSON.stringify(payload)
});
}
function fmt(n){ return Math.round(n).toLocaleString()+'원'; }
function pct(r){ return ((1-r)*100).toFixed(0)+'%'; }
function formatDate(d){ return Utilities.formatDate(new Date(d), 'Asia/Seoul', 'yyyy-MM-dd'); }
스케줄: 매일 08:50 실행. 경보는 #ops
또는 #store-alert
전용 채널로 분리해 소음 관리.
3-3) Slack/이메일 카드형 요약 — ‘읽자마자 감’이 오게
[일일 요약 — {{날짜}}]
실매출: {{net | 통화}} (전일 대비 {{Δnet}} / 7일평균 대비 {{Δ7}})
순이익: {{profit | 통화}} · 마진율: {{margin | %}}
광고비: {{ad | 통화}} · ROAS: {{roas}}
환불률: {{refund_rate | %}}
Top3 SKU ↓
1) {{sku1}} — {{net1 | 통화}} (환불률 {{refund1 | %}})
2) {{sku2}} — {{net2 | 통화}}
3) {{sku3}} — {{net3 | 통화}}
메모: {{note}} (이벤트/쿠폰 변경/광고 조정 사항)
Slack Block Kit로 포맷하면 가독성이 좋아집니다. 이메일은 고정 템플릿 + 굵은 텍스트/색상 배지 활용.
3-4) 보고서 운영 — 주간/월간 ‘리뷰 루틴’까지 자동화
- 주간(월 09:00): 전주 합계·증감, 베스트/워스트 SKU, 환불 Top3, 액션 아이템(3개)
- 월간(1일 09:00): 월간 매출/이익/마진/ROAS, 프로모션 영향 분석, 다음 달 가설
- 자동 발송: Looker Studio 링크와 함께 Apps Script로 PDF 스냅숏 첨부
function sendWeeklyReport(){
const url = 'LOOKER_STUDIO_REPORT_LINK'; // 기간 파라미터 포함 가능
const html = `
주간 리포트
링크: ${url}
- 실매 출/순이익/마진/ROAS 요약
- 베스트·워스트 SKU 및 환불 이슈
- 이번 주 액션 3가지
`;
GmailApp.sendEmail('team@example.com', '[스마트스토어] 주간 리포트', '', {htmlBody: html});
}
3-5) 대시보드 성능·정합성 체크리스트
영역 | 점검 항목 | 해결 가이드 |
---|---|---|
성능 | 로드 지연/타임아웃 | 시트에서 kpi_daily 등 요약 테이블 생성 후 연결 |
정합성 | 총계/세부 합계 불일치 | 기간/필터 상태 확인, 환불/수수료 반영 순서 재검토 |
권한 | 외부 공유 오류 | 스프레드시트/보고서 보기 권한 일치, 링크 만료 정책 |
3-6) ‘문제 먼저 알림’ SOP(복붙)
[매일]
08:30 수집 → 08:40 환불 반영 → 08:42 계산 → 08:45 kpi 갱신 → 08:50 이상 알림 → 09:00 대시보드 점검(3분)
[주간]
월 09:00 주간 리포트 발송 · 경보 상위 3건 원인/조치 기록(노션/시트)
[월간]
마지막 영업일: 프로모션/광고 가설 회고 · 다음 달 실험 계획 3개 합의
정리하면, 대시보드는 보는 순간 이해가 되어야 하고, 알림은 문제를 먼저 알려야 합니다. 이 두 가지가 갖춰지면 “자료 만들기”에 쓰던 시간이 “의사결정과 실험”으로 이동합니다. 오늘은 KPI 카드/추이/분해의 3단 레이아웃을 만들고, 실매 출 -30%·환불률 +2σ 경보만 먼저 붙이세요. 내일부터 아침이 한결 가벼워집니다.
결론: 툴보다 ‘흐름’이 먼저 — 매일 아침, 같은 자리에서 같은 품질의 숫자를 본다
스마트스토어 매출 관리는 화려한 대시보드나 비싼 설루션의 문제가 아닙니다. 핵심은 수집 → 정제 → 정산 → 리포트 → 알림이 끊김 없이 이어지는 한 줄의 파이프라인을 만드는 일입니다. 원본 CSV가 매일 같은 폴더에 들어오고, Apps Script가 표준 스키마로 정리하며, 시트 한 곳에서 실매 출·순이익이 자동으로 확정되고, Looker Studio 한 화면에서 모두가 같은 지표를 보며, 이상 징후는 Slack/이메일로 먼저 울리는 상태—이 흐름이 만들어지는 순간, “자료 만들기”는 0분이 되고 의사결정(가격·광고·재고)은 더 빨라집니다. 특히 전일 대비 급락/급증 경보가 아침에 잡히면, 같은 날 안에 손을 댈 수 있어 손실을 크게 줄일 수 있습니다. 결국 자동화는 시간을 벌어 주고, 그 시간은 상품 개선과 고객 경험에 재투자됩니다.
- 수집: Drive
/smartstore/exports/YYYYMMDD/
고정 ·raw_*
시트 자동 누적(중복 방지) - 정제/정산: 표준 스키마(날짜/금액/주문키) 확정 · 실매 출/마진 공식 문서화 · 환불 증분 반영
- 광고 결합: 일자 합계부터 붙이고, 캠페인/상품군은 네이밍 규칙으로 단계적 확대
- 대시보드: KPI 카드(전일 실매 출·순이익·마진·환불률·광고비) + 7/30일 추이 + SKU 분해
- 알림: 전일 실매 출
< 7일평균 × 0.7
, 환불률> 평균 + 2σ
경보 → Slack - 운영:
_logs
로 실패/헤더변경 감지 · 권한 최소화(raw 수정 금지) · 주 1회 스냅샷 백업
시작은 작게, 그러나 반드시 매일 돌아가게 만드세요. 오늘은 수집 스크립트와 표준 스키마, 실매 출 계산까지만. 내일은 대시보드 카드와 알림 두 가지를 얹고, 주말에 광고비 결합과 룰테이블(수수료·원가)을 정비해도 충분합니다. 일주일이면 “아침 3분 보고 체계”가 자리를 잡습니다. 흐름이 자동이면, 팀은 더 자주 실험하고 더 빨리 학습합니다. 그게 매출을 지속적으로 키우는 가장 현실적인 방법입니다.