SIGNAL: K-뷰티 BI 대시보드

매일 아침은 엑셀 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는 있었다. 올리브영은 그것조차 없다. 아무것도 없다.

한동안 올리브영 데이터를 가져올 방법이 안 보여서 포기할까도 했다. 근데 어느 날 방법이 하나 떠올랐다. 올리브영 벤더사에 연락해 일일 판매 데이터를 정해진 시간에 메일로 보내 달라고 부탁했다.

메일이 매일 같은 시간에 들어오니 그다음은 기술 문제였다. MS 권한으로 Outlook API를 열어 메일을 자동으로 읽고 본문에서 데이터를 뽑는 파이프라인을 만들었다. API도 없고 크롤링도 어려운 플랫폼 데이터를 지금 이메일 파싱으로 가져온다. 말하면 웃긴데 잘 돌아간다.

제각각인 데이터를 한 틀로 맞추기

모으는 것보다 더 골치 아픈 건 모은 데이터를 한 포맷으로 맞추는 일이었다.

쿠팡은 주문 단위로 데이터를 주고 스마트스토어는 결제 단위로 준다. 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개 플랫폼을 하나씩 붙여 나갔다. 하나 뚫리면 뿌듯했고 두 개 연속 막히면 다시 막막했다. 그게 계속 반복됐다.

SIGNAL 시스템 아키텍처

지금 연동된 플랫폼

국내 매출: 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.

뮤텍스라는 말을 그때 처음 들은 마케터가 프로덕션에서 도는 동시성 제어 코드를 만든 거다. 이런 일이 쌓이면서 모르는 개념이 나와도 일단 붙어 보게 됐다.

This is fine

"그래서 뭘 하라고?" 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 탭이다. API 6개를 병렬로 불러 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시간을 데이터 모으는 데 썼다. 지금은 커피 한 잔 들고 대시보드를 열면 다 준비돼 있다.


결과

21연동 플랫폼
49국내 판매 채널
6개국해외 권역
16하루 자동 스케줄러

얼마나 자동으로 도나

  • 판매 수집: 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로 제일 크게 바뀐 건 "왜?"에 데이터로 답할 수 있게 된 거다.

광고 플랫폼이 하나 늘면 이 파이프라인에도 탭이 하나 붙는다. 한국 광고주가 챗GPT 광고를 직접 집행할 수 있는지는 다른 글에 따로 적어 뒀다.

근데 이건 아직 절반이다. 백엔드에서 기다리는 6개 탭을 하나씩 열고 SIGNAL을 ONE STOCK 재고 데이터와 붙이면, 매출 예측부터 생산 계획까지 이어지는 End-to-End 의사결정 파이프라인이 된다. 매출이 튀면 생산 계획이 알아서 조정되고 원자재 발주가 올라가는 구조다. 지금 그걸 만들고 있다.