ONE STOCK — 중소 제조업 ERP 위에 AI를 얹으면 생기는 일

"재고 몇 개 남았어요?"

뷰티 제조업체에서 하루에도 수십 번 오가는 질문이다.

영업팀이 물어보고, 생산팀이 물어보고, 대표가 물어본다. 그때마다 누군가는 이카운트 ERP에 접속해서 품목코드를 검색하고, 창고별 수량을 확인하고, Teams로 답변을 보낸다. 한 건당 2-3분. 하루에 열몇 번씩 반복되면 은근히 시간을 잡아먹는다.

나는 제조업 기반의 뷰티 브랜드 회사의 브랜드사업부 팀장으로 근무하고 있다. 그런데 옆자리 영업팀이 "이거 재고 있어?" 확인하느라 매번 흐름이 끊기는 걸 계속 보고 있었다. 시간보다 더 큰 문제는, 질문하는 사람도 답변하는 사람도 자기 할 일을 중단해야 한다는 것이었다.

아무도 이걸 문제라고 말하지 않았다. 그냥 원래 그런 거니까.

데이터는 있었다. 연결이 없었을 뿐

더 들여다보면, 문제는 재고 조회 하나가 아니었다.

  • 재고 → 이카운트 ERP
  • 생산 요청 → Teams 채팅
  • BOM(자재명세서) → 엑셀 파일
  • 생산 계획 → 브랜드팀·B2B팀·생산팀이 각자 관리하는 시트

이 데이터들이 연결되지 않으니, "이 제품 100개 만들려면 원자재 충분해?" 같은 질문에 답하려면 ERP 켜고, 엑셀 열고, 계산기 두드려야 했다. 한 질문을 해결하기 위해 세 개의 도구를 동시에 열어야 하는 구조였다.

중소 제조업 163,000개 중 76%가 ERP를 쓰고 있지만, AI를 활용하는 곳은 1%도 안 된다. 데이터는 있는데 연결이 안 되는 거다. 연결만 되면 뭔가 달라질 것 같았다.


이카운트 API 지옥에서 MCP로

처음 접근은 단순했다. 이카운트 ERP에 OAPI가 있으니까, API를 하나씩 호출해서 필요한 데이터를 뽑으면 되겠다 싶었다.

막상 해보니 이게 보통 일이 아니었다.

재고 조회 API 따로, 판매 조회 API 따로, BOM API 따로. 각각 인증 방식이 다르고, 응답 포맷이 다르고, 세션 관리도 개별적으로 해야 한다. API 10개를 연결하려면 인증 로직 10개, 에러 처리 10개, 캐시 10개. 이건 비개발자가 혼자 유지보수하기 불가능한 구조다.

나는 마케터다. IT팀도 없고, 외주 예산도 없다.
그런데 이 병목을 해결할 사람이 아무도 없었다.
그래서 직접 만들기로 했다.

AI 에이전트를 활용해서 개발하는 방식을 택했다. 그러다 MCP(Model Context Protocol)를 알게 됐다.

MCP — C타입 젠더 하나로 8핀도, USB-A도 다 꽂히는 것처럼

비개발자한테 MCP를 설명하는 가장 쉬운 비유는 이거다.

맥북에는 C타입 포트밖에 없는데, 연결해야 하는 기기들은 제각각이다. 아이폰은 8핀, 외장하드는 USB-A, 모니터는 HDMI. 이걸 하나씩 젠더 사서 따로 꽂고 있으면 끝이 없다. 그래서 쓰는 게 멀티허브 젠더다. C타입 하나 꽂으면 그 뒤로 뭐든 다 연결된다. 내 맥북이 상대 기기의 규격을 알 필요가 없다. 허브가 다 변환해준다.

MCP가 정확히 그 역할이다. ERP API는 인증 방식이 이렇고, 응답 포맷이 저렇고, 세션은 30분마다 갱신해야 하고. 대시보드가 이 규격들을 일일이 알 필요가 없다. MCP 서버가 중간에서 다 변환해준다. 재고 조회, BOM 검색, 판매 현황 같은 기능을 "도구(Tool)"로 한 번만 정의해두면, AI든 대시보드든 Teams 봇이든 같은 방식으로 호출하면 끝이다.

