구글시트 웹앱 화면 여러 개 만들기: Apps Script doGet page 라우팅 — 입고 예약 시스템 4편
도입: 구글시트 웹앱 화면 여러 개, 한 번에 관리하고 싶을 때
구글시트 웹앱 화면 여러 개 만들기를 찾다 보면, 화면마다 Apps Script 프로젝트를 따로 만드는 예시가 많이 보입니다. 입고 예약처럼 체크인 화면, 관리용 대시보드, 현장 조회 화면을 따로 운영하다 보면 프로젝트가 여러 개로 늘어나고, 주소·권한·공통 함수가 서로 어긋나 실제 운영에서 유지보수가 어렵습니다.
이 글에서는 한 Apps Script 프로젝트 안에서 doGet의 page 파라미터만으로 여러 HTML 화면을 고르는 방법을 정리합니다. 주소 뒤에 ?page=checkin처럼 붙여 어떤 화면을 띄울지 고르고, 같은 코드 안에서 나중에 사이드바와 브라우저 웹앱을 함께 쓰기 좋도록 구조를 잡습니다.
앞 편인 구글시트 입고 예약 시스템 시트 구조 만들기 — 기본 1편에서 기본 시트 구조를 만들었다고 가정합니다. 이번 4편의 목표는, 그 위에 사람이 실제로 사용하는 웹 화면을 여러 개 얹는 뼈대를 완성하고, Apps Script doGet page 라우팅과 구글시트 웹앱 배포 방법을 한 번에 마치는 것입니다.
APPT 시리즈 구조 복습과 이번 편 목표
입고 예약 시스템은 현장 기사, 도어 배정 담당, 사무실 운영자가 동시에 사용하는 경우가 많습니다. 실무에서 나눠 보면 보통 다음 세 가지 화면이 필요합니다.
- 외부 운송사·거래처가 예약을 넣는 브라우저 웹앱
- 운영자가 예약 현황을 보며 수정·보완하는 관리용 화면
- 입고 담당자가 도착 차량을 체크인하는 현장 화면
각각을 다른 Apps Script 프로젝트로 만들면, 시트 구조·비즈니스 규칙이 바뀔 때마다 모든 프로젝트를 함께 고쳐야 하고, 어느 프로젝트가 최신인지 헷갈리기 쉽습니다. 그래서 이 시리즈는 처음부터 한 Apps Script 프로젝트에 전 편 코드를 모아도 충돌이 나지 않게 설계합니다.
이를 위해 예약 관련 상수와 함수 이름에는 모두 APPT_ 접두어를 붙입니다. 예를 들어 시트·웹앱 설정은 APPT_CONFIG, APPT_WEBAPP_CONFIG, 메뉴 관련 상수는 APPT_MENU_CONFIG처럼 작성합니다. 값이 같더라도 이름이 겹치면 const 중복 선언 오류가 나기 때문입니다. 1편에서 만든 getOrCreateSheet_() 같은 공용 함수는 그대로 사용하며, 이 글에서는 다시 싣지 않습니다. 날짜·시간 도우미(APPT_ymd_, APPT_hm_)는 5편에서 따로 다룹니다.
이번 편에서 완성할 항목은 세 가지입니다.
doGet(e)에서e.parameter.page값에 따라 다른 HTML 화면을 선택하는 라우팅 함수 만들기onOpen()과 연동되는APPT_addWebAppMenu_()로 시트 상단 메뉴에서 웹앱 각 화면을 여는 구조 만들기HtmlService템플릿에page와 제목을 전달해 화면마다 다른 제목이 뜨도록 만들기
최종 확인은 간단합니다. 웹앱을 한 번 배포한 뒤, 배포 주소와 ?page=checkin 주소를 각각 열었을 때 서로 다른 화면이 뜨고, 시트 상단 메뉴에서 홈·체크인 웹앱을 새 탭으로 띄울 수 있으면 이 편 목표는 달성한 것입니다.
doGet(e)로 page 파라미터에 따라 화면 나누기
먼저 Apps Script doGet(e)에서 URL의 page 파라미터를 읽어 어떤 HTML 파일을 띄울지 고르는 라우터 함수를 만듭니다. 이 구조를 한 번 잡아 두면 화면이 늘어나도 VALID_PAGES와 템플릿 파일만 추가하면 되므로 유지보수가 훨씬 수월합니다.
이 코드는 이 프로젝트의 Code.gs 파일 상단에 붙여 넣습니다. 이미 다른 글에서 doGet을 만든 적이 있다면, 그 함수 내용을 이 라우팅 구조로 교체해야 합니다. doGet은 프로젝트당 하나만 존재할 수 있기 때문입니다.
// → 여기만 본인 환경에 맞게 바꾸세요
const APPT_WEBAPP_CONFIG = { // → 웹앱 관련 설정 모음
DEFAULT_PAGE: 'home', // → 기본 화면 키
VALID_PAGES: ['home', 'checkin'], // → 허용하는 page 목록
TITLE_MAP: { // → 화면별 제목
home: '입고 예약 메인 화면', // → 기본 화면 제목
checkin: '입고 체크인 화면' // → 체크인 화면 제목
}
}; // → 설정 끝
function doGet(e) { // → 웹앱 GET 요청 진입점
var page = (e && e.parameter && e.parameter.page) // → page 파라미터 읽기
? String(e.parameter.page) // → 문자열로 변환
: APPT_WEBAPP_CONFIG.DEFAULT_PAGE; // → 없으면 기본 화면
if (APPT_WEBAPP_CONFIG.VALID_PAGES // → 허용 목록 존재 확인
&& APPT_WEBAPP_CONFIG
&& APPT_WEBAPP_CONFIG.VALID_PAGES.indexOf(page) === -1) { // → 허용 목록 검사
page = APPT_WEBAPP_CONFIG.DEFAULT_PAGE; // → 잘못된 값이면 기본 화면
}
var template = HtmlService // → 템플릿 객체 생성
.createTemplateFromFile('APPT_' + page); // → 파일명 규칙: APPT_home 등
template.page = page; // → 템플릿에 page 변수 전달
template.title = APPT_WEBAPP_CONFIG.TITLE_MAP[page] || ''; // → 제목 전달
var html = template.evaluate(); // → HTML 평가
html.setTitle(template.title); // → 브라우저 탭 제목 설정
return html; // → 사용자에게 응답
}동작 확인 방법: Apps Script 편집기에서 파일 → 새로 만들기 → HTML로 APPT_home, APPT_checkin 두 파일을 간단한 내용으로라도 만들어 둔 뒤, 웹앱을 한 번 배포합니다. 그런 다음 배포 주소(예: .../exec)와 .../exec?page=checkin을 각각 열었을 때 서로 다른 제목·내용이 뜨면 라우팅은 정상입니다.
HtmlService 템플릿 파일 만들기: home·checkin 화면
라우터가 준비되었으니, 실제로 브라우저에 표시할 HtmlService 템플릿을 만듭니다. 기본 구조는 일반 HTML과 같고, <?= ... ?> 문법으로 템플릿 변수(title, page)를 사용할 수 있습니다.
이번 편에서는 기능보다는 라우팅과 링크 동작 확인이 목적이므로, 최소한의 UI만 갖춘 두 개 화면을 만듭니다.
APPT_home: 입고 예약 메인 안내 + 체크인 화면으로 이동 버튼APPT_checkin: 차량·운송사 입력 필드 + 메인 화면으로 돌아가기 링크
두 파일 모두 스크립트 편집기 상단 메뉴 → 파일 → 새로 만들기 → HTML을 눌러 추가합니다. 파일명은 확장명 없이 정확히 APPT_home, APPT_checkin으로 입력하세요. (Apps Script가 자동으로 .html 확장명을 추가합니다)
1단계 — APPT_home 기본 화면 템플릿
<!-- APPT_home.html → 입고 예약 메인 화면 -->
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<meta charset="UTF-8">
<title><?= title ?></title>
<style>
body { font-family: sans-serif; padding: 16px; }
a.button {
display: inline-block;
padding: 8px 16px;
margin-top: 8px;
border-radius: 4px;
background: #1976d2;
color: #fff;
text-decoration: none;
}
</style>
</head>
<body>
<h1><?= title ?></h1>
<p>이 화면은 입고 예약 메인 화면입니다.</p>
<p>입고 차량 체크인을 진행하려면 아래 버튼을 클릭합니다.</p>
<a class="button" href="?page=checkin">체크인 화면 열기</a>
</body>
</html>제대로 됐는지 확인하는 법: 배포 주소(예: .../exec)만 열었을 때 '입고 예약 메인 화면입니다.' 문구와 '체크인 화면 열기' 버튼이 보이면 성공입니다.
2단계 — APPT_checkin 체크인 화면 템플릿
<!-- APPT_checkin.html → 입고 체크인 화면 -->
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<meta charset="UTF-8">
<title><?= title ?></title>
<style>
body { font-family: sans-serif; padding: 16px; }
label { display: block; margin-top: 8px; }
input { padding: 4px 8px; }
.back { margin-top: 16px; }
</style>
</head>
<body>
<h1><?= title ?></h1>
<p>이 화면은 입고 차량이 도착했을 때 정보를 입력하는 체크인 창입니다.</p>
<label>차량 번호
<input type="text" id="truckNo">
</label>
<label>운송사명
<input type="text" id="carrier">
</label>
<div class="back">
<a href="?page=home">메인 화면으로 돌아가기</a>
</div>
</body>
</html>제대로 됐는지 확인하는 법: 배포 주소 뒤에 ?page=checkin을 붙여 열었을 때 체크인 설명·입력 칸·돌아가기 링크가 보이고, 돌아가기 링크를 누르면 다시 메인 화면으로 이동하면 됩니다.
시리즈에서 onOpen이 겹치지 않게 합치는 방법
이 시리즈의 다른 글들(1, 2, 3편)에서도 상단 메뉴를 만들기 위해 onOpen()을 정의합니다. 한 프로젝트에 같은 이름의 함수가 여러 개 있으면 마지막 선언만 살아남습니다. 앞 편 onOpen이 그대로 남아 있는 채로 이 편 onOpen을 또 붙이면, 앞 편 메뉴(시트 생성·시드 생성·설정 관리)가 오류 하나 없이 조용히 사라집니다.
그래서 이 시리즈는 onOpen을 프로젝트 전체에 하나만 두고, 각 편은 add○○Menu_(menu) 도우미만 추가합니다. 아래가 4편까지 반영한 완성형 onOpen입니다. 앞 편을 아직 안 붙였어도 typeof 검사 덕분에 그대로 동작합니다.
function onOpen() { // → 시트를 열 때 한 번 실행
var ui = SpreadsheetApp.getUi(); // → UI 객체
var menu = ui.createMenu('예약 도구'); // → 시리즈 공용 메뉴 (이름 통일)
// 앞 편 코드가 이미 들어 있으면 그 편 메뉴도 함께 붙는다.
// 이름은 앞 편이 **실제로 만든 것**과 한 글자도 달라선 안 된다.
if (typeof addApptBaseMenu_ === 'function') { // → 1편 기본 메뉴
addApptBaseMenu_(menu); // → 시트 생성 항목
}
if (typeof APPT_addSeedMenu_ === 'function') { // → 2편 도어·야드 시드
APPT_addSeedMenu_(menu); // → 시드 생성 항목
}
if (typeof APPT_addSettingsMenu_ === 'function') { // → 3편 설정 관리
APPT_addSettingsMenu_(menu); // → 설정 항목
}
APPT_addWebAppMenu_(menu); // → 이번 편 웹앱 화면 열기
menu.addToUi(); // → 메뉴를 시트에 붙이기
}이미 1, 2, 3편에서 onOpen()을 만들어 둔 상태라면, 새 onOpen을 또 만들지 마세요. 기존 onOpen을 위 코드로 통째로 바꾸거나, 기존 onOpen 안에 아래 한 줄만 넣으면 됩니다. createMenu를 또 부르면 이름이 같은 메뉴가 두 개 생깁니다.
APPT_addWebAppMenu_(menu); // 기존 onOpen 안, menu.addToUi() 바로 앞에 한 줄정리하면:
- 프로젝트 전체에 onOpen 함수는 하나만 두고,
createMenu('예약 도구')도 한 번만 부르고,- 각 글에서는
addApptBaseMenu_(),APPT_addSeedMenu_(),APPT_addSettingsMenu_(),APPT_addWebAppMenu_()같은 메뉴 전용 도우미 함수만 추가한 뒤, - 그 하나뿐인 onOpen 안에서 도우미를 전부 차례로 호출합니다. 하나라도 빠지면 그 편 메뉴만 조용히 사라집니다.
구글시트 메뉴와 웹앱 주소 연결하기
실제 운영에서는 웹앱 주소를 북마크로만 관리하면 담당자가 바뀌거나 배포 방식이 바뀔 때마다 혼선이 생깁니다. 시트 상단 메뉴에서 바로 웹앱을 열어 주면, 교육·인수인계가 쉬워지고 실수도 줄어듭니다.
여기서는 APPT_getWebAppBaseUrl_()로 현재 배포된 웹앱 주소를 읽어 온 뒤, APPT_openHomeWebApp(), APPT_openCheckinWebApp()에서 해당 주소를 새 탭으로 여는 구조를 만듭니다. 이 코드는 Code.gs의 doGet(e) 함수 아래에 붙여 넣습니다.
// → 이 편에서 추가하는 메뉴 구성
const APPT_MENU_CONFIG = { // → 메뉴 설정 객체
ITEM_OPEN_HOME: '웹앱 메인 화면 열기', // → 홈 화면 메뉴
ITEM_OPEN_CHECKIN: '웹앱 체크인 화면 열기' // → 체크인 화면 메뉴
}; // → 설정 끝
// 메뉴 이름('예약 도구')은 1편이 만든 그대로 onOpen 에서 한 번만 쓴다.
// 여기에 또 두면 두 값이 갈릴 수 있고, 갈리면 메뉴가 두 개 생긴다.
function APPT_addWebAppMenu_(menu) { // → 이번 편 항목을 공용 메뉴에 추가
menu.addItem(
APPT_MENU_CONFIG.ITEM_OPEN_HOME,
'APPT_openHomeWebApp'
);
menu.addItem(
APPT_MENU_CONFIG.ITEM_OPEN_CHECKIN,
'APPT_openCheckinWebApp'
);
}
function APPT_getWebAppBaseUrl_() { // → 현재 웹앱 URL 얻기
var url = ScriptApp
.getService()
.getUrl();
return url;
}
function APPT_openHomeWebApp() { // → 홈 화면 브라우저 열기
var base = APPT_getWebAppBaseUrl_();
if (!base || base === '') {
SpreadsheetApp.getUi().alert(
'웹앱을 배포하지 않았습니다.\n' +
'Apps Script 편집기 → [배포] → [새로운 배포] → 웹 앱 선택 후 배포하세요.\n' +
'배포 후 구글시트를 새로 고침하여 메뉴를 다시 시도하세요.'
);
return;
}
var url = base + '?page=' + encodeURIComponent('home');
var html = HtmlService.createHtmlOutput(
'<script>window.open("' + url + '","_blank");' +
'google.script.host.close();</script>'
);
SpreadsheetApp.getUi().showModalDialog(
html,
'웹앱 메인 화면 열기'
);
}
function APPT_openCheckinWebApp() { // → 체크인 화면 브라우저 열기
var base = APPT_getWebAppBaseUrl_();
if (!base || base === '') {
SpreadsheetApp.getUi().alert(
'웹앱을 배포하지 않았습니다.\n' +
'Apps Script 편집기 → [배포] → [새로운 배포] → 웹 앱 선택 후 배포하세요.\n' +
'배포 후 구글시트를 새로 고침하여 메뉴를 다시 시도하세요.'
);
return;
}
var url = base + '?page=' + encodeURIComponent('checkin');
var html = HtmlService.createHtmlOutput(
'<script>window.open("' + url + '","_blank");' +
'google.script.host.close();</script>'
);
SpreadsheetApp.getUi().showModalDialog(
html,
'웹앱 체크인 화면 열기'
);
}이 코드를 실제로 쓰려면 위에서 만든 onOpen과 연결해야 합니다. onOpen은 프로젝트 전체에 하나뿐이어야 하므로, 위 「시리즈에서 onOpen이 겹치지 않게 합치는 방법」의 코드를 그대로 쓰고 여기서 또 만들지 마세요. 이미 onOpen이 있다면 그 안에 APPT_addWebAppMenu_(menu); 한 줄만 더하면 됩니다.
주의: 메뉴 테스트를 하려면 반드시 웹앱 배포가 먼저 완료되어야 합니다.
동작 확인 순서는 다음과 같습니다.
- Apps Script 편집기에서 [배포] → [새로운 배포]를 클릭하고, 유형을 '웹 앱'으로 선택해 처음 한 번 배포합니다.
- 구글시트를 새로 고침합니다.
- 상단 메뉴에 '예약 도구'가 보이면, 그 안의 '웹앱 메인 화면 열기', '웹앱 체크인 화면 열기'를 각각 눌러 봅니다.
- 각 메뉴를 눌렀을 때 웹 브라우저 새 탭이 열리고, 주소 끝이
?page=home,?page=checkin인 화면이 뜨면 성공입니다.
실무에서 자주 막히는 부분과 Apps Script 활용 팁
입고 예약 시스템을 실제로 돌리면서, 구글시트 웹앱 화면 여러 개를 한 프로젝트에서 관리할 때 자주 나오는 문제와 해결책을 정리하면 다음과 같습니다.
- 웹앱 주소 변경에 따른 링크 깨짐 문제
스크립트 구조를 바꾸거나 배포를 새로 만들면 /exec 주소가 바뀔 수 있습니다. HTML 템플릿 안에 절대 URL을 직접 적어 두면, 예전에 공유한 링크나 즐겨찾기가 한 번에 무용지물이 됩니다. 템플릿 안 링크는 ?page=checkin 같은 상대 경로로 두고, 시트 메뉴·외부 공지에는 APPT_getWebAppBaseUrl_()로 얻은 최신 주소를 쓰면 문제를 줄일 수 있습니다.
- 잘못된 page 파라미터 처리 문제
?page=aaa처럼 오타가 섞인 링크는 실제 현장에서 자주 등장합니다. 이 글의 APPT_WEBAPP_CONFIG.VALID_PAGES처럼 허용 목록을 두고, 그 목록에 없는 값은 모두 기본 화면으로 돌려보내는 식으로 방어 코드를 넣어 두면, page가 비어 있거나 예상 밖 값이 와도 시스템이 멈추지 않습니다.
- 시리즈 코드 간 충돌 문제
이 시리즈 전체를 하나의 Apps Script 프로젝트에 모아도 오류 없이 돌아가도록 다음 원칙을 지키는 것이 좋습니다.
- 예약 시스템 전용 상수·함수 이름에는 반드시
APPT_접두어를 붙입니다. doGet,onOpen처럼 프로젝트당 하나만 허용되는 엔트리 포인트는 기존 것을 지우지 말고, 본문만 라우팅 중심으로 교체·통합합니다.- 공용 도우미 함수는 한 번만 선언하고, 이후 편에서는 재정의하지 않습니다.
오류 처리나 동시 실행 잠금(LockService) 등 운영 단계의 안정성이 필요하다면, 별도 글인 구글시트 Apps Script 오류 처리 백업 방법 | LockService·try/catch·DriveApp 백업을 참고해 방어 로직을 추가하면 도움이 됩니다.
맺음말: 한 번 배포한 주소로 화면을 늘려 가는 구조 만들기
이 글에서는 구글시트 웹앱 화면 여러 개 만들기라는 요구에 맞춰, Apps Script doGet(e)의 page 파라미터로 HTML 템플릿을 라우팅하는 구조와, 시트 메뉴에서 각 화면을 여는 방법을 정리했습니다. APPT_WEBAPP_CONFIG의 VALID_PAGES와 TITLE_MAP으로 화면을 관리하고, APPT_home, APPT_checkin 템플릿을 나눠 두면 화면이 늘어나도 구조를 유지하기가 한결 수월합니다.
바로 해 볼 수 있는 행동은 하나입니다. 지금 쓰는 입고·예약 관련 구글시트가 있다면, Apps Script 편집기를 열어 이 글의 doGet(e)와 APPT_home·APPT_checkin 템플릿, APPT_MENU_CONFIG와 APPT_addWebAppMenu_()까지 그대로 붙여 넣은 뒤, 웹앱을 한 번 배포하고 ?page=checkin까지 접속해 보시기 바랍니다. 한 주소에서 여러 화면이 갈리는 것만 확인해 두면, 이후에는 같은 패턴으로 목록 조회, 관리자용 화면 등도 단계적으로 확장해 나갈 수 있습니다.