구글시트 출고 자동화 방법: Apps Script 2편
도입 – 출고할 때마다 재고를 손으로 빼고 있다면
구글시트로 재고 관리를 시작하면 가장 먼저 부딪히는 문제가 출고 처리입니다. 주문이 들어올 때마다 LOCATIONS 시트에서 수량을 찾고 계산기로 빼서 다시 입력하고, OUTBOUND 시트에 이력을 적어 두는 일을 반복하면 사람 손을 타는 구간이 너무 많습니다. 특히 여러 사람이 동시에 같은 시트를 만지면 “어느 시점 재고를 기준으로 뺀 것인지”가 금방 꼬이기 쉽습니다.
이 글에서는 구글시트 출고 자동화 방법을 중심으로, Apps Script로 출고 사이드바를 만들고 LOCATIONS 재고를 자동 차감하는 코드를 정리합니다. 1편 — 구글시트로 창고 재고관리 시스템 만들기에서 입고 처리와 적치 저장(savePutaway)까지 만들었다면, 이번 2편에서는 출고 시트(OUTBOUND)를 추가하고 savePicking 함수를 구현해, 사이드바에서 출고 정보를 입력하면 곧바로 OUTBOUND 기록과 LOCATIONS 음수 행 차감까지 한 번에 처리하는 흐름을 완성합니다.
출고 자동화를 위한 기본 시트 구조
구글시트에서 출고 자동화를 구현하려면, 먼저 시트 구조와 데이터 흐름을 단순하게 정리하는 것이 좋습니다. 실무에서 비교적 안정적이었던 기본 구조는 다음 네 가지 시트에 한 장을 더 추가하는 형태입니다.
INBOUND– 입고 예정·실적 기록LOCATIONS– 로케이션별 현재 재고(입·출고 모두 누적)FLOOR_MAP– 창고 로케이션 정의SETTINGS– 드롭다운 옵션, 코드값 등 공통 설정OUTBOUND– 출고 이력 기록(이번 글에서 추가)
1편에서 이미 앞 네 개 시트와 “창고” 메뉴, 입고 사이드바, 적치 저장 함수(savePutaway)까지 구현했다고 가정합니다. 이 상태에서 LOCATIONS 시트는 각 로케이션·모델별로 누적 수량을 합산해 현재 재고를 볼 수 있습니다.
출고 단계에서는 최소한 다음 정보가 남아야 이후 추적이 가능합니다.
- 어떤 주문 번호(또는 출고 지시 번호)로
- 어느 로케이션에서
- 어떤 모델을
- 몇 개 출고했는지
- 언제 처리했는지
그래서 OUTBOUND 시트에는 보통 아래 다섯 열을 기본으로 둡니다.
- A열:
ORDER_NO - B열:
LOCATION - C열:
MODEL - D열:
QTY - E열:
TIMESTAMP
이 구조를 만들어 둔 뒤, Apps Script에서 이 시트를 향해 출고 이력과 재고 차감을 동시에 기록하는 방식으로 구현합니다.
OUTBOUND 시트와 기본 상수 정의
먼저, Apps Script에서 여러 번 사용할 시트 이름을 상수로 묶어 두면 이후 유지보수가 편합니다. 시트 이름이 바뀌어도 상수 한 곳만 수정하면 되기 때문입니다.
- 이 코드가 하는 일
- INBOUND·LOCATIONS·OUTBOUND 등 주요 시트 이름을 상수로 정의합니다.
- 붙여넣을 위치
- 구글시트 → 확장 프로그램 → Apps Script →
Code.gs파일 맨 위.
- 붙여넣은 뒤 할 일
- 저장만 하면 됩니다. 별도 실행은 필요 없습니다.
const SHEET_INBOUND = 'INBOUND'; // → 입고 시트 이름
const SHEET_LOCATIONS = 'LOCATIONS'; // → 재고 시트 이름
const SHEET_OUTBOUND = 'OUTBOUND'; // → 출고 시트 이름
const SHEET_SETTINGS = 'SETTINGS'; // → 설정 시트 이름
const SHEET_FLOOR = 'FLOOR_MAP'; // → 로케이션 시트 이름제대로 됐는지 확인하는 법: Apps Script 편집기에서 빨간 오류 표시가 없고, 실제 스프레드시트 탭 이름과 상수 값이 동일하면 준비가 된 것입니다.
그다음 스프레드시트에서 OUTBOUND 탭을 새로 만들고, A1~E1에 다음 헤더를 입력해 둡니다.
- A1:
ORDER_NO - B1:
LOCATION - C1:
MODEL - D1:
QTY - E1:
TIMESTAMP
이제 출고 이력을 받을 최소한의 틀이 갖춰졌습니다.
getStockByLocation – 로케이션별 현재 재고 조회
출고 자동화의 핵심은 “지금 이 로케이션에 이 모델이 몇 개 있는가”를 정확히 계산하는 일입니다. LOCATIONS 시트에서 해당 로케이션·모델의 수량을 모두 합산해 현재 재고를 계산하는 함수가 필요합니다. 이 함수는 이후 출고 검증뿐 아니라, 사이드바에서 현재고를 보여 줄 때도 재사용할 수 있습니다.
- 이 코드가 하는 일
LOCATIONS전체를 읽어, 주어진 로케이션·모델 조합의 수량을 합산해 현재 재고를 반환합니다.
- 붙여넣을 위치
- Apps Script →
Code.gs파일, 상수 선언 아래 적당한 위치.
- 붙여넣은 뒤 할 일
- 저장 후, 테스트 실행으로 로그를 확인해 볼 수 있습니다.
function getStockByLocation(location, model) { // → 로케이션·모델 재고 조회
const ss = SpreadsheetApp.getActive(); // → 현재 스프레드시트
const sheet = ss.getSheetByName(SHEET_LOCATIONS); // → LOCATIONS 시트 선택
const range = sheet.getDataRange(); // → 전체 데이터 범위
const values = range.getValues(); // → 2차원 배열로 읽기
let total = 0; // → 합계 변수
for (let i = 1; i < values.length; i++) { // → 헤더 제외 반복
const row = values[i]; // → 현재 행
const rowLocation = row[0]; // → A열: LOCATION
const rowModel = row[1]; // → B열: MODEL
const qty = row[2]; // → C열: QTY
if (rowLocation === location && rowModel === model) { // → 조건 일치
total += Number(qty) || 0; // → 수량 합산
}
}
return total; // → 현재 재고 반환
}제대로 됐는지 확인하는 법: LOCATIONS에 테스트 데이터(같은 로케이션·모델로 양수·음수 수량 몇 줄)를 넣어 둔 뒤, 아래 테스트 함수를 Code.gs 맨 아래에 붙여넣고 편집기에서 실행해 실행 로그(보기 → 실행 로그)에 찍힌 값을 확인하시기 바랍니다.
function testGetStock() { // → 편집기에서 실행할 테스트 함수
const result = getStockByLocation('L-01', 'ABC123'); // → 본인 데이터에 맞게 수정
Logger.log('현재고: ' + result); // → 실행 로그에 결과 출력
}시트 셀에 =getStockByLocation(...) 형태로 넣는 방법도 있지만, 커스텀 함수는 재계산 시점이 다르고 사용할 수 있는 서비스에 제한이 있어 값이 갱신되지 않는 것처럼 보일 수 있습니다. 테스트는 위처럼 편집기에서 하는 편이 확실합니다.
savePicking – 재고 검증 후 OUTBOUND 기록과 LOCATIONS 차감
이제 출고 핵심 함수인 savePicking을 구현합니다. 이 함수는 사이드바에서 전달받은 출고 정보를 바탕으로 다음 순서로 동작합니다.
getStockByLocation으로 현재 재고 조회- 출고 수량이 0보다 큰지, 그리고 재고보다 많지 않은지 검증
OUTBOUND시트에 출고 이력 한 줄 추가LOCATIONS시트에 음수 수량 행을 추가해 재고 차감
재고를 직접 수정하지 않고, 입고·출고를 모두 누적 기록으로 남기는 구조이기 때문에 추후 감사나 오류 추적이 훨씬 수월합니다.
- 이 코드가 하는 일
- 출고 요청 데이터를 검증하고, OUTBOUND·LOCATIONS 두 시트에 동시에 반영합니다.
- 붙여넣을 위치
- Apps Script →
Code.gs파일,getStockByLocation아래.
- 붙여넣은 뒤 할 일
- 저장 후, 나중에 사이드바 자바스크립트에서 이 함수를 호출합니다.
function savePicking(data) { // → 출고 저장 함수
const location = String(data.location || '').trim(); // → 출고 로케이션
const model = String(data.model || '').trim(); // → 출고 모델
const orderNo = String(data.orderNo || '').trim(); // → 주문 번호
const qty = Number(data.qty); // → 출고 수량(숫자로 변환)
if (!location || !model || !orderNo) { // → 빈 값이 저장되는 사고 방지
throw new Error('주문번호·로케이션·모델을 모두 입력해 주세요.');
}
if (!isFinite(qty) || qty <= 0) { // → 숫자가 아니거나 0 이하 차단
throw new Error('출고 수량은 1 이상의 숫자여야 합니다.');
}
const lock = LockService.getScriptLock(); // → 동시 출고 충돌을 막는 잠금
if (!lock.tryLock(10000)) { // → 최대 10초 대기
throw new Error('다른 사용자가 처리 중입니다. 잠시 후 다시 시도해 주세요.');
}
try { // → 잠금 안에서 읽기·쓰기를 함께 처리
const ss = SpreadsheetApp.getActive(); // → 현재 스프레드시트
const currentStock = getStockByLocation(location, model); // → 잠금 후 재고 조회
if (qty > currentStock) { // → 재고 부족 검증
throw new Error('재고가 부족합니다. 현재고: ' + currentStock);
}
const timestamp = new Date(); // → 현재 시각
ss.getSheetByName(SHEET_OUTBOUND).appendRow([ // → 출고 이력 추가
orderNo, // → 주문 번호
location, // → 로케이션
model, // → 모델
qty, // → 출고 수량
timestamp // → 시간
]);
ss.getSheetByName(SHEET_LOCATIONS).appendRow([ // → 재고 차감 행 추가
location, // → 로케이션
model, // → 모델
-qty, // → 음수 수량
'PICKING', // → 입·출고 구분
timestamp // → 시간
]);
SpreadsheetApp.flush(); // → 시트에 즉시 반영
return { // → 결과 반환
success: true, // → 성공 여부
remaining: currentStock - qty // → 출고 후 재고
};
} finally {
lock.releaseLock(); // → 성공·실패와 무관하게 잠금 해제
}
}제대로 됐는지 확인하는 법: LOCATIONS에 특정 로케이션·모델로 100개 입고 행(양수)을 넣어 둔 뒤, savePicking을 테스트 호출해 10개를 출고하면, OUTBOUND에 한 줄, LOCATIONS에는 음수 -10 행이 추가되고, getStockByLocation 결과가 90으로 줄어 있어야 합니다.
실습실 — 출고 처리를 여기서 바로 돌려 보기
시트를 만들기 전에 출고 로직이 어떻게 도는지 눈으로 먼저 볼 수 있습니다. 아래는 이 페이지 안에서 실행되는 구글시트 시뮬레이터입니다. 브라우저 안에 만들어 둔 가짜 시트 위에서 위 코드가 그대로 돌아가며, 계정 로그인이나 권한 승인은 필요하지 않습니다.
▶ 실행을 누르면 L-01 로케이션의 ABC123 재고 100개에서 10개를 출고합니다. OUTBOUND에 이력 한 줄이 쌓이고 LOCATIONS에 -10 행이 추가되면서 재고가 90으로 줄어드는 과정을 로그에서 확인해 보시기 바랍니다. 코드에서 qty: 10을 qty: 500으로 바꿔 다시 실행하면 재고 부족 검증이 실제로 막아 주는 것도 볼 수 있습니다. ↺ 시트 초기화를 누르면 처음 상태로 돌아갑니다.
코드를 고친 뒤 실행을 눌러 보세요. 실제 구글시트가 아니라 브라우저 안의 가짜 시트에서 도는 시뮬레이터입니다.
실습실 코드는 본문 코드에서 사이드바 화면 부분만 덜어낸 것입니다. 시뮬레이터에는 입력 화면이 없어 demo() 함수가 사람 대신 값을 넣어 주며, 잠금·검증·기록 순서는 실제 시트에서와 완전히 같습니다.
사이드바 HTML – 입력 필드와 현재고 표시, 출고 버튼 연결
실무에서는 Apps Script 함수를 직접 실행하기보다, 사이드바에서 현장 작업자가 폼을 입력하고 버튼을 누르는 방식이 훨씬 직관적입니다. 1편에서 입고용 사이드바를 이미 만들었다면, 같은 HTML 파일 안에 출고용 섹션을 한 블록 더 추가하는 식으로 구성할 수 있습니다.
- 이 코드가 하는 일
- 출고용 입력 폼(로케이션·모델·수량·주문 번호)을 만들고, 현재고 표시와 출고 저장 버튼을 배치합니다.
- 붙여넣을 위치
- Apps Script 프로젝트 내 사이드바 HTML 파일(예:
sidebar.html)의<body>내부.
- 붙여넣은 뒤 할 일
- 저장 후, 스크립트 메뉴에서 사이드바를 다시 열어 UI를 확인합니다.
<div id="picking-section">
<h3>출고 처리</h3>
<label>LOCATION</label>
<input type="text" id="pickLocation">
<label>MODEL</label>
<input type="text" id="pickModel">
<button type="button" onclick="onClickCheckStock()">현재고 조회</button>
<div id="currentStockDisplay">현재고: -</div>
<label>QTY</label>
<input type="number" id="pickQty" min="1">
<label>ORDER NO</label>
<input type="text" id="pickOrderNo">
<button type="button" onclick="onClickSavePicking()">출고 저장</button>
</div>
<script>
function onClickCheckStock() { // → 현재고 조회 버튼
const location = document.getElementById('pickLocation').value;
const model = document.getElementById('pickModel').value;
google.script.run
.withSuccessHandler(function(stock) { // → 성공 시
document.getElementById('currentStockDisplay').innerText =
'현재고: ' + stock; // → 표시 갱신
})
.withFailureHandler(function(err) { // → 실패 시
alert('현재고 조회 오류: ' + err.message); // → 안내
})
.getStockByLocation(location, model); // → Apps Script 호출
}
function onClickSavePicking() { // → 출고 저장 버튼
const location = document.getElementById('pickLocation').value;
const model = document.getElementById('pickModel').value;
const qty = document.getElementById('pickQty').value;
const orderNo = document.getElementById('pickOrderNo').value;
const data = { // → 전달 데이터
location: location,
model: model,
qty: qty,
orderNo: orderNo
};
google.script.run
.withSuccessHandler(function(result) { // → 성공 시
alert('출고가 저장되었습니다. 남은 재고: ' + result.remaining);
document.getElementById('currentStockDisplay').innerText =
'현재고: ' + result.remaining; // → 화면 재고 갱신
})
.withFailureHandler(function(err) { // → 실패 시
alert('출고 중 오류: ' + err.message); // → 오류 안내
})
.savePicking(data); // → Apps Script 호출
}
</script>제대로 됐는지 확인하는 법: 스프레드시트에서 “창고” 메뉴 → 사이드바 열기 후, 로케이션·모델을 입력하고 [현재고 조회]를 눌렀을 때 LOCATIONS 기준 수량이 뜨고, 출고 수량·주문 번호를 채운 뒤 [출고 저장]을 누르면 OUTBOUND와 LOCATIONS 두 시트에 동시에 행이 추가되어야 합니다.
실무에서 챙겨야 할 동시 작업·권한·테스트 팁
출고 자동화 코드는 돌아가기만 하면 끝이 아니라, 실제 창고에서 여러 사람이 동시에 쓰는 상황까지 고려해야 합니다. 실무에서 겪었던 부분 중 꼭 짚고 싶은 것들을 정리합니다.
1) 여러 사람이 동시에 출고할 때의 기준 재고 문제
여러 작업자가 같은 로케이션·모델을 동시에 출고하면, 거의 같은 시점의 currentStock을 기준으로 각각 차감을 시도하게 됩니다. 이 경우 실제 재고보다 많이 빠져서 마이너스 재고가 생길 수 있습니다.
완벽한 해결은 트랜잭션 기반 WMS를 쓰는 것이지만, 구글시트 환경에서는 다음 수준의 완화책이 현실적입니다.
savePicking안에서 재고를 한 번 더 조회해 검증하는 현재 구조를 유지합니다.- 재고가 부족하면 바로 오류를 던져, 잘못된 출고가 기록되지 않도록 합니다.
- 동시 작업이 자주 생기는 품목은 “한 번에 한 사람만 출고”라는 운영 규칙을 두고, 피킹 순번을 정리하는 방식으로 보완합니다.
위 savePicking 코드에는 이미 LockService.getScriptLock() 으로 락을 걸고 finally에서 해제하는 구조가 들어가 있습니다. 이렇게 하면 같은 시점에 들어온 출고 요청도 순차적으로 처리되기 때문에, 실제 재고보다 많이 빠지는 상황을 줄일 수 있습니다. 다만 락을 과도하게 사용하면 속도가 느려질 수 있으니, 동시성이 높은 구간에만 최소한으로 적용하는 것이 좋습니다.
2) 스크립트 권한과 공유 범위
출고 자동화를 여러 명이 함께 쓰려면, 다음 사항을 미리 점검해 두는 편이 좋습니다.
- 스크립트 소유자를 업무용 공용 계정으로 두고, 시트를 해당 계정 소유로 맞춰 두면 향후 인사 변경 시에도 안정적입니다.
- 최초 실행 시 나오는 권한 승인 팝업은 담당자가 한 번 승인해 두어야 합니다.
- 외부 인원(3PL, 외주 창고 등)에게도 쓰게 할 경우, 구글 계정·접근 권한 정책을 사전에 합의해 두는 것이 안전합니다.
3) 테스트 데이터와 검증 절차
실제 운영에 넣기 전, 최소한 아래 수준의 테스트는 권장합니다.
- 로케이션·모델 여러 개를 만들어, 각각 입고 → 출고를 여러 번 반복해 합계가 맞는지 확인
- 일부러 재고보다 많은 수량을 출고 요청해, 오류 메시지가 제대로 뜨는지 확인
- 사이드바에서 필수값(로케이션·모델·수량·주문 번호)을 비우고 저장을 눌러, 어떤 형태로든 잘못된 데이터가 안 들어가도록 보완
위 코드에는 주문번호·로케이션·모델이 비어 있거나 수량이 숫자가 아닐 때 저장을 막는 검증이 이미 들어가 있습니다. 현장 상황에 맞춰 추가 규칙(예: 로케이션 코드 형식 검사)을 덧붙이면 실수를 더 줄일 수 있습니다.
맺음 – 작은 자동화로도 출고 실수를 크게 줄일 수 있다
이번 글에서는 구글시트와 Apps Script를 이용해 출고 사이드바를 만들고, OUTBOUND 이력 기록과 LOCATIONS 재고 차감을 자동으로 처리하는 방법을 정리했습니다. 핵심은 다음 세 가지입니다.
- 출고 이력용
OUTBOUND시트를 별도로 두고 getStockByLocation으로 로케이션·모델별 현재고를 계산한 뒤savePicking에서 재고 검증 → OUTBOUND 기록 → LOCATIONS 음수 행 추가를 한 번에 처리하는 것
이 구조를 갖추면, 기존에 사람이 계산기로 빼고 적던 구간을 상당 부분 자동화할 수 있고, 입·출고 전체 이력도 구글시트 안에서 투명하게 남길 수 있습니다.
다음 단계로는 바코드 스캐너 입력, 주문 목록과의 연동, 피킹리스트 자동 생성 등으로 확장해 볼 수 있습니다. 하지만 현장에서 가장 효과가 큰 건, 지금까지 설명한 “기본 출고 자동화”만 제대로 돌려도 충분히 체감하실 겁니다. 필요에 따라 이번 구조를 그대로 복사해 테스트용 파일을 하나 더 만든 뒤, 팀원들과 함께 시범 운영해 보며 조금씩 다듬어 가는 방식을 추천합니다.