Client Script onSubmit 에서 GlideAjax 를 동기처럼 막는 법
onSubmit 의 return false 차단 모델과 GlideAjax 비동기 default 가 충돌하는 문제. getXMLWait 가 죽은 이후 권장 패턴, async/await 함정, UI Action 옵션 비교.
개요
onSubmit 에서 서버 검증이 필요하다면, 첫 번째 호출에서 return false 로 폼 제출을 차단하고, GlideAjax 콜백 안에서 결과에 따라 g_form.submit() 을 재호출하는 패턴을 사용한다. getXMLWait 는 브라우저 정책 변화와 Service Portal 미지원으로 더 이상 권장되지 않으며, async/await 는 ServiceNow 의 차단 모델과 구조적으로 맞지 않아 사용해서는 안 된다.
OOTB(Out-of-the-Box, 기본 제공) Australia 기준입니다. 인스턴스 버전·플러그인 구성에 따라 동작이 달라질 수 있습니다.
1. 문제 — return false 와 비동기 default 의 충돌
ServiceNow 의 onSubmit Client Script 는 폼 제출을 차단하는 단 하나의 신호를 가진다. 함수가 정확히 false 를 반환할 때, ServiceNow 는 해당 submit 이벤트를 막는다. 그 외 모든 반환값 — undefined, null, 0, Promise 객체 — 은 차단 신호로 인식되지 않고 폼이 그대로 제출된다.
문제는 현실에서 가장 자주 만나는 시나리오가 서버를 거쳐야만 하는 검증이라는 점이다. 예를 들어 “사용자가 입력한 코드 값이 다른 테이블에 이미 존재하는지 확인”하는 중복 검사가 필요하다고 하자. 이 판단은 클라이언트 측 정보만으로는 불가능하고, GlideAjax 로 Script Include 를 호출해 서버에 물어봐야 한다. 그런데 GlideAjax 의 기본 동작은 비동기다. getXMLAnswer(callback) 을 호출하는 순간 함수는 즉시 반환되고, 서버 응답은 나중에 콜백 함수를 통해 전달된다. 그 사이에 onSubmit 함수는 이미 undefined 를 반환하고 종료된 상태이므로, 폼은 차단되지 않고 서버로 날아간다.
이것이 핵심 충돌이다. onSubmit 의 차단 모델은 동기적(synchronous)이고, GlideAjax 의 기본 동작은 비동기적(asynchronous)이다. 이 둘을 아무 장치 없이 조합하면 검증 로직이 응답을 받기도 전에 폼 제출이 완료된다. 해결책을 찾기 위해 가장 먼저 떠올리는 옵션이 getXMLWait 이지만, 이 메서드는 현재 시점에서 더 이상 실용적인 선택이 아니다.
2. 패턴 A — getXMLWait, 왜 더 이상 답이 아닌가
getXMLWait 는 GlideAjax 요청을 동기로 처리하도록 강제하는 메서드다. 브라우저가 서버 응답을 받을 때까지 스크립트 실행을 멈추고 기다리므로, onSubmit 함수 안에서 결과를 바로 읽고 true 또는 false 를 반환할 수 있었다. 과거 ServiceNow 개발자 커뮤니티에서는 이 메서드를 onSubmit 검증의 표준 해법처럼 다루는 글이 많았다.
그러나 현재 이 메서드에는 세 가지 구조적 문제가 있다.
브라우저 정책 변화. Chrome 80(2020년 초 배포)부터 메인 스레드의 동기 XMLHttpRequest 는 페이지 언로드(page dismissal) 단계에서 차단되며, 콘솔에 deprecation 경고가 출력된다. 폼 submit 은 사실상 페이지 이동 직전 단계와 맞물리므로, getXMLWait 가 제대로 동작하지 않거나 브라우저에 따라 완전히 무시될 수 있다. Firefox 와 Safari 역시 비슷한 방향으로 동기 XHR 지원을 줄여가고 있는 흐름이다.
Service Portal 미지원. Service Portal 은 AngularJS 기반으로 렌더링된다. 이 환경에서는 동기 XHR 자체를 지원하지 않아, getXMLWait 를 호출하면 에러가 발생하거나 동작이 무시된다. 모바일 앱이나 Portal 기반 Catalog 를 운영하는 조직에서는 처음부터 이 방법을 선택지에서 제외해야 한다.
Scoped Application 제약. ServiceNow 는 Scoped Application 내에서 getXMLWait 사용을 허용하지 않는다. 스코프드 환경에서 이 메서드를 호출하면 명시적 오류가 발생한다. 신규 개발 대부분이 Scoped Application 구조를 권장받는 현재 상황에서, 처음부터 사용이 막힌 메서드에 의존하는 것은 기술 부채를 쌓는 일이다.
실무에서는 세 가지 제약 중 하나만 걸려도 패턴이 무너진다. 인스턴스가 Service Portal 을 사용하거나 스코프드 앱 구조를 따른다면 처음부터 선택지에서 제외하는 것이 맞다.
3. 패턴 B — return false → callback → g_form.submit() 재호출 (권장)
현재 가장 안정적으로 쓰이는 패턴은 플래그(flag) 기반 두 번 호출 방식이다. 아이디어 자체는 단순하다. 첫 번째 onSubmit 호출에서 폼을 막고 서버 검증을 시작한다. 검증이 끝나면 프로그래밍 방식으로 폼을 다시 제출하되, 두 번째 호출임을 알 수 있는 표시를 남겨 무한 루프를 방지한다.
흐름을 세 단계로 정리하면 이렇다.
첫 번째 onSubmit 호출 시 플래그가 없으므로 분기를 건너뛴다. GlideAjax 요청을 시작하고, return false 로 폼 제출을 막는다. 두 번째, 서버 응답이 콜백으로 돌아오면 결과를 확인한다. 유효하면 플래그를 설정하고 g_form.submit(action) 을 호출한다. 유효하지 않으면 에러 메시지를 표시하고 종료한다. 세 번째, g_form.submit() 이 onSubmit 을 다시 발화시키면, 이번에는 플래그가 있으므로 즉시 return true 를 반환하고 폼이 정상 제출된다.
function onSubmit() {
if (g_scratchpad._ajaxChecked) {
return true;
}
g_scratchpad._action = g_form.getActionName();
var ga = new GlideAjax('MyScriptInclude');
ga.addParam('sysparm_name', 'validateThis');
ga.addParam('sysparm_value', g_form.getValue('u_my_field'));
ga.getXMLAnswer(function(answer) {
if (answer === 'valid') {
g_scratchpad._ajaxChecked = true;
g_form.submit(g_scratchpad._action);
} else {
g_form.addErrorMessage('검증 실패: 입력값을 확인하세요.');
}
});
return false;
}
위 코드에서 MyScriptInclude 는 가상 표기다. 실제 Script Include 명칭은 인스턴스마다 다르며, 별도로 서버 사이드 Script Include 를 작성해야 한다. validateThis 메서드도 마찬가지로 직접 구현이 필요하다.
흐름 시각화
함정 세 가지
이 패턴은 구조가 단순해 보이지만, 현실적으로 가장 자주 만나는 실수 지점이 세 곳 있다.
함정 1 — onLoad 에서 g_scratchpad 초기화 누락. g_scratchpad 는 페이지가 새로 로드될 때마다 초기화되는 것이 일반적이다. 그러나 Service Portal 같은 SPA(Single-Page Application) 방식의 환경에서는 폼 컴포넌트가 새로 마운트되지 않고 재사용되는 경우가 있다. 이때 이전에 검증을 통과했던 세션의 _ajaxChecked 플래그 값이 남아 있으면, 새 레코드를 입력해도 검증을 건너뛰고 곧바로 제출된다. onLoad Client Script 에서 g_scratchpad._ajaxChecked = false 로 명시 초기화하는 것이 방어적인 습관이다.
함정 2 — 콜백 안에서 조건 없이 g_form.submit() 호출. 콜백이 실행됐다는 사실은 “서버가 응답을 보냈다”는 의미일 뿐, “검증을 통과했다”는 의미가 아니다. 서버에서 실패 응답을 받았음에도 불구하고 콜백 안에서 조건 없이 무조건 폼을 제출하는 실수를 생각보다 자주 본다. 반드시 응답값을 확인하는 분기 조건 안에서만 g_form.submit() 을 호출해야 한다.
함정 3 — _action 저장 누락. ServiceNow 폼에는 Save, Submit, Update 등 여러 UI Action 버튼이 있을 수 있다. 각 버튼은 서로 다른 action 이름을 가지며, g_form.submit(action) 호출 시 이 이름을 함께 넘겨야 원래 의도한 액션으로 제출된다. 사용자가 누른 버튼의 액션 이름을 GlideAjax 호출 전에 g_scratchpad._action = g_form.getActionName() 으로 저장해두지 않으면, 콜백에서 액션 없이 g_form.submit() 을 호출하게 되어 기본 Submit 동작으로만 처리되거나 예상치 못한 버튼 동작 변형이 생길 수 있다.
Catalog Item 변형. Service Catalog item 의 경우 g_form.submit() 대신 g_form.orderNow() 를 사용한다. 플래그 기반 흐름은 동일하게 적용된다.
4. 패턴 C — async/await onSubmit 의 함정
최근 JavaScript 배경이 강한 개발자들이 자주 시도하는 방식이 async function onSubmit() 이다. await 키워드로 GlideAjax 응답을 기다리면 검증 결과를 동기처럼 읽고 false 를 반환할 수 있을 것 같다는 직관이 작동하기 때문이다. 그러나 이 접근은 ServiceNow 의 차단 모델과 근본적으로 충돌한다.
async 함수는 항상 Promise 를 반환한다. 함수 본문에서 return false 를 작성해도, 실제로 함수가 반환하는 값은 Promise<false> — Promise 객체다. JavaScript 에서 객체(object)는 truthy 값이다. ServiceNow 가 onSubmit 의 반환값을 평가할 때 false 가 아닌 truthy Promise 객체를 받게 되므로, 차단 신호가 없는 것으로 판단하고 폼이 바로 제출된다. ServiceNow 런타임은 이 Promise 를 await 하지 않기 때문에 비동기 검증 로직이 완료되기를 기다리지도 않는다.
여기서 자연스러운 의문이 따라붙는다. 그러면 ServiceNow 가 반환된 Promise 를 인식해서 자동으로 await 해주면 되지 않나? 답은 그렇지 않다. ServiceNow 의 Client Script 실행 엔진은 onSubmit 핸들러의 반환값을 동기적으로 평가한다. 함수 호출이 끝나는 즉시 그 자리에서 반환값을 검사해 차단 여부를 결정하므로, 반환된 Promise 가 resolved 상태인지 pending 상태인지에는 관심이 없다. 일반적인 React, Vue, Express 같은 모던 JS 프레임워크에서는 비동기 핸들러를 받아들이기 위해 프레임워크 측이 내부에서 await 를 처리해주지만, ServiceNow 의 Client Script 런타임에는 그런 계층이 존재하지 않는다. 즉 async 키워드가 의미를 가지려면 호출자(caller) 가 비동기성을 인식하고 기다려줘야 하는데, ServiceNow 는 그렇게 설계되어 있지 않다.
결국 async function onSubmit() { await someCheck(); return false; } 는 작성 의도와 달리 폼을 전혀 차단하지 못하는 코드가 된다. Promise 반환 구조를 정확히 이해하고 있는 개발자라도 onSubmit 의 특수한 차단 모델을 놓치면 이 함정에 빠진다. 패턴 B 처럼 동기 반환 기반 플래그와 콜백 방식이 여전히 올바른 이유가 여기에 있다.
5. UI Action 으로 옮기는 옵션
onSubmit 에서 검증을 처리하는 대신, Client UI Action 의 onclick 핸들러에서 GlideAjax 를 직접 호출하는 방식도 있다. UI Action 은 버튼 클릭 이벤트를 직접 처리하므로, 비동기 콜백이 끝난 뒤에 별도 로직으로 폼 제출을 트리거하거나 차단할 수 있다. onSubmit 의 비동기 충돌 문제를 우회할 수 있다는 점에서 이 방식이 더 자유롭게 느껴질 수 있다.
구체적인 흐름은 이렇다. 사용자가 버튼을 누르면 UI Action 의 onclick 함수가 실행되고, 그 안에서 GlideAjax 를 비동기로 호출한다. 콜백이 도착해 검증이 통과하면 gsftSubmit(null, g_form.getFormElement(), '<action_name>') 같은 헬퍼로 폼을 직접 제출한다. 검증이 실패하면 g_form.addErrorMessage 로 사용자에게 알리고 함수가 종료된다. onSubmit 패턴과 달리 폼 자체는 처음부터 한 번도 submit 흐름에 진입하지 않으므로 return false 같은 차단 신호가 필요하지 않고, 플래그도 두 번 호출도 없다.
그러나 선택 전에 트레이드오프를 명확히 짚어야 한다. onSubmit 패턴은 레코드 폼의 모든 submit 경로 — Save 버튼, Submit 버튼, 키보드 단축키, 커스텀 UI Action 버튼 등 — 에 동일하게 적용된다. 반면 UI Action 방식은 해당 버튼 하나를 통한 제출에만 적용되므로, 표준 Save 버튼이나 다른 액션 경로를 통해 검증을 우회할 수 있다. 이를 막으려면 표준 버튼을 숨기거나 비활성화하는 추가 작업이 필요하다.
결론을 간단히 정리하면 이렇다. 검증이 “모든 제출 경로에 걸려야 한다”면 onSubmit 패턴 B 가 구조적으로 우위다. “특정 액션에서만 서버 검증이 필요하고 나머지 경로는 자유롭게 두어도 된다”면 UI Action 방식이 오히려 더 명확한 설계일 수 있다.
6. 권장 패턴 정리
| 방식 | Service Portal | Scoped App | 모든 submit 경로 적용 | 현황 |
|---|---|---|---|---|
getXMLWait | 미지원 — AngularJS 환경에서 동기 XHR 차단됨 | 사용 불가 — ServiceNow 정책으로 명시 차단됨 | 가능 (제약 없는 환경 한정) | 비권장, 브라우저 deprecated |
async/await onSubmit | 폼 차단 불가 — Promise 반환이 truthy 로 평가됨 | 폼 차단 불가 — 동일한 구조적 이유 | 불가 — 비동기 반환으로 차단 신호 무력화 | 사용 금지 |
| 패턴 B (플래그 + 콜백) | 지원 — 표준 비동기 콜백 패턴 | 지원 — 스코프 관련 제약 없음 | 가능 — 모든 submit 경로에 자동 적용 | 권장 |
| Client UI Action | 지원 | 지원 | 해당 액션에만 선택적 적용 | 특정 액션 전용 검증에 합리적 |
세 가지 함정 — onLoad 초기화 누락, 조건 없는 g_form.submit() 호출, _action 저장 누락 — 을 인지하고 코드에 반영하면, 패턴 B 는 Service Portal 을 포함한 대부분의 환경에서 안정적으로 동작한다. getXMLWait 가 동작하던 시절의 코드를 유지보수하고 있다면, 점진적으로 패턴 B 로 교체하는 것을 권장한다.
서버 사이드의 자동화 함정은 별도 글에서 다룬다 — Business Rule 무한 루프와 setWorkflow(false) 우회 가 current.update() 재트리거와 차단 메커니즘을 정리한다.
참조
- getXMLWait Alternative for Service Portal (ServiceNow Community)
- GlideAjax getXMLWait is no longer supported (ServiceNow Community Forum)
- Chrome 80 Deprecations and Removals (Chrome Developers)
- How to Async GlideAjax in an onSubmit Script (ServiceNow Developer Blog)
- Synchronous-lite onSubmit Catalog Client Scripts (SN Pro Tips)
- Stopping Record Submission in ServiceNow (ServiceNow Guru)