jQuery / .ajax() / 동기식으로 사용하는 방법
jQuery.ajax()에서 async: false를 설정하면 요청이 끝날 때까지 현재 실행 흐름을 기다리게 하는 동기식 요청이 됩니다. 다만 브라우저의 주 실행 흐름을 막아 화면 반응이 멈출 수 있으므로 새 코드는 기본 비동기 요청을 사용하고 필요한 후속 작업을 연결하는 편이 적합합니다. ‘첫 응답을 받은 뒤 두 번째 요청’이라는 순서는 동기식으로 바꾸지 않고도 구현할 수 있습니다.
async:false의 의미와 적용 범위
jQuery.ajax() 공식 문서에서 async의 기본값은 true입니다. false는 요청을 빠르게 만드는 설정이 아니라, 응답 전까지 호출 다음 코드로 진행하지 않게 하는 설정입니다. MDN의 동기·비동기 요청 설명도 동기 XMLHttpRequest가 실행을 막아 화면이 응답하지 않는 문제를 설명합니다.
기존 동기 코드를 확인해야 할 때는 success·error·complete 옵션을 사용합니다. jQuery는 1.8부터 async:false와 jqXHR의 Deferred 방식 조합을 사용 중단 권장으로 안내합니다. 이 제한을 모든 $.ajax()나 비동기 .done()이 폐기됐다는 뜻으로 확대하지 않습니다. 이 글의 .then()·.done()·.fail()은 비동기 대안에 사용합니다.
동기식이 해결하려던 문제가 단순한 응답 순서라면, 요청이 끝났을 때 후속 요청을 시작하고 그 요청 객체를 반환해 연결합니다. 독립 요청을 동시에 보내는 것과 앞선 응답 값이 필요한 요청을 순서대로 보내는 것도 구분합니다.
지연 응답을 만드는 로컬 서버와 화면
Python 3와 인터넷 연결을 준비하고 새 연습 폴더를 만듭니다. 먼저 UTF-8의 ajax-server.py를 저장합니다. 파일 제공과 세 API를 한 서버에서 처리해 출처를 맞춥니다. /first는 id 7을 주고, /second는 전달된 parent를 응답에 넣습니다. /slow는 시간 초과 확인을 위해 더 늦게 응답합니다. 지연은 서버 코드에서 의도적으로 넣은 실습 조건입니다.
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
import json
import sys
import time
class Handler(SimpleHTTPRequestHandler):
def do_GET(self):
url = urlparse(self.path)
if url.path not in ("/first", "/second", "/slow"):
return super().do_GET()
time.sleep(2 if url.path == "/slow" else 0.4)
if url.path == "/second":
parent = parse_qs(url.query).get("parent", [""])[0]
data = {"detail": "Details for " + parent}
else:
data = {"id": 7, "name": "Alpha"}
body = json.dumps(data).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
try:
self.wfile.write(body)
except (BrokenPipeError, ConnectionResetError):
pass
if __name__ == "__main__":
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8000
ThreadingHTTPServer(("127.0.0.1", port), Handler).serve_forever()
같은 폴더에 ajax-demo.html을 저장합니다. 공식 jQuery 전체 파일 4.0.0을 사용하며 Ajax가 빠진 slim으로 바꾸지 않습니다. 시작 버튼은 작업 중 비활성화하고 현재 진행 중인 jqXHR만 active로 보관합니다. .then() 안에서 두 번째 요청을 반환해야 뒤의 완료·실패 처리가 두 번째 요청까지 기다립니다.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Keep request order without blocking</title>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<style>
body { font: 16px/1.6 sans-serif; max-width: 760px; margin: 24px auto; padding: 16px; }
button { font: inherit; margin: 4px; max-width: 100%; }
pre { white-space: pre-wrap; overflow-wrap: anywhere; }
</style>
</head>
<body>
<h1>Sequence asynchronous requests</h1>
<button type="button" class="start" id="run">Run two requests</button>
<button type="button" class="start" id="error">First request fails</button>
<button type="button" class="start" id="timeout">First request times out</button>
<button type="button" id="cancel" disabled>Cancel active request</button>
<button type="button" id="reset">Reset</button>
<p id="heartbeat">heartbeat=0</p>
<pre id="result" aria-live="polite">Ready</pre>
<script>
$(function () {
let active = null, busy = false, lines = [], ticks = 0;
setInterval(function () { ticks++; $("#heartbeat").text(`heartbeat=${ticks}`); }, 100);
function log(line) { lines.push(line); $("#result").text(lines.join("\n")); }
function run(path, timeout) {
if (busy) return;
busy = true; lines = [];
$(".start, #reset").prop("disabled", true); $("#cancel").prop("disabled", false);
log("before");
active = $.ajax({ url: path, dataType: "json", timeout: timeout });
active.then(function (first) {
log("first=" + first.name);
active = $.ajax({ url: "/second", data: { parent: first.id }, dataType: "json", timeout: 1000 });
return active;
}).done(function (second) {
log("second=" + second.detail);
}).fail(function (xhr, textStatus) {
log("failed=" + textStatus + "; HTTP=" + xhr.status);
}).always(function () {
log("complete"); active = null; busy = false;
$(".start, #reset").prop("disabled", false); $("#cancel").prop("disabled", true);
});
log("returned to caller");
}
$("#run").on("click", function () { run("/first", 1000); });
$("#error").on("click", function () { run("/missing.json", 1000); });
$("#timeout").on("click", function () { run("/slow", 300); });
$("#cancel").on("click", function () { if (active) active.abort(); });
$("#reset").on("click", function () { lines = []; $("#result").text("Ready"); });
});
</script>
</body>
</html>
초기 출력은 다음과 같습니다. heartbeat 숫자는 100ms 간격의 별도 타이머로 계속 늘어납니다.
Ready
터미널의 현재 폴더를 연습 폴더로 바꾸고 아래 명령을 실행합니다. python이 없다면 설치·실행 경로를 확인합니다. 기본 포트 8000이 사용 중이면 python ajax-server.py 8001처럼 인수를 주고 브라우저 주소도 맞춥니다.
python ajax-server.py
서버 실행 중 http://127.0.0.1:8000/ajax-demo.html로 접속합니다. 파일을 더블클릭하는 file:// 주소로 실행하지 않습니다. jQuery CDN(Content Delivery Network, 콘텐츠 전송망)을 위한 인터넷도 필요합니다. 끝나면 서버 터미널에서 Ctrl+C로 종료합니다. 예제는 Windows Edge, jQuery 3.7.1·4.0.0과 Python 서버에서 실행했고 검증 포트만 사용 가능한 포트로 변경했습니다.
비동기 순서·실패·중지를 확인하기
- Run two requests를 누르면 before와 returned to caller가 먼저 나옵니다. 응답 대기 동안 heartbeat는 계속 증가합니다.
- 이후 first=Alpha, second=Details for 7, complete 순서입니다. Network에서도 /first 응답 뒤 /second?parent=7을 요청하는지 확인합니다. 모든 작업을 동시에 시작하는 코드가 아닙니다.
- First request fails는 없는 파일에 요청해 failed=error; HTTP=404, complete를 표시합니다. 첫 요청이 실패했으므로 두 번째는 시작하지 않습니다.
- First request times out는 /slow의 2초 지연보다 짧은 300ms 제한을 사용해 failed=timeout; HTTP=0을 표시합니다. 이 0은 서버가 응답한 HTTP 상태 코드가 아닙니다.
- 실행 후 Cancel active request를 누르면 현재 요청을 중지하고 failed=abort; HTTP=0으로 정리합니다. 첫 응답 이후 누르면 active가 가리키는 두 번째 요청을 중지합니다.
- 작업이 끝나면 시작·초기화 버튼이 다시 활성화됩니다. Reset은 출력만 Ready로 돌리고 heartbeat는 계속 둡니다. 실패 후 정상 실행도 다시 확인합니다.
.always()는 성공과 실패 어느 쪽에서도 버튼과 busy 상태를 정리합니다. 성공 경로에서만 버튼을 되살리면 실패 후 다시 실행하지 못할 수 있습니다. 반환한 연결 객체와 실제 중지 가능한 jqXHR를 구분해 active는 각 $.ajax()의 반환값으로 갱신합니다. 요청 중지는 이미 서버에서 처리한 작업까지 되돌린다는 뜻은 아닙니다.
기존 동기식 코드의 실행 순서 확인하기
위 로컬 서버가 실행 중이고 다른 요청이 끝난 상태에서 개발자 도구 Console에 다음 코드를 한 번 실행합니다. async:false를 기존 코드에서 읽을 때의 동작을 확인하는 예제입니다. 응답은 이 서버의 /first만 사용하며 인터넷의 느린 외부 주소로 바꾸지 않습니다.
console.log("before synchronous request");
setTimeout(function () { console.log("timer after synchronous request"); }, 0);
$.ajax({
url: "/first",
dataType: "json",
async: false,
success: function (data) { console.log("success=" + data.name); },
error: function (xhr, textStatus) { console.log("failed=" + textStatus); }
});
console.log("after synchronous request");
before synchronous request
success=Alpha
after synchronous request
timer after synchronous request
success가 호출 다음 줄보다 먼저 실행되고 0ms 타이머는 동기 호출과 그 뒤 코드가 끝난 뒤 실행됩니다. 0ms는 즉시 끼어들어 실행한다는 뜻이 아닙니다. 브라우저가 동기 요청에 대한 사용 중단 경고를 표시할 수 있으며, 경고를 숨기는 것을 해결책으로 삼지 않습니다.
화면에 Loading 문구를 쓴 다음 동기 요청을 시작해도 브라우저가 그 문구를 그리거나 타이머·사용자 입력을 처리할 기회를 충분히 갖지 못할 수 있습니다. 비동기 예제의 heartbeat와 비교하면 ‘서버 응답을 기다린다’는 요구와 ‘주 실행 흐름을 막는다’는 동작의 차이를 이해하기 쉽습니다.
기존 코드를 바꿀 때 확인할 부분
응답을 외부 변수에 저장한 뒤 호출 직후 반환하던 코드를 async:true로만 바꾸면 값이 준비되기 전에 읽을 수 있습니다. 응답을 쓰는 코드까지 완료 경로 안으로 옮기거나 요청·Promise를 반환해 호출자가 기다리도록 구조를 바꿉니다. .then()에서 다음 요청을 만들기만 하고 반환하지 않으면 연결이 그 요청을 기다리지 않는 점도 확인합니다.
비동기 예제의 timeout은 응답 형식 오류, 네트워크 오류, 사용자 중지와 다른 결과입니다. 실제 프로그램에서는 서버 응답의 형태와 필요한 값도 확인한 후 다음 작업에 사용합니다. 동기 요청에 시간 제한이나 취소 옵션을 덧붙이는 것만으로 주 실행 흐름이 막히는 문제가 없어지지는 않습니다. 폼 전송·저장처럼 중복 실행 영향이 있는 작업은 실패 뒤 재시도 정책도 작업 성격에 맞게 정합니다.




