매일 아침, 엑셀 6개를 여는 것으로 시작했다
화장품 제조업체 브랜드사업부 팀장으로 일하고 있다.
매일 아침 출근하면 풍경이 비슷하다. 국내 매출 담당자는 이카운트 ERP를 열고, 쿠팡 윙을 열고, 스마트스토어 판매자센터를 열어서 전날 주문과 매출을 수기로 취합한다. 광고 담당은 네이버 광고센터, Meta 비즈니스 매니저, 쿠팡 광고센터를 각각 돌며 성과 데이터를 뽑는다. 해외 담당은 Shopee 셀러센터를 4개국(싱가포르, 베트남, 말레이시아, 태국) 각각 열어서 주문 현황을 확인한다.
이 작업에 각 담당자가 30분에서 길게는 1시간까지 쓴다. 그리고 그 결과물은? 각자의 채널에 편재된 비정형 데이터. 포맷도 다르고, 기준도 다르고, 누락도 있다. 이걸 한곳에 놓고 보는 유일한 "대시보드"가 엑셀이었다. 숫자만 나열된 엑셀 시트.
국내만 해도 판매 채널이 크고 작은 곳을 합치면 49곳, 주력은 6군데 내외. 여기에 해외 6개국, 광고 4개 플랫폼까지. 이 모든 매출처의 흐름과 광고 효율을 매일 진단해야 하는 사람이 나다.
부산 연산교차로를 아는가. 다섯 개 도로가 하나의 교차점에서 만나는, 처음 가면 어디로 빠져야 할지 감을 못 잡는 곳이다. 우리 매출 구조가 딱 그렇다. 어디서 돈이 들어오는지, 어디서 새고 있는지, 어떤 광고가 어떤 채널의 매출에 영향을 주는지. 엑셀에 숫자를 아무리 옮겨 적어도 보이지 않는다.
성공했는데, 왜 성공한지 모르겠다
어느 날 쇼피 베트남에서 매출이 두 배로 튀었다. 좋은 일이다. 문제는 그 다음이었다.
경쟁사 재고가 부족해서인지, 우리 광고가 먹혀서인지, 아니면 그냥 K-뷰티 트렌드인지. 각 플랫폼에 데이터가 흩어져 있으니 원인을 특정할 수가 없었다. 경영진한테 "베트남 매출 두 배 올랐습니다" 보고하면 돌아오는 질문은 당연히 "왜? 다른 나라도 이렇게 될 수 있어?"
대답할 수가 없었다.
성공 요인을 못 잡으면 반복할 수 없다. 실패 요인을 못 잡으면 계속 반복하게 된다.
그 막막함이 한 달쯤 쌓이니까, 서서히 확신이 생겼다.
매출-광고 상관관계를 한눈에 파악할 수 있는 대시보드.
그리고 그 위에서 공헌이익률 기조로 사업부를 운영하는 것.
이걸 만들지 않으면 사업부가 감(感)으로 운영되는 구조에서 영원히 벗어나지 못한다.
이게 SIGNAL을 만든 이유다.
"API 연결하면 끝 아닌가?"
ONE STOCK을 만들면서 API 연동에 자신감이 붙어 있었다. 이카운트 ERP도 뚫었는데, 쇼핑몰 API쯤이야. 각 플랫폼에서 API를 제공하니까 그걸 하나씩 연결해서 한 화면에 뿌려주면 되는 거 아닌가?
2주면 되겠다고 생각했다.
쿠팡에서 첫 번째 벽을 만나다
매출 볼륨 1위인 쿠팡부터 시작했다. Wing API로 판매 데이터를 연동하는 건 문제없었다. 그런데 광고 성과를 가져오려는데, 공식 API가 없다.
음? 없다고?
농담이 아니다. 국내 이커머스 매출 1위 플랫폼의 광고센터에 공식 API가 없다. 광고비, ROAS, 클릭수. 이걸 자동으로 가져올 방법이 공식적으로는 존재하지 않는다. 크롤링밖에 답이 없었다. Playwright로 headless 브라우저를 띄우고, 로그인하고, GraphQL 요청을 가로채서 데이터를 뽑아야 했다.
"2주면 끝나겠다"는 자신감은 쿠팡 광고센터 한 방에 날아갔다.
올리브영이라는 더 큰 벽
쿠팡이라도 판매 API는 있었다. 올리브영은 판매 API조차 없다. 아무것도 없다.
한동안 올리브영 데이터를 어떻게 가져올지 답이 안 보였다. 포기할까 싶기도 했다. 근데 어느 날, 한 가지 방법이 떠올랐다. 올리브영 벤더사에 연락해서, 일일 판매 데이터를 정해진 시간에 메일로 보내달라고 요청한 거다.
메일이 매일 정해진 시간에 들어오니까, 그 다음은 기술의 영역이었다. MS 권한을 통해 Outlook API로 메일을 자동으로 읽고, 본문에서 데이터를 추출하는 파이프라인을 만들었다. API도 없고 크롤링도 어려운 플랫폼의 데이터를 이메일 파싱으로 가져오고 있다. 말하면 웃기지만, 잘 돌아간다.
데이터를 가져올 수만 있다면 수단을 가리지 않겠다. 이게 SIGNAL 개발 내내 지킨 유일한 원칙이었다.
비정형에서 정형으로
데이터를 모으는 것보다 더 골치 아팠던 건, 모은 데이터를 하나의 포맷으로 정리하는 일이었다.
쿠팡은 주문 단위로 데이터를 내려주고, 스마트스토어는 결제 단위다. Shopee는 리전마다 통화가 다르고, 올리브영 메일은 HTML 테이블 안에 텍스트로 박혀 있다. 이걸 "날짜 / 채널 / 매출 / 수수료 / 순이익" 같은 통일된 스키마로 정제해야 대시보드에서 비교가 가능하다.
// 플랫폼별 비정형 데이터를 통일 스키마로 정제
interface SalesRecord {
date: string;
channel: string;
grossSales: number; // 총매출
commission: number; // 수수료
netSales: number; // 순매출
currency: string; // 통화
exchangeRate?: number; // 해외일 경우 환율
}각 플랫폼마다 파서를 따로 만들었다. 쿠팡 파서, 스마트스토어 파서, Shopee 파서, 올리브영 메일 파서. 파서 하나 만드는 데 보통 하루에서 이틀. 데이터가 들어오는 포맷이 바뀌면(실제로 바뀐다) 파서도 업데이트해야 한다. 화려하지 않지만, 이 정제 작업이 없으면 대시보드는 그냥 숫자 쓰레기통이다.
새벽 6시, 크롤링이 죽었다
Playwright 크롤링은 만들 때는 뿌듯한데, 운영하면 지옥이다.
쿠팡 광고센터 세션이 수시로 만료된다. 밤새 돌린 크롤러가 새벽 3시에 죽어 있는 걸 아침에 발견하면, 그날 광고 데이터는 수동으로 뽑아야 한다. 처음에는 대기 시간을 5초로 잡았다가 10초로 늘리고, 그래도 안 되면 쿠키를 수동 갱신하고, 그마저도 안 되면 그냥 내가 직접 광고센터에 들어가서 CSV 다운로드 받던 시기도 있었다.
자동화를 만들었는데 수동으로 하고 있다. 이게 뭔가 싶었다.
결국 하루 3번 자동으로 세션을 갱신하는 구조를 만들었다. 아침 8시, 오후 2시, 저녁 8시. 실패하면 15분 후 재시도, 그래도 안 되면 Teams로 알림. 이 자동 갱신이 안정되기까지 2주. 처음에 "2주면 끝나겠다"고 했던 그 2주를 크롤링 안정화에 다 쓴 셈이다.
매출 볼륨 1위 플랫폼이 API를 제공하지 않는다는 건, 이 분야에서 일하는 사람만 아는 고충이다.
21개 플랫폼, 하나씩 뚫어 나가다
결국 원칙은 하나로 수렴했다. API가 있으면 API로. 없으면 크롤링으로. 크롤링도 안 되면 메일 파싱으로. 최종 보루는 수기 입력.
"이거 진짜 다 되긴 하나?" 스스로도 반신반의하면서, 21개 플랫폼을 하나씩 연동해 나갔다. 하나 뚫릴 때마다 뿌듯하고, 두 개 연속 막히면 다시 막막하고. 그 반복이었다.
연동 플랫폼 전체 현황
국내 매출: iCount ERP에 등록된 49개 채널을 통합 동기화한다. 여기에 쿠팡(Wing API), 스마트스토어(Naver Commerce API), 카페24(자사몰 API), 올리브영(Outlook 메일 파싱)을 별도로 연동해서 채널별 상세 데이터를 보강한다.
해외 매출: Shopee 4개국(싱가포르, 베트남, 말레이시아, 태국)을 OAuth로 연동하고, Qoo10 Japan은 API 직접 연동, Amazon은 SP-API 키 발급 대기 중이다.
광고: 네이버 검색광고(HMAC-SHA256 서명), Meta(6개 광고 계정), 쿠팡 광고센터(GraphQL 크롤링), Shopee Ads(리전별 성과). 4개 플랫폼의 노출, 클릭, 광고비, 매출, ROAS를 한곳에서 비교한다.
분석: GA4로 자사몰 트래픽과 전환을 추적하고, 뉴스 RSS 4개 소스에서 화장품/뷰티 업계 동향을 자동 수집한다.
하나씩 적어놓고 보니 꽤 많다. 그런데 이 목록이 한 번에 완성된 게 아니다. 처음에는 쿠팡+스마트스토어 2개로 시작해서, "어 이것도 넣으면 좋겠는데?"가 반복되면서 21개까지 불어났다. 욕심이 시스템을 키운 셈이다.
동남아 4개국이 동시에 죽던 날
연동 과정에서 가장 소름끼쳤던 사건은 Shopee에서 터졌다.
싱가포르, 베트남, 말레이시아, 태국. 4개 리전이 각각 독립된 OAuth 토큰을 가진다. 토큰은 주기적으로 갱신해야 하는데, 나는 당연히 4개를 한꺼번에 갱신시켰다. 효율적이니까.
그러다 어느 날 아침, 대시보드를 켰는데 동남아 4개국 데이터가 전부 비어 있었다.
4개국 전멸.
로그를 뒤져보니, 4개 리전의 토큰 갱신이 동시에 실행되면서 서로 충돌한 거였다. 한 리전의 갱신이 끝나기 전에 다른 리전이 같은 자원에 접근하면서, 도미노처럼 전부 실패. 이걸 며칠 동안 못 잡았다. 새벽 자동 갱신이 실패해도 아침까지 모르니까, 출근해서 "어 왜 데이터가 없지?" 하면서 수동으로 토큰을 재발급하고, 다음 날 또 같은 일이 반복되고.
증상을 AI 에이전트한테 설명했더니 돌아온 답이 "race condition이니 mutex를 써라."
mutex? 그게 뭔데?
처음 들어보는 개념이었다. 하지만 AI가 증상을 정확히 진단하고, 해법 코드까지 짜줬다.
// 4리전 동시 갱신 방지: 뮤텍스 패턴
const refreshLock = { locked: false, promise: null };
async function withRefreshLock<T>(fn: () => Promise<T>): Promise<T> {
if (refreshLock.locked) {
await refreshLock.promise; // 선행 작업 완료 대기
}
refreshLock.locked = true;
try {
refreshLock.promise = fn();
return await refreshLock.promise;
} finally {
refreshLock.locked = false;
}
}
// + 재시도 3회 + 지수 백오프 (1s → 2s → 4s)이 코드를 적용한 다음 날 아침, 4개국 데이터가 빈틈없이 들어와 있는 걸 확인한 순간. 그 기분은 좀 특별했다. 하루 3번(04시, 10시, 16시) 4개국 순차 갱신, 실패 시 자동 재시도, 3회 모두 실패하면 Teams 알림. 이후 토큰 갱신 실패율은 사실상 0.
뮤텍스라는 개념을 태어나서 처음 접한 마케터가, 프로덕션에서 돌아가는 동시성 제어 코드를 만든 거다. 이런 순간들이 쌓이면서, "나는 개발자가 아니니까"라는 핑계가 점점 설 자리를 잃어갔다.
Gemini AI: "그래서 뭘 하라고?"
데이터를 21개 플랫폼에서 모아놓긴 했다. 근데 숫자만 나열해놓으면 결국 예전이랑 다를 게 없다. 엑셀이 대시보드로 바뀌었을 뿐.
내가 원한 건 "그래서 뭘 해야 하는데?"에 대한 답이었다.
Gemini AI에 매일 수집된 데이터를 넣고 분석을 시켰다. 처음 결과는 실망스러웠다. "매출이 상승 추세입니다. 지속적인 모니터링이 필요합니다." 이런 뻔한 소리를 듣자고 시스템을 만든 게 아니다. 누구나 할 수 있는 말이었다.
프롬프트를 갈아엎었다. 맥킨지 피라미드 원칙을 넣고, 모든 수치에 "우리 비즈니스에 어떤 영향인가"를 필수로 답변하게 했다. "~일 수 있습니다" 같은 애매한 표현은 금지. 구체적 액션과 예상 수치를 강제했다.
const SYSTEM_PROMPT = `
당신은 화장품 제조업체 전문 경영 컨설턴트입니다.
분석 원칙:
1. 결론 먼저, 근거는 뒤에 (Pyramid Principle)
2. 모든 데이터에 "우리 비즈니스에 어떤 영향인가?" 필수 답변
3. 정량화: 숫자로 영향도 표현 (₩, %, 일수)
4. 액션 지향: 구체적·실행 가능한 권장 액션
톤: 간결하고 단정적. "~일 수 있습니다" 대신 "~입니다".
`;프롬프트를 세 번 정도 고쳤을 때, 드디어 원하던 톤이 나왔다.
"쇼피 베트남 매출 +120%. 원인: K-뷰티 트렌드 확산 + 경쟁사 A 재고 부족. 권장 액션: 베트남 전용 번들 출시. 예상 추가 매출 ₩3.2M."
이거다. 느낌이 아니라 숫자와 액션. 매일 아침 이런 브리핑을 자동으로 받는다는 건, 사업부 운영의 질 자체가 달라진다는 뜻이다. "왜 올랐어?"라는 질문에 감으로 답하던 시절이 끝난 거다.
Pro 할당량이 바닥나면 Flash도 같이 죽는다
Gemini AI를 운영하면서 한 가지 뼈아픈 교훈이 있었다.
처음에는 이런 구조를 만들었다. Gemini Pro가 할당량 초과되면 자동으로 Flash(경량 모델)로 전환하고, 1시간 후에 Pro 복구를 시도하는 폴백 패턴.
// Gemini Pro → Flash 자동 폴백 (초기 설계)
let activeModel = "gemini-1.5-pro";
let switchedAt: number | null = null;
function getModel(): string {
if (switchedAt && Date.now() - switchedAt > 3_600_000) {
activeModel = "gemini-1.5-pro";
switchedAt = null;
}
return activeModel;
}
if (error.includes("QUOTA_EXCEEDED")) {
activeModel = "gemini-2.0-flash";
switchedAt = Date.now();
}꽤 그럴듯해 보이지 않나? 나도 그렇게 생각했다. 문제는, 실전에서 터지고 나서야 알게 된 사실이 있었다. Google AI의 할당량은 Pro와 Flash가 별개가 아니다. 전체 할당에서 모델을 선택하는 구조다. Pro가 바닥나면? Flash도 같이 쓸 수 없다.
그러니까 이 멋진 폴백 구조가, 의미가 없었다.
결국 방향을 바꿨다. 폴백에 기대는 게 아니라, 애초에 할당량을 넘지 않도록 호출을 최적화하는 쪽으로. 분석 대상을 우선순위별로 나누고, 변화가 큰 데이터만 AI에게 보내고, 나머지는 룰 기반으로 처리하는 하이브리드 구조를 만들었다. 결과적으로 이쪽이 훨씬 안정적이었다.
실패한 설계에서 배우는 게 제일 빠르다. Pro/Flash 폴백은 실패했지만, 덕분에 더 나은 구조를 만들었다.
10개 탭을 한 번에 만들려고 했다
이것도 교훈이었다.
SIGNAL을 설계할 때 처음부터 10개 탭을 구상했다. Dashboard, Briefing, Weekly, Monthly, Sales, Ads, GA4, Market, News, Agent Chat. 그리고 이 10개를 한 번에 다 만들어서 런칭하려고 했다.
결론부터 말하면, 이게 엄청난 병목이 됐다.
백엔드에서 21개 플랫폼의 데이터를 수집하는 것도 버거운데, 프론트엔드에서 10개 탭의 UI를 동시에 만들고, 각 탭에 맞는 API 엔드포인트를 설계하고, 차트 컴포넌트를 붙이고. 한쪽에서 구조를 바꾸면 다른 탭에도 영향이 가고, 테스트할 범위는 계속 늘어나고. 완성도가 애매한 탭 10개보다 제대로 된 탭 4개가 낫다는 걸 너무 늦게 깨달았다.
결국 백엔드에는 10개 탭 분량의 데이터가 다 쌓여 있지만, 실제로 오픈한 탭은 4개다.
지금 돌아가는 것들
Dashboard. 종합 매출 KPI. 6개 API를 병렬로 호출해서 3~5초 내에 전체 현황을 보여준다. 채널별 수수료율을 적용한 순이익까지 한 화면에서 확인 가능하다. 예전에 각 담당자가 30분~1시간 걸려서 취합하던 그 작업이, 화면 하나를 여는 것으로 끝난다.
Sales. 국내 49채널 + 해외 6개국 매출 상세. iCount 동기화, 쿠팡 Wing, 스마트스토어, Shopee, Qoo10 데이터가 한곳에 모인다. 이제 "이 채널 매출 얼마야?"라는 질문에 5초 안에 답할 수 있다.
Ads. 네이버/Meta/쿠팡/Shopee 광고의 ROAS, CPC, CPM 비교. Meta는 TOP 20 소재 썸네일까지 볼 수 있다. 어떤 소재가 먹히는지 한눈에 보이니까, 다음 소재 기획이 감이 아니라 데이터 기반이 된다. SIGNAL에서 가장 규모가 큰 탭이다.
Agent Chat. Gemini 기반 대화형 분석. 자연어로 "지난달 베트남 매출 왜 떨어졌어?"라고 물으면 데이터 기반으로 답변한다. 경영진이 질문하기 전에 내가 먼저 답을 알고 있을 수 있는 이유.
아직 백엔드에서 대기 중인 것들
Weekly/Monthly 리포트, Briefing(AI 인사이트 카드), GA4 트래픽 분석, Market(시장 신호 감지), News 자동 수집. 이 탭들의 백엔드 로직과 데이터 수집은 이미 완성되어 있고, 프론트엔드 컴포넌트도 만들어져 있다. 다만 아직 활성화하지 않았다.
처음부터 10개를 한 번에 열겠다고 욕심부렸다가, 결국 4개만 먼저 열고 안정화한 뒤에 하나씩 추가하는 전략으로 바꿨다. 급하게 10개를 열어서 어중간한 것보다, 4개가 완벽하게 도는 게 훨씬 낫다. 이 교훈은 비싸게 배웠다.
매일 새벽 3시부터 밤 8시까지 16개 자동 스케줄러가 돌아간다. Shopee 토큰 갱신 하루 3회, 쿠팡 광고 세션 갱신 하루 3회, 올리브영 메일 확인 하루 3회. 예전에는 각 담당자가 매일 아침 30분~1시간을 데이터 수집에 쓰고 있었다. 지금은 커피 한 잔 들고 대시보드를 열면 모든 게 준비되어 있다.
결과
자동화 달성도
- 판매 수집: 90% 자동화 (Amazon SP-API 키 발급 대기 중)
- 광고 성과: 100% 자동화 (4개 플랫폼)
- AI 인사이트: 100% 자동화 (Gemini 매일 자동 생성)
- 이상 감지 알림: 100% 자동화 (Teams Webhook 즉시 알림)
- 시장 모니터링: 100% 자동화 (순위, 가격, 프로모션 변동 감지)
숫자로 보면 깔끔하지만, 여기까지 오는 과정은 전혀 깔끔하지 않았다. API가 없어서 크롤링하고, 크롤링이 깨져서 메일 파싱하고, 토큰이 동시에 죽어서 mutex를 배우고, AI 폴백이 안 먹혀서 구조를 갈아엎고, 10개 탭을 한 번에 열려다 4개로 줄이고. 21개 플랫폼 각각에 사연이 있다.
돌아보며
2주면 끝나겠다고 생각했던 프로젝트가, 21개 플랫폼과의 장기전이 됐다.
쿠팡 광고센터에 API가 없다는 걸 알았을 때의 허탈함. 올리브영 데이터를 메일 파싱으로 가져오겠다고 결심했을 때의 막막함. Shopee 4개국이 동시에 죽어서 원인을 못 찾던 며칠간의 답답함. Gemini Pro/Flash 폴백이 의미 없다는 걸 뒤늦게 깨달았을 때의 허무함. 10개 탭을 한 번에 열려다 병목에 빠졌을 때의 자괴감. 그리고 그 모든 걸 하나씩 해결하고 대시보드에 숫자가 채워질 때마다의 짜릿함.
이 프로젝트는 기술적으로 어려웠다기보다, 포기할 타이밍이 너무 많았다. "이 플랫폼은 그냥 수동으로 하자"는 타협이 매번 유혹했다. 근데 하나라도 수동이 남으면 매일 아침 엑셀을 여는 루틴에서 벗어날 수 없다. 그걸 알았기 때문에 끝까지 갔다.
부산 연산교차로 같던 매출 구조가
이제 한 화면의 조종석이 됐다.
감으로 운영하던 사업부가, 데이터로 의사결정하는 사업부가 됐다.
49개 국내 채널, 6개국 해외 매출, 4개 광고 플랫폼의 데이터가 한곳에 모이니까, 비로소 보이기 시작한 것들이 있다. 어떤 광고가 어떤 채널의 매출에 영향을 주는지, 동남아 매출이 왜 튀었는지, 어디에 리소스를 더 투입해야 하는지. "왜?"에 대한 답을 데이터로 할 수 있게 된 것, 그게 SIGNAL이 만든 가장 큰 변화다.
그리고 이건 아직 절반이다. 백엔드에서 대기 중인 6개 탭이 하나씩 열리고, SIGNAL이 ONE STOCK의 재고 데이터와 연동되면, 매출 예측에서 생산 계획까지 이어지는 End-to-End 의사결정 파이프라인이 완성된다. 매출이 튀면 자동으로 생산 계획이 조정되고, 원자재 발주가 올라가는 구조. 지금 만들고 있다.