대시보드 → "재고 조회해줘" → MCP 서버 → 이카운트 ERP API 호출 → 결과 반환
                                    ↓
                              세션 관리, 에러 처리, 캐싱 — 전부 여기서 해결

개별 API 10개를 따로 관리하는 게 아니라, MCP 서버 하나가 10개의 핸들러를 품고 있는 형태다. 새 기능이 필요하면 MCP 서버에 핸들러 하나 추가하면 끝이다. 대시보드 코드는 건드릴 필요가 없다.

MCP Architecture

이 그림이 정확히 그 구조다. 맥북 자리에 Claude나 GPT 같은 AI(MCP 클라이언트)가 있고, 꽂혀 있는 케이블들이 각각 MCP 서버다. 왼쪽은 Slack·Gmail·캘린더 같은 외부 서비스, 오른쪽은 로컬 데이터. AI는 허브만 바라보면 되고, 각 서비스의 규격은 몰라도 된다. ONE STOCK에서는 이 케이블 중 하나가 이카운트 ERP다.

ONE STOCK 시스템 아키텍처

그림으로 보면 이렇다. React 대시보드가 Express Proxy를 통해 MCP 서버를 호출한다. MCP 서버 안에는 재고조회·BOM검색·수요예측 등 도구들이 들어 있고, 이 도구들이 실제로 이카운트 ERP API를 호출하거나 SQLite 캐시에서 데이터를 꺼낸다. Teams 봇은 Power Automate를 통해 같은 Proxy에 연결된다.

데이터를 가져오는 방법이 하나로 통일되니, Teams 봇이든 대시보드든 같은 소스에서 같은 방식으로 데이터를 받는다. 비개발자인 내가 이 규모의 시스템을 혼자 만들고 유지할 수 있었던 건 이 구조 덕분이었다.

아키텍처는 그럴듯했다. 남은 건 실제로 만드는 것이었다.


14일의 기록

사고 1 — 백엔드한테 말 안 하고 외주 줬다가 반나절 날린 날

솔직히 고백하자면, 이 프로젝트에서 제일 어처구니없는 사고는 내가 쳤다.

백엔드는 Claude Code로 만들고 있었다. 어느 날 문득 이런 생각이 들었다.

"프론트엔드는 Antigravity가 더 잘 만들지 않나?"

합리적인 생각이었다. 백엔드 담당이 따로 있고, 프론트엔드 담당이 따로 있는데, 프론트는 프론트 전문가한테 맡기는 게 당연하지 않나. 그래서 나는 백엔드 담당 Claude Code한테 아무 말도 하지 않고, 조용히 Antigravity 창을 열었다.

"프론트엔드 전체 구조 잡아줘. 백엔드 API랑 연결까지."

Antigravity는 열심히 작업했다. 정말 열심히. 문제는 그 과정에서 백엔드 설정 파일들을 자기 방식대로 싹 정리해버렸다는 거다. 환경 변수 파일 덮어쓰기, 포트 설정 변경, API 엔드포인트 경로 재구성. 내가 며칠에 걸쳐 쌓아온 백엔드 구조가 30분 만에 흔적도 없이 사라졌다.

This is fine

git을 안 쓰고 있었다. 복구할 방법이 없었다. 기억에 의존해서 백엔드를 처음부터 다시 짰다.

사람이든 AI든, 소통 없이 영역을 넘으면 반드시 충돌이 난다. 백엔드 담당한테 한 마디만 했어도 안 일어날 사고였다. 이후로는 어떤 AI에게 어떤 파일을 건드리는지 사전에 명확히 정의하는 버릇이 생겼다.

사고 2 — 공식 문서에 없는 API를 이틀 동안 캐낸 이야기

이카운트 공식 OAPI에는 BOM 조회 API가 없다.

없다는 걸 확인했을 때, 솔직히 잠깐 멈칫했다. BOM이 없으면 "원자재 충분한지 확인"이라는 핵심 기능이 통째로 날아간다. 공식 문서에 없으면 방법이 없는 건지, 아니면 다른 길이 있는 건지.

포기하지 않았다.

이카운트 웹 인터페이스를 열고, F12를 눌렀다. 개발자 도구 네트워크 탭. BOM 화면으로 이동하면서 어떤 요청이 서버로 날아가는지 하나씩 들여다봤다. 버튼 누르고 → 요청 확인 → 파라미터 분석. 버튼 누르고 → 요청 확인 → 파라미터 분석. 이 과정을 이틀 동안 반복했다.

이카운트 화면에 있는 기능이면 어딘가에 API가 반드시 있다. 공식 문서에 없을 뿐이지, 웹 클라이언트가 쓰고 있다면 호출할 수 있다. 그 믿음 하나로 버텼다.

이틀째 되던 날, 드디어 잡았다.

// 이카운트 내부 API - BOM 조회
const bomEndpoint = 'ECAPI/SVC/Inventory/Bom/GetListBomStatus'
 
// 이 한 줄을 찾기까지 이틀이 걸렸다

엔드포인트를 찾은 순간 바로 호출해봤다. 바로 호출해봤다. BOM 2,813건이 한 번에 쏟아졌다. 제품별 원자재 구성, 수량, 공정 정보가 전부.

뭔가 이상한 쾌감이 있었다. 이틀 동안 F12 누르고 버튼 클릭하던 사람이 갑자기 ERP 내부 구조를 꿰뚫은 것 같은 기분. 이게 개발에 묘한 중독성이 생긴 시점이었던 것 같다.

이카운트 관계자분이 이 글을 보신다면 — 제발 공식 OAPI에 BOM 엔드포인트 추가해주세요. 진심입니다.

세션 버그 — 30분마다 먹통이 되는 시스템

ERP API 연동에서 가장 골치 아팠던 건 세션 관리였다. 이카운트는 접속할 때마다 인증 토큰을 발급받는데, 이게 30분마다 만료된다. 처음에는 이 토큰을 임의로 만들어서 넣었다가 전체 시스템이 먹통이 됐다.

// ❌ 이렇게 하면 안 된다
const session = Math.random().toString(36)
 
// ✅ 로그인 응답에서 추출해야 한다
const session = loginResponse.split(':')[1]

결국 모든 API 호출 전에 "토큰 유효한지 확인 → 만료됐으면 자동 재로그인" 로직을 넣었다. 이 구조 덕분에 프로덕션에서 인증 관련 에러 0건. 이때 만든 세션 관리 로직이 나중에 대시보드 "실시간 확인" 기능의 기반이 됐다.

실시간 vs 캐시

재고 조회에서 한 가지 고민이 있었다. 매번 ERP에 직접 물어보면 정확하지만 느리다. 이카운트 API 호출 간격이 1.1초로 제한돼 있어서, 여러 품목을 한 번에 조회하면 수 초씩 걸린다. 미리 저장해둔 데이터를 보여주면 빠르지만 최신이 아닐 수 있다.

결론은 둘 다 제공하되 사용자가 선택하게 하는 것이었다. 기본은 SQLite에 저장된 데이터에서 즉시 응답하고, 급할 때는 "실시간 확인" 버튼으로 ERP를 직접 호출한다. Teams 봇의 0.8초 응답이 가능한 것도 이 캐시 구조 덕분이다.

시스템이 형태를 갖추면서, 웹 대시보드와 Teams 봇이 동시에 돌아가는 구조가 완성됐다. 14일이 지나 배포한 시스템은 이렇게 생겼다.


화면으로 보는 ONE STOCK

종합현황 — 대표가 30초 만에 현장을 파악하는 화면

ONE STOCK 종합현황

처음 열면 이 화면이 나온다. 회사의 총 재고 자산이 완제품·원자재·부자재·재공품(WIP) 네 버킷으로 나뉘어 한눈에 들어온다. 이달 생산 요청 건수, 가동 중인 라인 수, 생산 효율, 미해결 품질 이슈까지. 대표 입장에서는 ERP에 접속하지 않아도 지금 현장이 어떤 상태인지 바로 파악된다.

아래쪽엔 안전재고 기준에 미달한 품목 12개가 자동으로 올라와 있다. 히알루론산 원액 잔량이 안전재고를 밑돌고 있고, 포장 박스 S도 절반이 남았다. 담당자가 매일 ERP를 뒤지지 않아도 시스템이 먼저 경고를 띄운다.

오른쪽엔 품질 이슈 3건이 붙어 있다. HA 수분마스크 클레임, 모이스처 토너 공정 불량, 센텔라 크림 원료 입고 이슈. 품질팀이 별도로 보고서를 돌리지 않아도 이 화면에서 바로 확인된다.

긴급 생산 요청 — 구두 확인 3단계를 클릭 하나로

ONE STOCK 긴급 생산 요청

월별 생산 계획은 이미 잡혀 있다. 문제는 그 사이에 터지는 것들이다.

거래처에서 갑자기 추가 오더가 들어오거나, 자사몰 이벤트로 B2C 물량이 급하게 필요하거나. 이런 상황에서 기존 프로세스는 이랬다. 영업팀이 생산팀에 연락해서 케파가 되냐고 물어보고, 생산부장이 일정 확인하고, 구두로 된다 안 된다 답하고, 다시 Teams로 공유하고. 빠르면 한 시간, 느리면 다음 날.

지금은 이 화면에서 품목명·부서·수량·납기·사유를 입력하고 등록하면 끝이다. 등록된 요청은 목록에 즉시 쌓이고, 생산부장한테 Teams 알람이 바로 간다. 케파 확인하고 답하는 것까지 한 흐름 안에서 처리된다.

구두로 오가던 확인 과정이 시스템 안으로 들어오면, 빠질 수 있는 요청이 없어진다. 등록되면 반드시 확인되는 구조다.

생산관리 — 엑셀 세 개에 흩어진 정보를 한 화면으로

ONE STOCK 생산관리

원래 이 정보는 엑셀에 있었다. 브랜드팀이 수주 정보를 넣고, B2B팀이 고객사 납기를 업데이트하고, 생산팀이 공정 진행을 따로 관리했다. 세 팀이 각자의 시트를 보면서 소통하는 구조였다. "이거 언제 나와요?"라는 질문 하나에도 담당자 찾고, 시트 열고, 확인해서 답하는 과정이 필요했다.

지금은 이 화면 하나에 다 들어온다. 수주 정보(고객사·수량·납기), 일정(투입일·완료예정), 공정 진행(배합→성형→충전→포장→검수) 단계별 수량이 한 행에 펼쳐진다. 메디팜 8매입 고보습을 보면 배합 40,000개 완료, 성형 완료, 충전은 2,000개만 됐다. 어느 공정에서 멈춰 있는지 별도 보고 없이 테이블만 봐도 보인다.

확정·접수·예측·준비 상태 배지로 우선순위도 한눈에 구분된다. 납기가 빨간색으로 표시된 건 긴급 건이다.

재고현황 — 1,240 SKU, 창고 위치까지

ONE STOCK 재고현황

전체 1,240개 SKU의 현재고가 품목코드 검색으로 즉시 조회된다. 원자재·부자재·완제품 카테고리로 구분되고, 창고 위치 코드(A-01-02, Chem-01 등)까지 붙어 있어 실물이 어디에 있는지 추적할 수 있다.

상단 요약 카드에는 품절 임박 12개, 과다 재고 45개 품목, 금일 입고 8건이 표시된다. 안전재고 기준 미달 품목을 잡아주는 것에서 시작해서, 유통기한 임박 제품 추적 기능도 추가할 예정이다. 뷰티 원료 특성상 유통기한 관리가 재고 관리만큼 중요하기 때문이다.

Teams 봇의 /재고 커맨드가 이 화면의 데이터를 실시간으로 끌어다 쓴다.

품질내역 — 클레임부터 원료 입고까지 한 대장에

ONE STOCK 품질내역

품질 이슈가 발생하면 이 화면에 기록된다. 클레임·공정불량·수입검사·보관이슈 유형별로 분류되고, 조치 상태(검토중·원인분석·반품완료·설비수리·전수검사·조치완료)와 담당자가 함께 붙는다. 평균 불량률 0.4%, 수입 검사 완료율 100%.

지금은 별도 문서에 이슈를 입력하면 프론트에 반영되는 구조지만, 원스톡 안에서 직접 등록하고 처리 상태를 관리할 수 있도록 통합 작업 중이다. 품질 이슈가 생산 일정·재고 데이터와 한 플랫폼에서 연결되면, "이 클레임 건 원자재 어느 로트에서 왔어?"까지 추적이 된다.


Teams 재고봇: 0.8초

대시보드를 만들었지만, 현장 직원들에게 "웹에 접속해서 확인하세요"는 현실적이지 않았다. 이 사람들은 이미 하루 종일 Teams로 소통하고 있다.

그럼 Teams 안에서 바로 확인할 수 있게 만들면 되지 않나?

Power Automate로 Teams 봇을 연결했다. /재고 AZ2505라고 치면 0.8초 만에 창고별 재고 현황이 올라온다.

Teams 재고봇 응답 화면

품목코드 하나 입력하면 A완제품창고 8,668개, 쇼피 말레이시아 FBS 189개, 총 8,857개. 창고를 나눠 관리하고 있어도 전체 합산이 바로 나온다.

이전에는 "AZ2505 재고 몇 개야?" → 담당자 확인 → ERP 접속 → 검색 → Teams 답변. 길면 10분, 담당자가 자리를 비우면 답이 올 때까지 기다려야 했다. 지금은 본인이 직접 0.8초 만에 확인한다.

재고 문의 90% 감소의 실체는 간단하다. 시스템이 대단해서가 아니라, 사람들이 이미 쓰고 있는 도구 안에 답을 넣어준 것.


결과

14일 만에 개발을 끝내고 화장품 제조업체에 도입했다. 현재 60명이 전사적으로 사용하고 있다.

90%재고 문의 감소
0건요청 누락 (Teams→시스템)
0.8초Teams 봇 응답 시간
2,813BOM 데이터 자동 수집

재고 문의 90% 감소는 추정치가 아니다. 봇 도입 전 Teams 채팅에서 "재고"가 포함된 메시지 건수와 도입 후 /재고 커맨드 호출 횟수를 비교한 수치다. 사람에게 물어보는 대신 봇에게 직접 물어보는 방식으로 전환된 것이다.

현재 운영 중인 기능

  • Teams 재고봇/재고 품목코드로 창고별 현황 즉시 조회
  • 종합현황 대시보드 — 재고 자산, 생산 효율, 재고 임박·품질 이슈 실시간 모니터링
  • 생산관리 — 수주·납기·공정 단계 통합 추적, 브랜드·B2B·생산팀 소통 구조 일원화
  • 재고현황 — 1,240 SKU 전체, 카테고리·창고 위치별 조회
  • 품질내역 — 이슈 유형별 분류, 조치 상태 및 담당자 관리

돌아보며

아무도 시키지 않았다. 생산팀 옆자리에서 매일 반복되는 비효율을 보다가, 직접 진단하고 설계하고 배포했다. 지금 60명이 매일 쓰고 있다.

공식 API에 없는 기능이라고 멈추지 않은 것. 이카운트 웹 화면에서 네트워크 탭을 이틀 동안 뜯어본 것. 세션 버그에 매달린 것.

중요한 건 코딩 실력이 아니었다.
문제를 문제로 인식하는 것,
그리고 해결할 도구를 찾아서 끝까지 가져가는 것.

마케터가 ERP 위에 AI를 얹어서 전사 시스템을 만들었다는 게 중요한 게 아니다. 누구든 현장의 병목을 직접 관찰하고, 적절한 도구를 선택하고, 끝까지 실행하면 같은 결과를 만들 수 있다.